feat(composition): add set_tag + accept container={kind="list"} block

- set_tag(entity, tag, present): adds/removes entity from index_by_tag
  and entity_meta.tags. Idempotent (double-add and double-remove are
  no-ops). Loud-Error on entity not created by composition.
- define_template now accepts an optional container block. Validates
  kind="list" (only supported value). Loud-Errors individually on each
  unimplemented constraint field (weight_max, volume_max, grid,
  accepts_fluid, accepts_gas, restrictions) and on unknown container
  keys, preventing silent capability mismatches.
- create() copies tpl.tags into entity_meta so per-entity set_tag calls
  do not mutate the shared template tag-set.
- manifest.lib version bumped to 0.2.0.
- README and header comment updated to reflect new surface and revised
  deferred-list.
This commit is contained in:
Axel Meyer
2026-06-13 13:55:01 +02:00
parent b6a16df6f0
commit 87357571bd
3 changed files with 147 additions and 19 deletions

103
init.lua
View File

@@ -1,5 +1,5 @@
-- =====================================================================
-- lib-core.composition v0.1.0 — Template + Instantiation + Tag-Index
-- lib-core.composition v0.2.0 — Template + Instantiation + Tag-Index
-- See: meta/docs/superpowers/specs/2026-06-09-phase-A-inactive-entities-...
--
-- v0.1.0 Template-Only-Subset:
@@ -15,9 +15,18 @@
-- - composition.destroy(entity) — removes from indices + destroys
-- engine.entity.
--
-- DEFERRED (loud-error if attempted in v0.1):
-- v0.2.0 additions:
-- - composition.set_tag(entity, tag, present) — adds (present=true) or
-- removes (present=false) entity in index_by_tag + entity_meta.
-- Idempotent. Loud-Error on entity not created by composition.
-- - define_template accepts optional container={kind="list"} block.
-- All constraint fields (weight_max, volume_max, grid, accepts_fluid,
-- accepts_gas, restrictions) → Loud-Error (Phase F/G deferred).
--
-- DEFERRED (loud-error if attempted):
-- - slots → Phase D Trigger (Composite Items with Sub-Items)
-- - container → Phase B Trigger (Items + Inventory)
-- - container constraint fields (weight_max, volume_max, grid,
-- accepts_fluid, accepts_gas, restrictions) → Phase F/G Trigger
-- - quality → Phase J Trigger (Damage / Wear)
-- - condition → Phase J Trigger (Damage / Wear)
-- - parent: → Template-Inheritance, no consumer yet
@@ -145,25 +154,59 @@ function M.define_template(def)
if def.slots ~= nil then
error(string.format(
"composition.define_template '%s': 'slots' block is Phase-D " ..
"trigger (Composite Items with Sub-Items); not supported in v0.1",
"trigger (Composite Items with Sub-Items); not supported in v0.2",
def.id))
end
if def.container ~= nil then
error(string.format(
"composition.define_template '%s': 'container' block is " ..
"Phase-B trigger (Items + Inventory); not supported in v0.1",
def.id))
if type(def.container) ~= "table" then
error(string.format(
"composition.define_template '%s': 'container' must be a table",
def.id))
end
if def.container.kind ~= "list" then
error(string.format(
"composition.define_template '%s': container.kind must be " ..
"'list' (only kind='list' is supported in v0.2; got %s)",
def.id, tostring(def.container.kind)))
end
-- Reject each unimplemented constraint field individually so the
-- error message names the offending field (Capability-by-Declaration:
-- no silent ignorance of declared constraints).
local deferred_fields = {
"weight_max", "volume_max", "grid",
"accepts_fluid", "accepts_gas", "restrictions",
}
for _, field in ipairs(deferred_fields) do
if def.container[field] ~= nil then
error(string.format(
"composition.define_template '%s': container.%s is a " ..
"Phase-F/G trigger (Constraint fields not implemented in v0.2)",
def.id, field))
end
end
-- Reject any unknown container keys (forward-protects against typos).
local known_container_keys = { kind = true,
weight_max = true, volume_max = true, grid = true,
accepts_fluid = true, accepts_gas = true, restrictions = true }
for k, _ in pairs(def.container) do
if not known_container_keys[k] then
error(string.format(
"composition.define_template '%s': unknown container " ..
"field '%s'",
def.id, tostring(k)))
end
end
end
if def.quality ~= nil or def.condition ~= nil then
error(string.format(
"composition.define_template '%s': 'quality' / 'condition' " ..
"are Phase-J triggers (Damage / Wear); not supported in v0.1",
"are Phase-J triggers (Damage / Wear); not supported in v0.2",
def.id))
end
if def.parent ~= nil then
error(string.format(
"composition.define_template '%s': 'parent:' inheritance has " ..
"no consumer in v0.1; declare inline until a real use-case appears",
"no consumer in v0.2; declare inline until a real use-case appears",
def.id))
end
@@ -206,6 +249,7 @@ function M.define_template(def)
id = def.id,
props_flat = props_flat,
tags = tags_set,
container = def.container, -- nil when not declared; B.2 reads this
}
index_by_template[def.id] = index_by_template[def.id] or {}
end
@@ -264,9 +308,13 @@ function M.create(spec)
end
-- Register in indices + meta.
-- Copy tpl.tags so per-entity set_tag calls don't mutate the template's
-- tag-set (which would affect future create() calls for the same template).
local reg_id = next_reg_id
next_reg_id = next_reg_id + 1
entity_meta[reg_id] = { template_id = tpl.id, tags = tpl.tags, handle = entity }
local entity_tags = {}
for t, v in pairs(tpl.tags) do entity_tags[t] = v end
entity_meta[reg_id] = { template_id = tpl.id, tags = entity_tags, handle = entity }
table.insert(index_by_template[tpl.id], entity)
for tag, _ in pairs(tpl.tags) do
@@ -335,6 +383,39 @@ function M.destroy(target_entity)
entity.destroy(target_entity)
end
function M.set_tag(target_entity, tag, present)
-- Validate tag argument.
if type(tag) ~= "string" or tag == "" then
error("composition.set_tag: 'tag' must be a non-empty string")
end
-- Validate present argument.
if type(present) ~= "boolean" then
error("composition.set_tag: 'present' must be a boolean")
end
-- Resolve entity → meta via the composition.reg_id property.
local reg_id = target_entity and target_entity:get_property("composition.reg_id")
if not reg_id or reg_id == 0 then
error("composition.set_tag: entity was not created by composition")
end
local meta = entity_meta[reg_id]
if not meta then
error("composition.set_tag: entity was not created by composition")
end
if present then
-- Idempotent add.
if meta.tags[tag] then return end
meta.tags[tag] = true
index_by_tag[tag] = index_by_tag[tag] or {}
table.insert(index_by_tag[tag], target_entity)
else
-- Idempotent remove.
if meta.tags[tag] == nil then return end
meta.tags[tag] = nil
remove_from_list(index_by_tag[tag], target_entity)
end
end
-- ---------- introspection (debug/testing) ----------
-- Returns the set of declared template-ids (for tests + debug).