-- ===================================================================== -- lib-core.inventory-list v0.1.0 — List-Container Inventory -- -- Manages a composition-entity acting as a list-container. -- Items are attached as synthetic engine.entity children under -- monotonically-increasing slot names (item.1, item.2, ...). -- -- Surface: -- inventory.add(container, item) -- inventory.remove(container, item) -> item -- inventory.contents(container) -> {item, ...} -- inventory.contains(container, item) -> bool -- inventory.count(container) -> number -- -- Deps: -- lib-core.composition (set_tag, get_container) -- engine.entity.* (attach, detach, get_children, get_parent) -- ===================================================================== local composition = require("lib-core.composition") local M = {} -- ---------- internal helpers ---------- -- Compute the next synthetic slot name for the given container. -- Scans existing children for slots matching "item." and returns -- "item.". This is fully derivable from the tree state, -- so no counter needs to be persisted — reconstruction-safe after load. local function next_slot(container) local max_n = 0 for slot_name, _ in pairs(container:get_children()) do local n = tonumber(slot_name:match("^item%.(%d+)$")) if n and n > max_n then max_n = n end end return string.format("item.%d", max_n + 1) end -- Return a stable entity identity key for comparison. -- Uses composition.reg_id (a monotonic integer set on all composition -- entities). Falls back to nil for raw entities. local function entity_id(e) local rid = e:get_property("composition.reg_id") -- reg_id is 0 for non-composition entities (or unset); treat as nil. if rid and rid ~= 0 then return rid end return nil end -- Find the slot name under which `item` is attached to `container`. -- Returns the slot string or nil if not found. -- Compares by composition.reg_id because get_children() returns fresh -- userdata wrappers (no __eq on entity userdata — raw pointer differs). local function slot_of(container, item) local item_rid = entity_id(item) -- get_children() returns fresh userdata wrappers each call; no cache. for slot_name, child in pairs(container:get_children()) do local child_rid = entity_id(child) if item_rid ~= nil and child_rid == item_rid then return slot_name end end return nil end -- Validate that container is a composition-entity with container-block -- kind="list" and no constraint fields. Errors loudly on violation. local function validate_container(container) local block = composition.get_container(container) if block == nil then error("inventory.add: entity has no container block " .. "(template must declare container={kind='list'})") end -- Defensive: composition.define_template currently rejects non-list -- kinds at template-load, so this branch is unreachable via normal flow. -- Kept as forward-protection if composition relaxes that constraint. if block.kind ~= "list" then error(string.format( "inventory.add: container.kind must be 'list' (got '%s')", tostring(block.kind))) end -- Defensive: composition v0.2 rejects declared constraint fields at -- template-load (weight_max/volume_max/grid/accepts_fluid/accepts_gas/ -- restrictions), so this branch is unreachable via normal flow. Kept -- as forward-protection if composition relaxes that constraint. local constraint_fields = { "weight_max", "volume_max", "grid", "accepts_fluid", "accepts_gas", "restrictions", } for _, field in ipairs(constraint_fields) do if block[field] ~= nil then error(string.format( "inventory.add: container declares '%s' which is a " .. "Phase-F/G constraint not supported in v0.1", field)) end end end -- ---------- public API ---------- -- add(container, item) -- Validates container (list, no constraints) and item (has stack_mode, -- stack_mode == "individual"). Detaches item from any current parent, -- attaches to container under a fresh synthetic slot, hides from -- renderable index. function M.add(container, item) -- 1. Validate container. validate_container(container) -- 2. Validate item has stack_mode. local sm = item:get_property("stack_mode") if sm == nil or sm == "" then error("inventory.add: entity is not an item (no 'stack_mode' property)") end -- 3. Validate stack_mode == "individual". if sm ~= "individual" then error(string.format( "inventory.add: stack_mode='%s' is not supported in v0.1 " .. "(only 'individual')", tostring(sm))) end -- 4. Detach from current parent if any. local old_parent = item:get_parent() if old_parent ~= nil then local old_slot = slot_of(old_parent, item) if old_slot then entity.detach(old_parent, old_slot) end end -- 5. Determine new slot and attach. local slot = next_slot(container) entity.attach(container, slot, item) -- 6. Remove from renderable index. composition.set_tag(item, "renderable", false) end -- remove(container, item) -> item -- Detaches item from container, restores renderable tag, returns item. -- Loud-Error if item is not in container. function M.remove(container, item) local slot = slot_of(container, item) if slot == nil then error("inventory.remove: item is not in this container") end entity.detach(container, slot) composition.set_tag(item, "renderable", true) return item end -- contents(container) -> {item, ...} -- Returns a stable-ordered list of all items in the container. -- Filters children by item-recognition rule (has stack_mode property). -- Sorted by the numeric suffix of the synthetic slot for stable ordering. function M.contents(container) local children = container:get_children() -- Collect slot/entity pairs whose slot matches item.. local entries = {} for slot_name, child in pairs(children) do local n = tonumber(slot_name:match("^item%.(%d+)$")) if n ~= nil then -- Item-recognition: entity must have stack_mode property. local sm = child:get_property("stack_mode") if sm ~= nil and sm ~= "" then table.insert(entries, {n = n, entity = child}) end end end -- Sort by n for stable ordering. table.sort(entries, function(a, b) return a.n < b.n end) local out = {} for _, entry in ipairs(entries) do table.insert(out, entry.entity) end return out end -- Returns true iff `item` is currently in `container`. function M.contains(container, item) return slot_of(container, item) ~= nil end -- Returns the number of items currently in `container`. function M.count(container) return #M.contents(container) end return M