aboutsummaryrefslogtreecommitdiff
path: root/AGENTS.md
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-20 03:20:01 -0400
committerhistoria <[not public]>2026-06-20 03:20:01 -0400
commite4400c5f1e84c18d2126f2cb502ae10685977cbb (patch)
tree0523e9d68f1cfdc6f81b862e3068fe12c1a85054 /AGENTS.md
parent5ea082ab4e82409aebc889b496f43e52b003d4f7 (diff)
downloadthehouseoficarus-e4400c5f1e84c18d2126f2cb502ae10685977cbb.tar.gz
refactor: combined all station crafting
Diffstat (limited to 'AGENTS.md')
-rw-r--r--AGENTS.md40
1 files changed, 37 insertions, 3 deletions
diff --git a/AGENTS.md b/AGENTS.md
index f97c8ee..f2f528e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -52,7 +52,7 @@ internal/
## Key Conventions
-**YAML-driven.** Rooms, items, objects, mobs, drops, recipes, modules, courses are YAML files under `data/`. Read from disk on every access — edit and it takes effect immediately, no restart. Items, rooms, and recipes are organized into subdirectories by category (items: `equipment/`, `tools/`, etc.; rooms: `town/`, `wilderness/`, etc.; recipes: `smithing/`, `crafting/`, etc.). All IDs remain globally unique across subdirectories via path-indexed loading.
+**YAML-driven.** Rooms, items, objects, mobs, drops, recipes, modules, courses are YAML files under `data/`. Read from disk on every access — edit and it takes effect immediately, no restart. Recipes are walked from disk on first access then cached in-memory with an `O(1)` ID index (see GetByID below). Items, rooms, and recipes are organized into subdirectories by category (items: `equipment/`, `tools/`, etc.; rooms: `town/`, `wilderness/`, etc.; recipes: `smithing/`, `crafting/`, etc.). All IDs remain globally unique across subdirectories via path-indexed loading.
**Live state.** Player data (characters, accounts) is written to YAML on every state change. Ground items, mob instances, and object states live in memory only and are lost on restart.
@@ -91,7 +91,7 @@ data/mobs/<id>.yaml Mob definitions (combat stats, drops, behavior, steal
data/objects/<id>.yaml Object definitions (name, behavior, hidden, inroom_description, removal_item)
data/drops/<id>.yaml Shared drop tables (weighted item lists, sub-table references)
data/help/<id>.yaml Help topics
-data/recipes/<id>.yaml Recipe definitions (cooking, smithing, crafting, fletching, etc.)
+data/recipes/<id>.yaml Recipe definitions — individual or consolidated format (see worldbuilding_guide/recipes.md)
data/modules/<id>.yaml Science module definitions (id, level, max_hit, base_xp, junk_cost, category)
data/courses/<id>.yaml Agility course definitions (name, required_level, start_room, obstacles)
data/players/accounts/ Account YAML (gitignored)
@@ -516,7 +516,7 @@ Prompt variables: `%h` (HP), `%H` (max HP), `%b` (battery), `%B` (max battery),
Production skills share a unified system in `core_production.go`:
-1. Create recipe YAMLs in `data/recipes/` with the new `type`
+1. Create recipe YAMLs in `data/recipes/<type>/` — either individual (one per product) or consolidated (one per material group, with shared fields at top level and a `products:` list). See `worldbuilding_guide/recipes.md`.
2. Create a station object in `data/objects/` if needed
3. Add a `cmd_<skill>.go` command handler following existing patterns
4. Add the command to `commandRegistry` in `cmd_registry.go` with the appropriate class
@@ -524,6 +524,40 @@ Production skills share a unified system in `core_production.go`:
The unified production cycle handles: HowMany prompt, start message, timed loops, success/fail rolls, XP, output quantity, stackable stacking, byproducts, and end message. Tool requirements are checked in the command handler before calling `promptHowMany`.
+## Recipe Loading and Lookup
+
+Recipes have two YAML formats — **individual** (one file per recipe) and **consolidated** (shared `type`/`station`/`wait`/`consume` at top level, products listed under `products:`). The loader expands consolidated files into individual `RecipeDef` structs transparently. The production engine (`core_production.go`) sees only `RecipeDef` — no code changes needed for format.
+
+**RecipeStore** (`internal/action/recipe.go`) manages recipe access:
+
+### GetByID — Step by Step
+
+`GetByID(recipeID)` is an `O(1)` direct recipe lookup. Here's the full chain:
+
+1. **First call anywhere in the game** — a command like `cook`, `smith`, or the tick advancing an active production action calls `GetByID("smith_bronze_dagger")`.
+
+2. **Index check.** `GetByID` checks the `byID` map (`map[string]*RecipeDef`). On first call, it's empty, so the method calls `ensureLoaded()`.
+
+3. **`ensureLoaded()`** checks if the full recipe slice is already cached under key `"_all"` in `s.cache`. If not (cold cache), it calls `loadAllRaw()`.
+
+4. **`loadAllRaw()`** walks `data/recipes/` recursively. For each YAML file:
+ - First attempts to unmarshal as a plain `RecipeDef` (individual format). If `type` is non-empty, the recipe is appended.
+ - If that fails, attempts to unmarshal as `consolidatedRecipeDef`. If `type` is set and `products` is non-empty, it expands each product entry via `expandProduct()` into a full `RecipeDef` (inheriting top-level `type`, `station`, `wait`, `consume`; overriding with product-level fields where specified).
+ - **YAML parse errors are logged to stderr** — no more silent failures.
+
+5. **Index building.** `ensureLoaded()` caches the full recipe slice as `s.cache["_all"]` (permanent backing store), then populates `s.byID` with pointers into that slice: `byID["smith_bronze_dagger"] = &all[i]`. The cached slice keeps the backing array alive, so byID pointers never dangle.
+
+6. **GetByID returns.** The `*RecipeDef` pointer is returned and used by the caller (e.g., `loadProductionRecipe()` or `menuEntryName()`).
+
+7. **All subsequent calls** to `GetByID`, `LoadAll()`, or `LoadByType()` hit the in-memory cache — no filesystem walk. `LoadByType("smithing")` filters the cached `"_all"` slice and caches the filtered result.
+
+**Key properties:**
+- `GetByID` — `O(1)` map lookup, zero allocations
+- `LoadByType` — `O(n)` filter pass on first call per type, `O(1)` copy on cache hits
+- `LoadAll` — same as `LoadByType("")`, returns copy of `"_all"` cache
+- All public methods return copies — callers can't mutate cached data
+- No cache invalidation — recipes are static after startup. Server restart picks up changes.
+
## Adding a New Command
1. Add a `cmd_*.go` file in `internal/game/` with the handler function `func (g *Game) executeXxx(sess *net.Session, args []string, rawInput string)`