In jecs, entity IDs are local to each world — the same conceptual entity on the server and the client will have different numeric IDs, because each world allocates IDs independently starting from its own internal counter. Sending a raw jecs entity ID over the network and expecting the client to use it directly would silently produce wrong results or collide with existing client entities. feces solves this by maintaining a bidirectional lookup table that maps local entity IDs to stable, sequential lookup IDs that are safe to send over the network.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/NeonD00m/feces/llms.txt
Use this file to discover all available pages before exploring further.
Two jecs worlds are completely independent; there is no shared ID space. Even
if you create components and entities in the same order on both server and
client, the numeric IDs will match only by coincidence and you should never
rely on that. The lookup table is the only safe bridge.
LocalEntity vs LookupEntity
feces defines two distinct type aliases in types.luau to make the distinction explicit in code:
LocalEntity is only meaningful within the world it was created in. A LookupEntity is a sequential integer (1, 2, 3, …) that has no meaning inside any world on its own — it is purely a key for the lookup tables.
The Lookup Tables
EveryGroup (including the default group embedded in a Feces instance) carries two tables:
lookups is the forward direction (server uses this to find what index to put in a packet) and refs is the reverse direction (client uses this to find or create the local entity for an incoming index).
feces:lookup(entity) — Server Side
lookup() takes a LocalEntity and returns its LookupEntity. If the entity has not been registered yet, it is inserted into the group’s refs array with table.insert and then table.find is called on that array to retrieve the resulting sequential position, which becomes the entity’s permanent lookup ID.
The sequential index is determined by the insertion position in the
refs
array, not by an independent counter. This means lookup IDs are compact and
gap-free as long as entities are only added and never removed from a group.You rarely need to call
lookup() manually. During feces:delta(), feces
calls it automatically for every changed entity before placing it in the
Changes table. Calling it early simply ensures the entity is pre-registered
in the group before the first delta.feces:ref(key) — Client Side
ref() is the inverse. Given a LookupEntity received from the server, it returns the corresponding LocalEntity in the client world. If no local entity exists yet for that lookup ID, ref() creates a brand-new entity with world:entity(), registers it in both refs and lookups, and returns it.
ref() is idempotent for known IDs, calling it multiple times with the same lookup ID always returns the same local entity.
Groups and Namespace Isolation
feces.reserve(size) creates a standalone Group with its own empty refs and lookups tables. This lets you partition your entity space — for example, keeping NPC entities and player-character entities in separate groups so their lookup IDs do not interfere with each other.
delta(group) or apply(packet, group), feces uses that group’s refs/lookups tables instead of the default ones. Entities not registered in the provided group are silently skipped during delta.
Automatic Registration
In normal usage the lookup tables are managed entirely by feces:- Server —
delta(): callslookup()on every changed entity to obtain its ID before writing it into theChangestable. - Client —
apply(): callsref()for everyLookupEntityin the incomingApplyable, creating local entities on demand.
lookup() or ref() directly when building custom tooling — for example, pre-populating a group before the first delta, or resolving an entity ID for a non-replication purpose.
End-to-End Example
1 flows safely across the network and both sides agree on which entity it refers to through their respective lookup tables.