diff options
Diffstat (limited to 'AGENTS.md')
| -rw-r--r-- | AGENTS.md | 40 |
1 files changed, 37 insertions, 3 deletions
@@ -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)` |
