Skip to main content

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.

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.
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:
export type LocalEntity  = Entity  -- the entity ID inside the current world
export type LookupEntity = Entity  -- the stable sequential index used in packets
A 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

Every Group (including the default group embedded in a Feces instance) carries two tables:
-- Group type (from types.luau)
export type Group = {
    origin:  string,                            -- debug label, e.g. file:line
    size:    number,                            -- max entities before a warning
    lookups: { [LocalEntity]:  LookupEntity },  -- local  → stable index
    refs:    { [LookupEntity]: LocalEntity  },  -- stable → local
}
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.
local entity = world:entity()
world:set(entity, Transform, CFrame.new(0, 10, 0))
world:add(entity, feces.replicated)

local id = feces:lookup(entity)
-- id is now a stable integer, e.g. 1
-- safe to include in any network packet
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.
-- Client receives a packet and resolves entity 1
local localEntity = clientFeces:ref(1)
-- If entity 1 was seen before, returns the same local entity every time.
-- If it's new, a fresh local entity is created and remembered.
Because 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.
-- Server
local npcGroup = feces.reserve(512)

local npc = world:entity()
feces:setGroup(npcGroup, npc)  -- register npc in the dedicated group

local changes, deleted = feces:delta(npcGroup)  -- only npc-group entities
When you call 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(): calls lookup() on every changed entity to obtain its ID before writing it into the Changes table.
  • Client — apply(): calls ref() for every LookupEntity in the incoming Applyable, creating local entities on demand.
You only need to call 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

-- SERVER
local serverFeces = feces.new(jecs, serverWorld)

local entity = serverWorld:entity()
serverWorld:set(entity, Health, 100)
serverWorld:add(entity, serverFeces.replicated)

-- feces:delta() internally calls lookup(entity) → returns 1
local changes, deleted = serverFeces:delta()
local packets = feces.combine(changes, deleted)

for player, packet in packets do
    ReplicationRemote:FireClient(player, packet)
end


-- CLIENT
local clientFeces = feces.new(jecs, clientWorld)

ReplicationRemote.OnClientEvent:Connect(function(packet)
    -- feces:apply() internally calls ref(1)
    -- → creates a new local entity the first time
    -- → reuses the same local entity on subsequent calls
    clientFeces:apply(packet)
end)
The server never sends local entity IDs. The client never generates lookup IDs. The sequential integer 1 flows safely across the network and both sides agree on which entity it refers to through their respective lookup tables.

Build docs developers (and LLMs) love