From 6822184c469ea25973ef44815d68ca0259e9494e Mon Sep 17 00:00:00 2001 From: Calic Date: Sat, 13 Jun 2026 17:31:52 +0200 Subject: [PATCH] =?UTF-8?q?initial:=20inventory-list=20v0.1.0=20=E2=80=94?= =?UTF-8?q?=20list-container=20inventory=20over=20engine=20entity-tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provides add/remove/contents/contains/count over composition entities acting as list containers. Items attach as synthetic children under item. 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). --- LICENSE | 24 +++++++ README.md | 134 +++++++++++++++++++++++++++++++++++++++ init.lua | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++ manifest.lib | 1 + 4 files changed, 331 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 init.lua create mode 100644 manifest.lib diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..81c9d65 --- /dev/null +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e417745 --- /dev/null +++ b/README.md @@ -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 + + +```mermaid +graph LR + this["lib-core.inventory-list"] + comp["lib-core.composition"] + engine["engine.*"] + this --> comp + this --> engine +``` + + +## 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.`), 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.` 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.` 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. diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..e1c5468 --- /dev/null +++ b/init.lua @@ -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." 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 + +-- 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.. + 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 diff --git a/manifest.lib b/manifest.lib new file mode 100644 index 0000000..c240d75 --- /dev/null +++ b/manifest.lib @@ -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"}]}