initial: inventory-list v0.1.0 — list-container inventory over engine entity-tree

Provides add/remove/contents/contains/count over composition entities
acting as list containers. Items attach as synthetic children under
item.<n> slots; renderable tag toggled via composition.set_tag on
add/remove. Slot counter is reconstruction-safe (derived from
get_children scan, no persisted state). Depends on lib-core.composition
0.2.0 (get_container + set_tag).
This commit is contained in:
Calic
2026-06-13 17:31:52 +02:00
commit 6822184c46
4 changed files with 331 additions and 0 deletions

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
Copyright (c) 2026 Calic. All rights reserved.
This software is part of the Sporel platform — **Tier 1 (Official /
Proprietary)** content per the Three-Tier Licensing Model documented in
`meta/docs/archive/design/vision.md §Licensing Model` (current source;
migration to `meta/docs/architecture/licensing-model.md` pending).
⚠ **WIP — Legal review required before public launch.** The terms below
reflect design intent only; the formalized license framework will be
finalized through legal counsel before the first public release. Until
then, this notice serves as a placeholder defending the platform owner's
rights against unintentional re-licensing.
No license is granted to copy, modify, distribute, sublicense, or otherwise
use this software in any form without prior written permission from the
copyright holder.
References:
- Tier 1 (this file): all rights reserved, proprietary, sold/distributed
via official channels (Steam, etc.)
- Tier 2 (Semi-Commercial Co-Development): bilateral contracts, revenue-
share — see vision.md §Licensing Model
- Tier 3 (Community Content): CC BY-NC-SA 4.0 + asymmetric CLA — applies
to community-uploaded libs/modules/assets, not this repo

134
README.md Normal file
View File

@@ -0,0 +1,134 @@
# lib-core.inventory-list
List-container inventory over the `engine.entity` tree. Attaches items
as synthetic children under slot names `item.1`, `item.2`, ... and
manages the composition tag-index so items are hidden from the renderable
index while inside a container.
**Version:** 0.1.0
**Lib-ID:** lib-core.inventory-list
**Requires:** lib-core.composition 0.2.0
**Tags:** inventory, entity, composition, list-container
## Topology
<!-- topology:start (auto-generated; do not edit) -->
```mermaid
graph LR
this["lib-core.inventory-list"]
comp["lib-core.composition"]
engine["engine.*"]
this --> comp
this --> engine
```
<!-- topology:end -->
## Scope (v0.1.0)
Thin list-container inventory spine. Items must be `stack_mode="individual"`.
No weight, volume, or grid constraints (Phase F/G). No stackable items (Phase C+).
**Supported:**
- `add(container, item)` — attach item to container under synthetic slot
- `remove(container, item)` — detach item, return it
- `contents(container)` — ordered list of items currently in container
- `contains(container, item)` — membership check
- `count(container)` — number of items
**Deferred (Loud-Error or not applicable):**
- Stack items (`stack_mode != "individual"`) → Loud-Error in v0.1
- Capacity / weight / volume constraints → Phase F/G
## API
### `inventory.add(container, item)`
**Syntax:** `inventory.add(container: entity, item: entity) -> void`
**Example:**
```lua
local inventory = require("lib-core.inventory-list")
inventory.add(backpack, sword)
-- sword is now a child of backpack under "item.1"
-- sword is removed from the renderable index
```
**Description:** Validates that `container` is a composition-entity with
`container={kind="list"}` (Loud-Error if not). Validates `item` has
`stack_mode` property (Loud-Error if missing) and `stack_mode == "individual"`
(Loud-Error otherwise). Detaches `item` from its current parent if any,
attaches to `container` under the next synthetic slot (`item.<n>`), and
calls `composition.set_tag(item, "renderable", false)` to remove the item
from the renderable index.
### `inventory.remove(container, item) -> item`
**Syntax:** `inventory.remove(container: entity, item: entity) -> entity`
**Example:**
```lua
local dropped = inventory.remove(backpack, sword)
-- sword detached from backpack; renderable tag restored
-- caller re-parents dropped into world + sets position
```
**Description:** Finds `item` in `container`'s children (Loud-Error if not
present), detaches it via `entity.detach`, calls
`composition.set_tag(item, "renderable", true)`, returns `item`.
### `inventory.contents(container) -> {item, ...}`
**Syntax:** `inventory.contents(container: entity) -> {entity, ...}`
**Example:**
```lua
for _, it in ipairs(inventory.contents(backpack)) do
print(it:get_property("name"))
end
```
**Description:** Returns all items currently in `container`, ordered by
insertion order (numeric suffix of synthetic slot names). Filters children
by the item-recognition rule: child must have a `stack_mode` property.
Non-item children (future use) are silently excluded.
### `inventory.contains(container, item) -> bool`
**Syntax:** `inventory.contains(container: entity, item: entity) -> bool`
**Description:** Returns `true` if `item` is currently a child of
`container` under a synthetic slot.
### `inventory.count(container) -> number`
**Syntax:** `inventory.count(container: entity) -> number`
**Description:** Returns the number of items currently in `container`.
Equivalent to `#inventory.contents(container)`.
## Notes
### Synthetic Slot Naming
Items are stored under slot names `item.<n>` where `n` is a
monotonically-increasing integer per container. The counter is not
persisted — on any cold call `next_slot` scans existing children for
`item.<n>` patterns and takes `max(n) + 1`. This makes slot naming
reconstruction-safe after engine reload without any additional state.
### Item-Recognition Rule
An entity is recognized as an item if it has a `stack_mode` property
(any non-nil, non-empty string value). The `stack_mode` must be
`"individual"` for `add` to succeed in v0.1. This rule is consistent
with the template convention for item templates (declaring `stack_mode`
in their properties block).
### set_tag Interaction
`add` calls `composition.set_tag(item, "renderable", false)`, which
removes the item from `lib-core.composition`'s `list_by_tag("renderable")`
index. `remove` calls `set_tag(item, "renderable", true)` to restore it.
This ensures items in inventory are invisible to the render system without
any per-frame filtering.

172
init.lua Normal file
View File

@@ -0,0 +1,172 @@
-- =====================================================================
-- 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.<n>" and returns
-- "item.<max_n + 1>". 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
-- Find the slot name under which `item` is attached to `container`.
-- Returns the slot string or nil if not found.
local function slot_of(container, item)
for slot_name, child in pairs(container:get_children()) do
if child == item 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
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).
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.<n>.
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
-- contains(container, item) -> bool
function M.contains(container, item)
return slot_of(container, item) ~= nil
end
-- count(container) -> number
function M.count(container)
return #M.contents(container)
end
return M

1
manifest.lib Normal file
View File

@@ -0,0 +1 @@
{"id":"lib-core.inventory-list","version":"0.1.0","api_min":"0.1","deps":[{"id":"lib-core.composition","version":"0.2.0"}]}