From a814b10ef2bb77e58522b6d1f40cbe11ec32a8cc Mon Sep 17 00:00:00 2001 From: Calic Date: Sat, 13 Jun 2026 17:35:21 +0200 Subject: [PATCH] fix: use composition.reg_id for entity identity in slot_of get_children() returns fresh userdata wrappers without __eq; raw == compares Lua pointers (fails across separate get_children calls). slot_of now reads composition.reg_id from each child to find the matching slot, making contains/remove reliable across wrapper instances. --- README.md | 10 ++++++++++ init.lua | 31 +++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e417745..29ffc5e 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,11 @@ Non-item children (future use) are silently excluded. **Syntax:** `inventory.contains(container: entity, item: entity) -> bool` +**Example:** +```lua +local has_rock = inventory.contains(backpack, rock) +``` + **Description:** Returns `true` if `item` is currently a child of `container` under a synthetic slot. @@ -104,6 +109,11 @@ Non-item children (future use) are silently excluded. **Syntax:** `inventory.count(container: entity) -> number` +**Example:** +```lua +local n = inventory.count(backpack) +``` + **Description:** Returns the number of items currently in `container`. Equivalent to `#inventory.contents(container)`. diff --git a/init.lua b/init.lua index e1c5468..96670d5 100644 --- a/init.lua +++ b/init.lua @@ -36,11 +36,26 @@ local function next_slot(container) 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 - if child == item then + local child_rid = entity_id(child) + if item_rid ~= nil and child_rid == item_rid then return slot_name end end @@ -55,14 +70,18 @@ local function validate_container(container) 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 - -- Double-belt: reject any declared constraint fields (composition v0.2 - -- already rejects these at template-load, but we guard here too so the - -- lib is safe against containers created outside the composition gateway). + -- 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", @@ -159,12 +178,12 @@ function M.contents(container) return out end --- contains(container, item) -> bool +-- Returns true iff `item` is currently in `container`. function M.contains(container, item) return slot_of(container, item) ~= nil end --- count(container) -> number +-- Returns the number of items currently in `container`. function M.count(container) return #M.contents(container) end