feat: lib-core.crafting v0.2.0 — Multi-Source / Single-Sink Locale-API
Extends can_craft + craft to take a `locale` parameter that describes
WHERE inputs come from and WHERE outputs go. Two forms are accepted;
the v0.1 single-container call signature continues to work unchanged
via a transparent bw-compat shim.
Locale-Schema:
Form 1 (bw-compat): a bare container-entity-handle. Internally
wrapped to {sources={h}, sink=h}; the single
container is both the sole input source AND the
output sink. v0.1 call sites need no migration.
Form 2 (explicit): { sources = {c1, c2, ...}, sink = c_out } where
sources is a non-empty array of container-entity-
handles and sink is a single container-entity-
handle. sink MAY appear in sources (e.g. workbench
buffer as both source and sink).
can_craft aggregates input counts across the union of all sources
before comparing against the recipe's needs. `missing` entries report
the aggregated `have` count across sources.
craft greedy-drains in array-order: for each input.need it walks
sources left-to-right, fully draining matching-template items from
the first source before moving on to the next. The order is stable
and deterministic so callers can encode priority via the array.
A file-local resolve_locale helper centralizes the Form-1-vs-Form-2
dispatch and validates Form 2 with loud errors:
locale == nil → "locale must not be nil"
Form 2, sources not array or empty → "sources must be non-empty array"
Form 2, sink == nil → "sink must not be nil"
The count_by_template helper now takes a sources array and walks each
container's inventory in turn, summing per-template counts.
Manifest bumped 0.1.0 → 0.2.0. Deps unchanged (composition 0.3.0,
inventory-list 0.1.0). README documents both locale forms, the
greedy-drain order, the bw-compat guarantee, and the loud-error
conditions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
134
README.md
134
README.md
@@ -4,7 +4,7 @@ Recipe Registry + Match-Check + Craft Action. Headless data + logic layer.
|
|||||||
Recipes reference item-template-ids; inputs are consumed from a container,
|
Recipes reference item-template-ids; inputs are consumed from a container,
|
||||||
outputs are placed back into the same container.
|
outputs are placed back into the same container.
|
||||||
|
|
||||||
**Version:** 0.1.0
|
**Version:** 0.2.0
|
||||||
**Lib-ID:** lib-core.crafting
|
**Lib-ID:** lib-core.crafting
|
||||||
**Requires:** `lib-core.composition` 0.3.0, `lib-core.inventory-list` 0.1.0
|
**Requires:** `lib-core.composition` 0.3.0, `lib-core.inventory-list` 0.1.0
|
||||||
**Tags:** crafting, recipe, registry
|
**Tags:** crafting, recipe, registry
|
||||||
@@ -20,18 +20,20 @@ graph LR
|
|||||||
this --> inventory
|
this --> inventory
|
||||||
```
|
```
|
||||||
|
|
||||||
## Scope (v0.1.0)
|
## Scope (v0.2.0)
|
||||||
|
|
||||||
Solid-Cut: recipe registry + match-check + craft-action + per-recipe
|
Solid-Cut: recipe registry + match-check + craft-action + per-recipe
|
||||||
discovery hook. Single-container parameter (inputs source AND output
|
discovery hook. Multi-Source / Single-Sink locale (Form 2) plus
|
||||||
destination). Count-form recipe-inputs.
|
bw-compat bare-handle locale (Form 1). Count-form recipe-inputs.
|
||||||
|
|
||||||
**Supported:**
|
**Supported:**
|
||||||
- `define_recipe{id, inputs, output, name?, description?, is_known?}`
|
- `define_recipe{id, inputs, output, name?, description?, is_known?}`
|
||||||
- `list_recipes()`, `get_recipe(id)`
|
- `list_recipes()`, `get_recipe(id)`
|
||||||
- `is_known(recipe_id, ctx)` — per-recipe discovery gating
|
- `is_known(recipe_id, ctx)` — per-recipe discovery gating
|
||||||
- `can_craft(recipe_id, container, ctx)` — non-mutating availability check
|
- `can_craft(recipe_id, locale, ctx)` — non-mutating availability check
|
||||||
- `craft(recipe_id, container, ctx)` — mutating action
|
- `craft(recipe_id, locale, ctx)` — mutating action
|
||||||
|
- `locale` parameter accepts either a bare container-entity (Form 1,
|
||||||
|
bw-compat) or an explicit `{sources={...}, sink=...}` table (Form 2)
|
||||||
|
|
||||||
**Deferred (Phase D+ / E):**
|
**Deferred (Phase D+ / E):**
|
||||||
- Workpiece-Model / multi-step crafting / Batch / Time-coupled craft
|
- Workpiece-Model / multi-step crafting / Batch / Time-coupled craft
|
||||||
@@ -101,9 +103,9 @@ named recipe, or `false` if the `recipe_id` is unregistered. `ctx`
|
|||||||
must be a table (loud-error otherwise); empty `{}` is allowed. Used by
|
must be a table (loud-error otherwise); empty `{}` is allowed. Used by
|
||||||
display libs to filter the recipe list to known recipes only.
|
display libs to filter the recipe list to known recipes only.
|
||||||
|
|
||||||
### `crafting.can_craft(recipe_id, container, ctx)`
|
### `crafting.can_craft(recipe_id, locale, ctx)`
|
||||||
|
|
||||||
**Syntax:** `crafting.can_craft(recipe_id, container, ctx) -> result`
|
**Syntax:** `crafting.can_craft(recipe_id, locale, ctx) -> result`
|
||||||
|
|
||||||
**Result-Shape:**
|
**Result-Shape:**
|
||||||
```lua
|
```lua
|
||||||
@@ -124,18 +126,25 @@ display libs to filter the recipe list to known recipes only.
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Description:** Non-mutating check whether `recipe_id` can be crafted
|
**Description:** Non-mutating check whether `recipe_id` can be crafted
|
||||||
right now from items in `container`. Returns `ok=true` if the recipe is
|
right now from items in the locale's sources. Returns `ok=true` if the
|
||||||
registered, `is_known(ctx)` returns `true`, AND `inventory.contents(container)`
|
recipe is registered, `is_known(ctx)` returns `true`, AND the union of
|
||||||
contains at least `input.count` items per `input.template` for every
|
`inventory.contents` across all `locale.sources` contains at least
|
||||||
input. Otherwise returns `ok=false` with an `error` discriminator. For
|
`input.count` items per `input.template` for every input. Otherwise
|
||||||
`missing_inputs`, the `missing` array lists each input that's under-
|
returns `ok=false` with an `error` discriminator. For `missing_inputs`,
|
||||||
supplied with its `needed` and `have` count.
|
the `missing` array lists each input that's under-supplied with its
|
||||||
|
aggregated `needed` and `have` count (summed across all sources).
|
||||||
|
|
||||||
Loud-Error: `ctx` must be a table (empty `{}` OK).
|
The `locale` parameter accepts two forms (see "Locale-Schema (v0.2.0)"
|
||||||
|
below for details): a bare container-entity (Form 1, bw-compat to v0.1)
|
||||||
|
or an explicit `{sources={c1, c2, ...}, sink=c_out}` table (Form 2).
|
||||||
|
|
||||||
### `crafting.craft(recipe_id, container, ctx)`
|
Loud-Error: `ctx` must be a table (empty `{}` OK); `locale` must not be
|
||||||
|
`nil`; for Form 2, `locale.sources` must be a non-empty array and
|
||||||
|
`locale.sink` must not be `nil`.
|
||||||
|
|
||||||
**Syntax:** `crafting.craft(recipe_id, container, ctx) -> result`
|
### `crafting.craft(recipe_id, locale, ctx)`
|
||||||
|
|
||||||
|
**Syntax:** `crafting.craft(recipe_id, locale, ctx) -> result`
|
||||||
|
|
||||||
**Result-Shape:**
|
**Result-Shape:**
|
||||||
```lua
|
```lua
|
||||||
@@ -152,17 +161,28 @@ Loud-Error: `ctx` must be a table (empty `{}` OK).
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Description:** Performs an internal `can_craft` check first; on failure
|
**Description:** Performs an internal `can_craft` check first; on failure
|
||||||
returns the same result early (container untouched). On success:
|
returns the same result early (sources untouched). On success:
|
||||||
1. Removes `input.count` matching-template items from the container via
|
1. For each `input.need`, greedy-drain matching-template items from the
|
||||||
`inventory.remove`, then `composition.destroy`s each consumed item.
|
sources in array-order (`locale.sources[1]` first, then `[2]`, ...).
|
||||||
2. Creates `output.count` new items via `composition.create{template=output.template}`.
|
Each item is removed via `inventory.remove` from its source and then
|
||||||
3. Adds each new output to the container via `inventory.add`.
|
`composition.destroy`d. The first source is fully drained of matching
|
||||||
|
items before moving on to the next.
|
||||||
|
2. Creates `output.count` new items via
|
||||||
|
`composition.create{template=output.template}`.
|
||||||
|
3. Adds each new output to `locale.sink` via `inventory.add`.
|
||||||
|
|
||||||
|
The greedy-drain-order is deterministic and stable: caller controls which
|
||||||
|
source supplies first by ordering the `sources` array. Common patterns:
|
||||||
|
put the workpiece-stash first to consume its leftovers; put the player
|
||||||
|
backpack first to leave the workbench-buffer for next time.
|
||||||
|
|
||||||
The returned `consumed` array references the input entity handles AFTER
|
The returned `consumed` array references the input entity handles AFTER
|
||||||
they were destroyed; they're useful for debug logging but must not be
|
they were destroyed; they're useful for debug logging but must not be
|
||||||
operated on (their composition meta-record is gone).
|
operated on (their composition meta-record is gone).
|
||||||
|
|
||||||
Loud-Error: `ctx` must be a table (empty `{}` OK).
|
Loud-Error: `ctx` must be a table (empty `{}` OK); `locale` must not be
|
||||||
|
`nil`; for Form 2, `locale.sources` must be a non-empty array and
|
||||||
|
`locale.sink` must not be `nil`.
|
||||||
|
|
||||||
## Recipe-Schema
|
## Recipe-Schema
|
||||||
|
|
||||||
@@ -183,6 +203,60 @@ Loud-Error: `ctx` must be a table (empty `{}` OK).
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Locale-Schema (v0.2.0)
|
||||||
|
|
||||||
|
The `locale` parameter to `can_craft` / `craft` describes WHERE inputs
|
||||||
|
come from and WHERE outputs go. Two forms are accepted:
|
||||||
|
|
||||||
|
### Form 1 (bw-compat, v0.1)
|
||||||
|
|
||||||
|
A bare container-entity-handle:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
crafting.craft("rock_pick", player_backpack, ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
Internally treated as `{sources = {player_backpack}, sink = player_backpack}`
|
||||||
|
— the single container is both the sole input source AND the output sink.
|
||||||
|
This is the v0.1 single-container behavior and remains supported
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
### Form 2 (explicit, v0.2)
|
||||||
|
|
||||||
|
A table with `sources` (non-empty array) and `sink`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
crafting.craft("rock_pick",
|
||||||
|
{ sources = {workbench_buffer, player_backpack}, sink = workbench_buffer },
|
||||||
|
ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `sources` is an array of container-entity-handles. Inputs are
|
||||||
|
consumed greedy-left-to-right: the first source is fully drained of
|
||||||
|
matching items before moving to the next. Caller controls priority via
|
||||||
|
array order.
|
||||||
|
- `sink` is a single container-entity-handle. All outputs are added to
|
||||||
|
this container.
|
||||||
|
- The `sink` MAY appear in `sources` (e.g. the workbench buffer is both
|
||||||
|
an input source AND the output sink). It does not have to.
|
||||||
|
- `can_craft` aggregates `have` counts across ALL sources before
|
||||||
|
comparing against `needed`.
|
||||||
|
|
||||||
|
### Bw-Compat Guarantee
|
||||||
|
|
||||||
|
Any v0.1 call site that passed a bare container-entity-handle as the
|
||||||
|
second argument continues to work unchanged in v0.2; the shim wraps it
|
||||||
|
into `{sources = {h}, sink = h}` transparently. No call-site migration
|
||||||
|
is required.
|
||||||
|
|
||||||
|
### Loud-Error Conditions
|
||||||
|
|
||||||
|
| Condition | Error message |
|
||||||
|
|---|---|
|
||||||
|
| `locale == nil` | `crafting.<fn>: locale must not be nil` |
|
||||||
|
| Form 2, `sources` not a table or empty | `crafting.<fn>: locale.sources must be non-empty array` |
|
||||||
|
| Form 2, `sink == nil` | `crafting.<fn>: locale.sink must not be nil` |
|
||||||
|
|
||||||
## Test Backdoors
|
## Test Backdoors
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
@@ -195,21 +269,23 @@ re-initialize state between assertions.
|
|||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- **Container-Parameter:** v0.1 uses a single container for both inputs
|
- **Locale-Parameter:** v0.2 takes a `locale` describing input sources
|
||||||
and output. Modules pass the appropriate container (e.g. the player's
|
and output sink. Single-container call sites stay simple via Form-1
|
||||||
backpack). Workbench-Entity / multi-container splits are deferred to
|
bw-compat (bare-handle); workbench-style "merge inputs from N
|
||||||
Phase D.
|
containers into a sink-entity" call sites use Form-2 explicit table.
|
||||||
- **Discovery-Hook:** Per-recipe `is_known(ctx)` lets modders gate
|
- **Discovery-Hook:** Per-recipe `is_known(ctx)` lets modders gate
|
||||||
recipes on schematic-store-lookups, skill-property-checks, etc.
|
recipes on schematic-store-lookups, skill-property-checks, etc.
|
||||||
without expanding the v0.1 surface.
|
without expanding the surface.
|
||||||
- **No silent fail:** `craft` always re-checks `can_craft` and returns
|
- **No silent fail:** `craft` always re-checks `can_craft` and returns
|
||||||
a structured error if conditions changed since the UI's last frame.
|
a structured error if conditions changed since the UI's last frame.
|
||||||
|
- **Greedy-drain-order is stable:** `craft` consumes from sources in
|
||||||
|
array-order, fully draining each matching template before moving on.
|
||||||
|
Callers can rely on this for sink-priority patterns.
|
||||||
|
|
||||||
## Future Phases
|
## Future Phases
|
||||||
|
|
||||||
| Phase | Surface addition | Spec |
|
| Phase | Surface addition | Spec |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| D (Workbench) | Workbench-Entity-Container parameter; post-craft-relocate callback | crafting-model.md |
|
|
||||||
| D+ (Workpiece) | Multi-step crafting with intermediate workpiece-entities | crafting-model.md |
|
| D+ (Workpiece) | Multi-step crafting with intermediate workpiece-entities | crafting-model.md |
|
||||||
| D+ (Batch / Time) | Batch parameter, time-system coupling, abort-decon | crafting-model.md |
|
| D+ (Batch / Time) | Batch parameter, time-system coupling, abort-decon | crafting-model.md |
|
||||||
| E (Property-driven) | `composition.iron ≥ 0.8` as input matcher | composition-model.md, crafting-model.md |
|
| E (Property-driven) | `composition.iron ≥ 0.8` as input matcher | composition-model.md, crafting-model.md |
|
||||||
|
|||||||
91
init.lua
91
init.lua
@@ -1,14 +1,22 @@
|
|||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- lib-core.crafting v0.1.0 — Recipe Registry + Craft Action
|
-- lib-core.crafting v0.2.0 — Recipe Registry + Craft Action
|
||||||
-- Spec: meta/docs/superpowers/specs/2026-06-14-phase-C-crafting-design.md
|
-- Spec: meta/docs/superpowers/specs/2026-06-14-phase-D-workbench-design.md
|
||||||
--
|
--
|
||||||
-- Surface:
|
-- Surface:
|
||||||
-- crafting.define_recipe(recipe_def) -- register a recipe
|
-- crafting.define_recipe(recipe_def) -- register a recipe
|
||||||
-- crafting.list_recipes() -> {recipe_def, ...}
|
-- crafting.list_recipes() -> {recipe_def, ...}
|
||||||
-- crafting.get_recipe(id) -> recipe_def or nil
|
-- crafting.get_recipe(id) -> recipe_def or nil
|
||||||
-- crafting.is_known(recipe_id, ctx) -> bool
|
-- crafting.is_known(recipe_id, ctx) -> bool
|
||||||
-- crafting.can_craft(recipe_id, container, ctx) -> result (non-mutating)
|
-- crafting.can_craft(recipe_id, locale, ctx) -> result (non-mutating)
|
||||||
-- crafting.craft(recipe_id, container, ctx) -> result (mutating)
|
-- crafting.craft(recipe_id, locale, ctx) -> result (mutating)
|
||||||
|
--
|
||||||
|
-- Locale-Param (v0.2.0):
|
||||||
|
-- Form 1 (bw-compat): a bare entity_handle. Treated as both the sole
|
||||||
|
-- source AND the sink.
|
||||||
|
-- Form 2 (explicit): a table { sources = {c1, c2, ...}, sink = c_out }
|
||||||
|
-- where `sources` is a non-empty array of
|
||||||
|
-- container-entity-handles and `sink` is a single
|
||||||
|
-- container-entity-handle.
|
||||||
--
|
--
|
||||||
-- Recipe-Schema:
|
-- Recipe-Schema:
|
||||||
-- {
|
-- {
|
||||||
@@ -68,12 +76,40 @@ local function shallow_copy(t)
|
|||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
local function count_by_template(container)
|
-- Locale-Resolver: akzeptiert entity_handle (bw-compat) ODER table
|
||||||
|
-- {sources={...}, sink=...}. Returns immer Form-2 mit validierten
|
||||||
|
-- Feldern. Loud-Error bei kaputter Form-2.
|
||||||
|
local function resolve_locale(locale, fn_name)
|
||||||
|
-- Form 2: explicit table
|
||||||
|
if type(locale) == "table" and locale.sources ~= nil then
|
||||||
|
if type(locale.sources) ~= "table" or #locale.sources == 0 then
|
||||||
|
error(string.format(
|
||||||
|
"crafting.%s: locale.sources must be non-empty array",
|
||||||
|
fn_name), 3)
|
||||||
|
end
|
||||||
|
if locale.sink == nil then
|
||||||
|
error(string.format(
|
||||||
|
"crafting.%s: locale.sink must not be nil",
|
||||||
|
fn_name), 3)
|
||||||
|
end
|
||||||
|
return locale
|
||||||
|
end
|
||||||
|
-- Form 1 (bw-compat): bare entity_handle
|
||||||
|
if locale == nil then
|
||||||
|
error(string.format(
|
||||||
|
"crafting.%s: locale must not be nil", fn_name), 3)
|
||||||
|
end
|
||||||
|
return { sources = {locale}, sink = locale }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function count_by_template(sources)
|
||||||
local out = {}
|
local out = {}
|
||||||
for _, ent in ipairs(inv.contents(container)) do
|
for _, source in ipairs(sources) do
|
||||||
local tpl = composition.template_of(ent)
|
for _, ent in ipairs(inv.contents(source)) do
|
||||||
if tpl ~= nil then
|
local tpl = composition.template_of(ent)
|
||||||
out[tpl] = (out[tpl] or 0) + 1
|
if tpl ~= nil then
|
||||||
|
out[tpl] = (out[tpl] or 0) + 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return out
|
return out
|
||||||
@@ -149,7 +185,7 @@ function M.is_known(recipe_id, ctx)
|
|||||||
return r.is_known(ctx) == true
|
return r.is_known(ctx) == true
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.can_craft(recipe_id, container, ctx)
|
function M.can_craft(recipe_id, locale_arg, ctx)
|
||||||
if type(ctx) ~= "table" then
|
if type(ctx) ~= "table" then
|
||||||
error("crafting.can_craft: ctx must be table", 2)
|
error("crafting.can_craft: ctx must be table", 2)
|
||||||
end
|
end
|
||||||
@@ -160,7 +196,8 @@ function M.can_craft(recipe_id, container, ctx)
|
|||||||
if r.is_known(ctx) ~= true then
|
if r.is_known(ctx) ~= true then
|
||||||
return { ok = false, error = "unknown_recipe" }
|
return { ok = false, error = "unknown_recipe" }
|
||||||
end
|
end
|
||||||
local have = count_by_template(container)
|
local locale = resolve_locale(locale_arg, "can_craft")
|
||||||
|
local have = count_by_template(locale.sources)
|
||||||
local missing = {}
|
local missing = {}
|
||||||
for _, need in ipairs(r.inputs) do
|
for _, need in ipairs(r.inputs) do
|
||||||
local have_n = have[need.template] or 0
|
local have_n = have[need.template] or 0
|
||||||
@@ -178,34 +215,40 @@ function M.can_craft(recipe_id, container, ctx)
|
|||||||
return { ok = true }
|
return { ok = true }
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.craft(recipe_id, container, ctx)
|
function M.craft(recipe_id, locale_arg, ctx)
|
||||||
local pre = M.can_craft(recipe_id, container, ctx)
|
local pre = M.can_craft(recipe_id, locale_arg, ctx)
|
||||||
if not pre.ok then return pre end
|
if not pre.ok then return pre end
|
||||||
|
|
||||||
|
local locale = resolve_locale(locale_arg, "craft")
|
||||||
local r = recipes[recipe_id]
|
local r = recipes[recipe_id]
|
||||||
|
|
||||||
-- Consume inputs: per-input-need, snapshot contents BEFORE the
|
-- 1. Greedy-drain from sources in array-order. Per input-need, walk
|
||||||
-- inner loop because we mutate via inv.remove during iteration.
|
-- sources left-to-right and snapshot each source's contents
|
||||||
|
-- BEFORE the inner loop because we mutate via inv.remove during
|
||||||
|
-- iteration.
|
||||||
local consumed = {}
|
local consumed = {}
|
||||||
for _, need in ipairs(r.inputs) do
|
for _, need in ipairs(r.inputs) do
|
||||||
local remaining = need.count
|
local remaining = need.count
|
||||||
local snapshot = inv.contents(container)
|
for _, source in ipairs(locale.sources) do
|
||||||
for _, ent in ipairs(snapshot) do
|
|
||||||
if remaining == 0 then break end
|
if remaining == 0 then break end
|
||||||
if composition.template_of(ent) == need.template then
|
local snapshot = inv.contents(source)
|
||||||
inv.remove(container, ent)
|
for _, ent in ipairs(snapshot) do
|
||||||
consumed[#consumed + 1] = ent
|
if remaining == 0 then break end
|
||||||
composition.destroy(ent)
|
if composition.template_of(ent) == need.template then
|
||||||
remaining = remaining - 1
|
inv.remove(source, ent)
|
||||||
|
consumed[#consumed + 1] = ent
|
||||||
|
composition.destroy(ent)
|
||||||
|
remaining = remaining - 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Produce outputs: composition.create + inv.add per output count.
|
-- 2. Create outputs + add to sink.
|
||||||
local crafted = {}
|
local crafted = {}
|
||||||
for _ = 1, r.output.count do
|
for _ = 1, r.output.count do
|
||||||
local out = composition.create{ template = r.output.template }
|
local out = composition.create{ template = r.output.template }
|
||||||
inv.add(container, out)
|
inv.add(locale.sink, out)
|
||||||
crafted[#crafted + 1] = out
|
crafted[#crafted + 1] = out
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"id":"lib-core.crafting","version":"0.1.0","api_min":"0.1","deps":[{"id":"lib-core.composition","version":"0.3.0"},{"id":"lib-core.inventory-list","version":"0.1.0"}]}
|
{"id":"lib-core.crafting","version":"0.2.0","api_min":"0.1","deps":[{"id":"lib-core.composition","version":"0.3.0"},{"id":"lib-core.inventory-list","version":"0.1.0"}]}
|
||||||
|
|||||||
Reference in New Issue
Block a user