diff --git a/docs/global-reference.md b/docs/global-reference.md index 49f58d0..036ac20 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -5046 globals (298 curated, 4748 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +5048 globals (307 curated, 4741 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -19,6 +19,9 @@ | address | name | conf | source | usage | |---|---|---|---|---| +| `0x671b` | shared_spendable_points | high | investigation | Shared spendable point balance used by the alchemy, study, evolution, and summoning systems. ALCHEMY requires and deducts each recipe's alchemy_recipe_point_costs value; STAGECLEAR awards points to this balance and caps it at 999. The exact player-facing Japanese resource label remains unresolved. | +| `0x671d` | alchemy_level | high | investigation | Current alchemy level. ALCHEMY hides recipes whose alchemy_recipe_minimum_levels value exceeds this level and raises it, up to 6, when alchemy_level_progress reaches the current threshold. | +| `0x6720` | alchemy_level_progress | high | investigation | Progress toward the next alchemy level. Each successful ALCHEMY synthesis increments it; reaching the current threshold raises alchemy_level and resets this counter. | | `0x2052e` | scjump_progress_b | med | inference | Second SCJUMP switch input (1223 comparison reads) — progression counter/position. INFERENCE from SCJUMP usage. | | `0x4dfbc` | scjump_progress_a | med | inference | Dominant SCJUMP switch input (1609 comparison reads) — a per-chapter story-progress counter/position the progression machine branches on. INFERENCE from SCJUMP usage; confirm exact meaning via a listing/playthrough. | | `0x665d6` | modal_message_line_count | high | investigation | Number of populated strings in modal_message_lines. Dozens of menu/gameplay producers append at lines[count] and increment this value; MES renders the resulting non-selecting modal and clears the count, while SBUNKI consumes the same buffer as selectable options. | @@ -216,6 +219,13 @@ | `0x152877` | battle_entity_indices | high | investigation | The two runtime entity rows participating in BTL. battle_actor_side_index and battle_target_side_index select these cells before BTL and CALCDMG access HP, unit definitions, skills, animation state, and voice banks. Columns: 0=side_0, 1=side_1. | | `0x152879` | battle_selected_skill_ids | high | investigation | Selected skill for each battle side. BTL displays a positive entry through the skill-name table and selects skill-use voice columns; CALCDMG uses the same id for skill parameters. A zero entry follows the equipped/default ordinary-attack path. Columns: 0=side_0, 1=side_1. | | `0x1561f6` | magic_action_information_handler_script_ids | high | investigation | MAINIT handler column indexed by current_magic_action_id. MAGIC and STUDY call the selected packed script id; every shipped action routes to MAMES, whose text dispatcher has authored descriptions for ids 1..9 only. | +| `0x156214` | alchemy_recipe_output_item_ids | high | investigation | Output item id indexed by sparse alchemy recipe id. ALCHEMY scans recipe ids 0..999, treats a nonzero cell as a populated recipe, and adds one copy of this item after a successful synthesis. | +| `0x1565fc` | alchemy_recipe_minimum_levels | high | investigation | Minimum alchemy level indexed by recipe id. ALCHEMY exposes a populated recipe only when this value is less than or equal to alchemy_level. | +| `0x1569e4` | alchemy_recipe_required_story_flags | high | investigation | Two positive story prerequisites per recipe. ALCHEMY subtracts one from every populated id and hides the recipe unless the corresponding story_event_flags cell equals 1; shipped ALINIT data populates only the first column. Columns: 0=required_flag_1, 1=required_flag_2. | +| `0x1571b4` | alchemy_recipe_forbidden_story_flags | high | investigation | Two negative story prerequisites per recipe. ALCHEMY subtracts one from every populated id and hides the recipe when the corresponding story_event_flags cell equals 1; shipped ALINIT data populates only the first column. Columns: 0=forbidden_flag_1, 1=forbidden_flag_2. | +| `0x157d6c` | alchemy_recipe_point_costs | high | investigation | Spendable point cost indexed by recipe id. ALCHEMY checks recipe availability against its point-capacity condition, then requires and deducts this value from shared_spendable_points on synthesis. | +| `0x158154` | alchemy_recipe_ingredient_item_ids | high | investigation | Up to four ingredient item ids per recipe. ALCHEMY checks total inventory for each populated slot and removes the paired alchemy_recipe_ingredient_quantities amount during synthesis. Columns: 0=ingredient_1, 1=ingredient_2, 2=ingredient_3, 3=ingredient_4. | +| `0x1590f4` | alchemy_recipe_ingredient_quantities | high | investigation | Required ingredient quantities paired cell-for-cell with alchemy_recipe_ingredient_item_ids. ALCHEMY requires and consumes this many copies of each populated ingredient. Columns: 0=ingredient_1, 1=ingredient_2, 2=ingredient_3, 3=ingredient_4. | | `0x15a097` | information_message_handler_script_ids | high | investigation | INFOMES's 32-row, four-column handler registry. INIT2 populates row zero with CIMES (0x334a), EIMES (0x334b), and VIMES (0x334c); INFOMES scans rows in order for the current tab, calls nonzero handlers, and stops after information_message_handled is set. Columns: 0=character_handler, 1=enemy_handler, 2=glossary_handler, 3=reserved_handler. | | `0x15a118` | character_profile_unit_ids | high | investigation | CIINIT's profile-to-unit join. INFOCH uses the selected unit id for reveal state, map sprite, short unit descriptions, species, and sex while CIMES remains keyed by the enclosing profile id. | | `0x15a17c` | character_profile_portrait_asset_ids | high | investigation | Optional CIINIT portrait resource indexed by character profile id. INFOCH draws it when nonzero and otherwise falls back to the backing unit's map-sprite asset. | @@ -388,9 +398,6 @@ | `0x15619c` | — | low | auto-shape | array | | `0x1561ba` | — | low | auto-shape | array | | `0x1561d8` | — | low | auto-shape | array | -| `0x156214` | — | low | auto-shape | array | -| `0x1565fc` | — | low | auto-shape | array | -| `0x157d6c` | — | low | auto-shape | array | | `0x15a501` | — | low | auto-shape | array | | `0x15a5c9` | — | low | auto-shape | array | | `0x15a691` | — | low | auto-shape | array | @@ -431,7 +438,6 @@ | `0x3231` | game_mode | med | inference | Game-mode/phase selector in the 0x3231-0x3234 progression-state cluster (chapter_mode is 0x3234). Enum 1..9, written by the gameplay scripts (AIM/ALCHEMY/BTL/BUNKI), branch-read in 136 scenes. Distinct from chapter; likely current sub-mode/screen. INFERENCE — confirm with a listing/sweep before relying on the exact meaning. | | `0x3234` | chapter_mode | high | investigation | Progression chapter/mode selector. SCJUMP's top-level switch keys on it; branch-read by progression scripts (FIELD etc.), not directly by SC/SP scenes. | | `0x3275` | — | med | auto-shape | TODO: confirm. Branch-read in 136 scenes / 143 scripts; compared against [0]; writers=['CAMP.BIN', 'DEBUGADV.BIN', 'DEBUGADV2.BIN', 'FIELD.BIN']. | -| `0x671b` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 5 scripts; compared against [999]; writers=['ALCHEMY.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']. | | `0x671e` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against [5, 6]; writers=['USEMAGIC.BIN']. | | `0x6722` | — | med | auto-shape | TODO: confirm. Branch-read in 13 scenes / 18 scripts; compared against [0, 1, 5, 99]; writers=['GAMESTART.BIN', 'SALLY.BIN', 'SC0010.BIN', 'SC0140.BIN']. | | `0x6724` | — | med | auto-shape | TODO: confirm. Branch-read in 4 scenes / 10 scripts; compared against [0, 25, 50, 99]; writers=['DEBUGADV2.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']. | @@ -5082,10 +5088,6 @@ | `0x155d8a` | — | med | auto-shape | record-table[stride 10] | | `0x155e9b` | — | med | auto-shape | record-table[stride 14] | | `0x156015` | — | med | auto-shape | record-table[stride 10] | -| `0x1569e4` | — | med | auto-shape | record-table[stride 2] | -| `0x1571b4` | — | med | auto-shape | record-table[stride 2] | -| `0x158154` | — | med | auto-shape | record-table[stride 4] | -| `0x1590f4` | — | med | auto-shape | record-table[stride 4] | | `0x15a75b` | — | med | auto-shape | record-table[stride 21] | | `0x15a785` | — | med | auto-shape | record-table[stride 3] | diff --git a/docs/name-resolution.md b/docs/name-resolution.md index fb95f6c..2643ba0 100644 --- a/docs/name-resolution.md +++ b/docs/name-resolution.md @@ -669,17 +669,19 @@ are *not* story flags — the miner over-tags them; they are recategorized `unkn The v1 map labels *shapes and tables*; the next increments add *meaning*, cheapest first: -1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, EBINIT, STINIT, CCINIT, SCINIT, RTINIT, and MAINIT now have +1. **Continue INIT semantics by evidence density.** ITINIT/SKINIT, EBINIT, STINIT, CCINIT, SCINIT, RTINIT, MAINIT, ILINIT, CGINIT, and ALINIT now have machine-readable investigation surfaces and semantic joins; EBINIT's populated schema is fully named, STINIT's joined object/enemy payloads are decoded, and CCINIT's 71 class-change rules expose predicates and effects. SCINIT closes the progression decision-to-scene join, and RTINIT's twenty movement/battle banks are structurally decoded with every populated movement-parameter cell classified and all 1,043 - shipped movement steps joined to provider behavior. With RTINIT's used-provider surface closed and - ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and INFOMES/MES classified, resume other reader-proven 2D tables; + shipped movement steps joined to provider behavior. ILINIT/RECOVER closes the condition ABI, CGINIT + closes the gallery registry, and ALINIT closes the 107-recipe alchemy registry with complete ITINIT + joins. With RTINIT's used-provider surface closed and ITMES/SKMES/VIMES/EIMES/CIMES/MAMES joined and + INFOMES/MES classified, audit the malformed AFINIT/CTINIT outputs next; never assign one universal meaning to a parameter bank whose meaning varies by provider selector. -2. **Label 2D record tables by their readers** — cross-reference which scripts read each - `rec[sN]` table and infer purpose from context (e.g. RECOVER's 30-wide tables ↔ a - status/recovery system). Static, medium effort. +2. **Label remaining 2D record tables by their readers** — cross-reference which scripts read each + `rec[sN]` table and infer purpose from context, preserving reserved rows and sparse cells. RECOVER's + 30-wide status tables and ALINIT's paired recipe tables are completed examples. Static, medium effort. 3. **Classify other `set-string`/`copy-to-global` writers** not covered by the completed INIT/MES set when their data flow reaches a current implementation need. 4. **Name *which stat* each field is (Frida).** The one step needing live tools: change a diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 5814792..07afa99 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -2878,7 +2878,27 @@ registry names those arrays plus CGMODE's ten-cell configured sheet-resource vec the complete write accounting, sparse id range, four atlases, optional previews, representative image joins, and raw-to-semantic field projection. -**Next:** profile the remaining generic/footer INIT outputs by evidence density. MPINIT is structurally -regular but still semantically shallow; the smaller CTINIT/CVINIT/AFINIT families may offer faster -reader-proven joins. Rank them by malformed field count and direct-consumer coverage before choosing the -next dedicated schema. +## Data-semantics sidebar: ALINIT alchemy recipe registry (2026-07-23) + +ALINIT's former generic output—18 records and 853 address-derived fields—was another ownership error. +Its 914 writes form seven parallel structures indexed by sparse recipe id: three 1,000-cell arrays for +output item, minimum alchemy level, and point cost; two `1000 × 2` required/forbidden story-flag tables; +and two `1000 × 4` ingredient-item/quantity tables. There are 107 populated recipe ids from 1 through +467. The dedicated extractor classifies every write and exposes only the thirteen actually populated +columns. + +ALCHEMY establishes the complete consumer contract. A nonzero output item marks a populated recipe. +Availability requires the current alchemy level, required and forbidden story flags, a point-capacity +condition, and sufficient quantities for each of four possible ingredient slots. Synthesis consumes +the paired ingredient quantities, adds one output item, deducts the recipe cost from the shared +spendable-point balance, and advances alchemy-level progress. All 107 output ids and all 286 ingredient +references resolve through ITINIT. + +The generated records retain the raw addresses, add output and ingredient names plus a compact nested +ingredient view, and project all seven table bases through `vm-map/globals.toml`. The same semantic map +now names the shared spendable-point balance, alchemy level, and alchemy-level progress. Regressions +protect the full write accounting, sparse layout, item joins, story gates, paired ingredient cells, and +raw-to-semantic projection. + +**Next:** audit the remaining small malformed INIT outputs, especially AFINIT and CTINIT, before +spending a larger slice on structurally regular but semantically shallow MPINIT. diff --git a/docs/script-inventory.md b/docs/script-inventory.md index 9b08814..73a8b9e 100644 --- a/docs/script-inventory.md +++ b/docs/script-inventory.md @@ -102,6 +102,12 @@ another ownership artifact. Its 3,941 writes actually define 851 sparse gallery- ordinal behind that thumbnail. CGMODE supplies the grouping and ordering evidence and consumes every record; the dedicated extraction joins all asset ids to filenames while retaining the raw arrays. +ALINIT's generic numeric parse likewise collapsed 914 writes into 18 malformed records and 853 +address-derived fields. ALCHEMY proves that they are 107 sparse recipe ids in a reserved 1,000-row +layout, backed by output item, minimum level, required/forbidden story flags, point cost, and four paired +ingredient-id/quantity slots. The dedicated extraction classifies every write, joins all 107 output +items and 286 ingredient references to ITINIT, and retains the seven raw table coordinates. + RTINIT is a seventh banked shape. Its 3,336 writes populate twenty parallel `1000 × 20` banks: ten movement banks consumed by MVRTN/RTN_M providers and ten battle banks consumed by BTRTN/RTN_B providers. The generated 172 routine-set records assemble 1,043 movement steps and fourteen battle steps, join diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 74c9e9e..e5f7765 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -57,7 +57,7 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited * | `extract_message_table.py` | Discover repeated global-id dispatch chains in either fallthrough-body (ITMES/SKMES/MAMES) or branch-target-body (VIMES/EIMES/CIMES) form, reconstruct player-facing text and furigana, preserve guard/body offsets, and emit an ID-keyed message table. Title-bearing tables expose `title`/`description`; EIMES exposes its untitled lines as `summary`/`strategy`; CIMES exposes one untitled multiline `biography`; MAMES exposes untitled `description` text. | `extract_message_table.py [OUTNAME]` | `.BIN` → `build/data/.json` | | `extract_init.py` | Parse a `*INIT` data source (auto-detects name / numeric / footer / mixed / rules / dispatch / banked shape). Name tables infer their reserved record span, preserve sparse one-based runtime ids, distinguish lookup bases from first written cells, statically evaluate direct and negative-value writes, and separate parallel `fields` from linked row-major `record_fields`; when flat global ranges overlap, a parallel base established by other records wins over a coincidental row-table cell calculation. Dedicated name-mode schemas recover VIINIT's pre-title glossary prerequisites and CIINIT's profile name, backing-unit, optional portrait, and reserved placement columns. Mixed tables recover selector-dispatched records, condition strings, scalars, preallocated buffer cells, consumer-confirmed strides, and length-prefixed footer arrays; STINIT additionally assembles confirmed buffers into `object_placements` and `enemy_spawns`, joins object type names and available descriptions from OBINIT, and exposes consumer-proven tagged payloads (teleport, treasure, card, initial faction, non-triggering hazard/barrier faction, and OBINIT-gated initial object state), preserves engine-dead type-27 writes under `ignored_payload_fields`, and exposes reinforcement schedules and first-clear-only enemy gates. Rules mode decodes CCINIT's source-ordered class-change predicates and output accumulators, joining EBINIT unit names, SKINIT skill names, titles, named stat bonuses, cost deltas, and applied-state slots. Dispatch mode decodes SCINIT's paired decision-to-packed-scene and authored-chapter arrays, preserving source-order overwrites, resolving SYS4INI script names, and cross-checking chapter tags against decoded SCJUMP paths. Banked mode decodes RTINIT's twenty parallel 1000-by-20 movement/battle routine banks, retains all assignments and overwrites, assembles final per-slot steps, resolves provider selectors to RTN_M/RTN_B scripts, and applies selector-scoped schemas where proven. Current schemas cover all nineteen used providers, RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061, including progress-only, randomized roaming, object-slot, coordinate, enemy/ally, treasure and Magic Pillar search, Healing Feather, waypoint, faction-terrain, retreat, reachable normal-attack routing, immediate offensive target/action selection, and immediate allied healing. Parameterless behavior schemas publish an empty `parameter_fields` mapping while still joining `provider_behavior`, target selection, action selection, and completion rules. Provider defaults such as M004's unwritten slot 0, M010's zero/HP resource index, and M013's any-foreign-faction filter are projected explicitly and counted separately from populated source cells. Authored cells proven unread by M001/M008 remain under per-step `ignored_movement_parameters` and contribute to top-level `ignored_movement_parameter_count`; they are not silently discarded or assigned invented meanings. Raw provider banks remain intact, and top-level `movement_provider_parameter_schemas` documents the projection. ITINIT, SKINIT, VIINIT, EBINIT, and CIINIT join their matching MES text. Top-level `field_semantics` maps raw keys to canonical global/column names, while each record's `semantic_fields` is the generated name-keyed join; complete footer copies map to a row and expose its values without the raw provenance wrapper. Raw keys and unresolved tagged payload cells remain provenance. OUTNAME accepts a stem or one `.json` suffix and rejects paths. Refreshes the generated data index. | `extract_init.py [OUTNAME] [--mode …]` | `
.BIN` plus EBINIT/SKINIT for CCINIT, OBINIT for STINIT, `build/callscript-names.json` for RTINIT/SCINIT, `build/scjump-decisions.json` for SCINIT, matching `.BIN` when supported, and `build/globals.json` → `build/data/.json`, `build/data/README.md` | | `init_table_profile.py` | Build the static investigation surface for an extracted name/numeric/mixed/rules/dispatch/banked table: per-scalar/string/array-cell/footer-array population and value distributions, representative records, and direct opcode/script consumers. Name tables report message coverage for title/description, summary/strategy, biography, and description-only layouts; rule tables summarize covered units, titled and level-independent rules, threshold/slot distributions, and skill awards; dispatch tables summarize assignments, overwrites, script-name resolution, and SCJUMP chapter agreement; banked tables summarize populated/reserved banks, movement/battle steps and providers, selector-specific semantic coverage, and overwrite conflicts. `--message-query REGEX` searches every supported message field beside all populated fields for semantic correlation. Findings are evidence only; confirmed meanings go in `vm-map/globals.toml`. | `init_table_profile.py
[--build] [--limit N] [--message-query REGEX]` | `build/data/
.json` + corpus → stdout; with `--build`, `build/data/
-field-profile.{json,md}` | -| `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based, mixed selector-dispatched, CCINIT rule, SCINIT dispatch, RTINIT banked, ILINIT condition-matrix, CNINIT unit-name/voice-family, and CGINIT gallery extraction; RECOVER ABI validation; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; overwrite/provider joins; and field/message/rule/dispatch/banked profiling. | run each directly | — | +| `test_extract_init.py`, `test_init_table_profile.py` | Regression checks for sparse one-based, mixed selector-dispatched, CCINIT rule, SCINIT dispatch, RTINIT banked, ILINIT condition-matrix, CNINIT unit-name/voice-family, CGINIT gallery, and ALINIT alchemy-recipe extraction; RECOVER ABI validation; MES reconstruction/joins; footer-array accounting and semantic projection; EBINIT/SKINIT/OBINIT definition joins; STINIT object/enemy joins and tagged-payload behavior; overwrite/provider joins; and field/message/rule/dispatch/banked profiling. | run each directly | — | | `global_map.py` | Build the partial global-variable name map from static evidence. | `global_map.py` | corpus + `build/data/` → `build/global-var-map.{json,md}` | `extract_init.py` also has name-mode specializations for VIINIT's sparse 200-row glossary table, @@ -80,6 +80,12 @@ sheets are read from INIT2's ten-cell CGMODE configuration and exposed as the 6- through `SO026D.AGF` atlases. The optional preview column is identified from SAVE/SELSTAGE's 112-by-84 consumer path; all raw addresses remain present. +Numeric-mode ALINIT also has a consumer-proven specialization. It classifies all 914 static writes +into 107 sparse recipe ids within a reserved 1,000-row layout: output item, minimum alchemy level, +required and forbidden story flags, point cost, and four paired ingredient-id/quantity cells. Output +and ingredient ids must resolve through ITINIT. The generated record adds a nested `ingredients` view +while retaining every raw scalar and `base/stride/column` coordinate. + ## VM | Tool | Purpose | Run | Reads → Writes | diff --git a/docs/vm-mapping-plan.md b/docs/vm-mapping-plan.md index 8400d9f..1edb3ce 100644 --- a/docs/vm-mapping-plan.md +++ b/docs/vm-mapping-plan.md @@ -53,13 +53,14 @@ - [x] **2.0 — Project structure.** Established `docs/`, `build/{disasm,text,data,scripts-json}/`, `godot/`; game install stays read-only in place. Also relaxed the loader magic check to the `SYS4` family (`SYS4424` patch scripts now parse — was silently skipping 5 scripts). - [x] **2.1 — Text corpora.** `tools/extract_phase2.py` → 481/481 scripts: full disassembly (`build/disasm/*.asm`), per-script strings, `build/text/dialogue.jsonl` (**30,057 show-text lines** — the translation corpus), `build/text/strings.jsonl` (38,449 strings tagged by source opcode), `build/manifest.json`. -- [x] **2.2 — `*INIT` data and rule sources → JSON.** `tools/extract_init.py` auto-detects shape (`name`/`numeric`/`footer`/`mixed`/`rules`/`dispatch`/`banked`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions), CIINIT (24 character-information profiles)** [name: sparse one-based name/description/fields, with CIINIT's dedicated profile registry], **CGINIT (851 sparse gallery images)** [numeric: full image, optional save/stage preview, thumbnail sheet/slot, and variant ordinal in a reserved 2,000-row layout], **MPINIT (1472 map records)** [footer: length-prefixed arrays], **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays], **CCINIT (71 class-change rules over 33 units)** [rules: unit/level/state predicates plus title/cost/stat/skill effects], **SCINIT (1,209 final decision rows)** [dispatch: scene resource plus authored chapter metadata and overwrite history], and **RTINIT (172 routine sets)** [banked: twenty movement/battle step banks, provider joins, and overwrite history]. Validated; see `build/data/README.md`. Raw addresses remain bytecode provenance; confirmed semantics come from `vm-map/globals.toml`. +- [x] **2.2 — `*INIT` data and rule sources → JSON.** `tools/extract_init.py` auto-detects shape (`name`/`numeric`/`footer`/`mixed`/`rules`/`dispatch`/`banked`) → **SKINIT (131 skills), ITINIT (287 items), EBINIT (277 units), OBINIT (46 object definitions), CIINIT (24 character-information profiles)** [name: sparse one-based name/description/fields, with CIINIT's dedicated profile registry], **CGINIT (851 sparse gallery images)** [numeric: full image, optional save/stage preview, thumbnail sheet/slot, and variant ordinal in a reserved 2,000-row layout], **ALINIT (107 sparse alchemy recipes)** [numeric: output, level/story gates, point cost, and four paired ingredient slots in a reserved 1,000-row layout], **MPINIT (1472 map records)** [footer: length-prefixed arrays], **STINIT (74 stages)** [mixed: selector-dispatched strings/scalars/buffer cells/footer arrays], **CCINIT (71 class-change rules over 33 units)** [rules: unit/level/state predicates plus title/cost/stat/skill effects], **SCINIT (1,209 final decision rows)** [dispatch: scene resource plus authored chapter metadata and overwrite history], and **RTINIT (172 routine sets)** [banked: twenty movement/battle step banks, provider joins, and overwrite history]. Validated; see `build/data/README.md`. Raw addresses remain bytecode provenance; confirmed semantics come from `vm-map/globals.toml`. - [x] **2.3 — Extract `STINIT`'s 74 sparse stage records.** The mixed mode identifies the dominant `scjump_progress_a` dispatch, recovers 29 preallocated buffer layouts (including six consumer-confirmed row strides), and keeps four condition strings, six scalars, fixed-buffer writes, and all 1,396 footer-array copies separated by stage id. Profiles supply population/value and direct-consumer evidence. Header/map semantics cover conditions, BGM, turn/replay/clear-reward settings, and map texture overrides. The object join assembles 2,312 placements with type/position/gates, OBINIT names and available descriptions, 604 reinforcement schedules, and typed initial-faction, teleport, treasure, card-list, non-triggering-faction, and initial-object-state payloads; FIELD's dedicated special-spawn path proves the final three populated type-27 tagged writes are engine-dead, so they remain visible as ignored provenance rather than unresolved semantics. The 30-cell enemy family contributes 1,378 joined spawns with unit/faction, placement, difficulty/story gates, level scaling, weighted selection, difficulty-specific movement/battle routine sets, and 485 first-clear-only gates. Raw address views remain alongside generated `semantic_fields`. - [x] **2.3a — Extract `CCINIT`'s 71 class-change rules.** Rules mode preserves source-order guards and detects the unit id, minimum level, clear applied-state slot, selected title/level, deployment-cost delta, named fourteen-stat bonuses, awarded SKINIT skills, and state slot set by each rule. EBINIT/SKINIT definition joins and the common global semantic projection coexist with raw addresses. CALCCC/ADDEXP establish the apply/report ABI; EVOLVE explains the two level-independent empty-title Lily rules. The generated profile covers 33 units, 69 titled rules, 30 skill awards, three used promotion slots, and 19 populated output fields. - [x] **2.3b — Extract `SCINIT`'s scene-dispatch registry.** Dispatch mode recognizes 2,179 alternating writes to two 10,000-cell arrays, preserves all 710 overwritten decision ids, and emits 1,209 final rows. The primary column maps decisions to 135 packed SYS4INI ids, all resolved to numbered SC scripts; the parallel column is authored chapter metadata. All 847 live SCJUMP decisions join to rows and 844 final chapter tags agree with the independently decoded paths; three legacy/stale mismatches remain explicit. - [x] **2.3c — Extract `RTINIT`'s routine-step banks.** Banked mode recognizes twenty parallel 1000-by-20 tables and emits 172 sparse routine-set rows with all 3,336 source assignments and 3,307 final cells. Movement banks join all 1,043 steps to all 19 used RTN_M providers (22 dispatchable); battle banks join fourteen steps to all four RTN_B providers. Activation percentages, progress gates, and required/forbidden story flags are consumer-proven; six empty banks remain explicit. RTN_M001/002/003/004/005/006/007/008/009/010/011/012/013/014/015/017/051/052/061 add selector-specific progress, randomized roaming, object-slot, coordinate, enemy/ally, treasure and object search, Healing Feather, waypoint, faction-terrain, retreat, normal-attack routing, offensive target/action selection, and immediate allied-healing semantics. All 977 populated movement-parameter cells are classified: 974 semantic inputs and three M001/M008 cells proven unread; thirteen unwritten defaults are projected separately while the generic raw banks remain intact. +- [x] **2.3d — Extract `ALINIT`'s alchemy recipes.** Numeric specialization recognizes seven recipe-indexed structures and emits 107 sparse recipes from all 914 static writes. ALCHEMY proves the output, minimum level, required/forbidden story gates, point cost, and four paired ingredient-id/quantity slots. Every output and all 286 ingredient references join to ITINIT; raw coordinates remain beside the semantic and nested ingredient views. - [x] **2.4 — Partial global-var map BUILT + wired into the disassembler.** `tools/global_map.py` → `build/global-var-map.{json,md}` (16,354/49,435 globals labelled: string tables, `*INIT` field arrays, 122 record tables w/ strides, current-entity index pointers). `sys4load` renders the labels inline (`=rec[s30]`, `=current-entity-index?`). See `docs/name-resolution.md`. -- [ ] **2.5 — Grow the global-var map (future, incremental).** Static first: RTINIT is closed at 1,043/1,043 movement steps; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. The next evidence-dense slice labels other 2D record tables by their readers, beginning with RECOVER's 30-wide status/recovery tables. Then use Frida only for semantics that static consumers cannot settle. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Packed `call-script` ids, SCJUMP decision-to-scene dispatch, the shipped RTINIT movement-provider join, and the six completed message joins are resolved. +- [ ] **2.5 — Grow the global-var map (future, incremental).** Static first: RTINIT is closed at 1,043/1,043 movement steps; the RECOVER/ILINIT condition ABI and ALINIT recipe registry are closed; ITMES/SKMES/VIMES/EIMES/CIMES/MAMES are joined to their definitions; and the non-table INFOMES/MES ABIs are classified. Next, audit the malformed small AFINIT/CTINIT outputs by their readers, then use Frida only for semantics static consumers cannot settle. Full detail: `docs/name-resolution.md` → "Future step — growing the map". Packed `call-script` ids, SCJUMP decision-to-scene dispatch, the shipped RTINIT movement-provider join, and the six completed message joins are resolved. ## Phase 3 — Name the unnamed opcodes *(top ~20 BEFORE Phase 4; the rest on demand)* diff --git a/tools/extract_init.py b/tools/extract_init.py index 3160631..55834d5 100644 --- a/tools/extract_init.py +++ b/tools/extract_init.py @@ -588,6 +588,17 @@ GALLERY_VARIANT_ORDINAL_ARRAY_BASE = 0x64C11 GALLERY_THUMBNAIL_SHEET_CONFIG_BASE = 0x66381 GALLERY_THUMBNAIL_SHEET_CONFIG_SPAN = 10 +ALCHEMY_RECIPE_OUTPUT_ITEM_ARRAY_BASE = 0x156214 +ALCHEMY_RECIPE_MINIMUM_LEVEL_ARRAY_BASE = 0x1565FC +ALCHEMY_RECIPE_REQUIRED_FLAGS_BASE = 0x1569E4 +ALCHEMY_RECIPE_FORBIDDEN_FLAGS_BASE = 0x1571B4 +ALCHEMY_RECIPE_POINT_COST_ARRAY_BASE = 0x157D6C +ALCHEMY_RECIPE_INGREDIENT_ITEM_IDS_BASE = 0x158154 +ALCHEMY_RECIPE_INGREDIENT_QUANTITIES_BASE = 0x1590F4 +ALCHEMY_RECIPE_RECORD_SPAN = 1000 +ALCHEMY_RECIPE_STORY_FLAG_STRIDE = 2 +ALCHEMY_RECIPE_INGREDIENT_STRIDE = 4 + RECOVER_CURRENT_ENTITY = 0x152616 RECOVER_EFFECTIVE_STATS = 0x4E11B RECOVER_CURRENT_RESOURCES = 0x4E085 @@ -2043,6 +2054,209 @@ def extract_gallery_definitions(scr): } +def extract_alchemy_recipes(scr): + """Extract ALINIT's sparse alchemy recipe registry.""" + scalar_arrays = { + ALCHEMY_RECIPE_OUTPUT_ITEM_ARRAY_BASE: "output_item_id", + ALCHEMY_RECIPE_MINIMUM_LEVEL_ARRAY_BASE: "minimum_alchemy_level", + ALCHEMY_RECIPE_POINT_COST_ARRAY_BASE: "point_cost", + } + row_tables = { + ALCHEMY_RECIPE_REQUIRED_FLAGS_BASE: ( + ALCHEMY_RECIPE_STORY_FLAG_STRIDE, + "required_story_flag_id", + ), + ALCHEMY_RECIPE_FORBIDDEN_FLAGS_BASE: ( + ALCHEMY_RECIPE_STORY_FLAG_STRIDE, + "forbidden_story_flag_id", + ), + ALCHEMY_RECIPE_INGREDIENT_ITEM_IDS_BASE: ( + ALCHEMY_RECIPE_INGREDIENT_STRIDE, + "ingredient_item_id", + ), + ALCHEMY_RECIPE_INGREDIENT_QUANTITIES_BASE: ( + ALCHEMY_RECIPE_INGREDIENT_STRIDE, + "ingredient_quantity", + ), + } + records_by_id: dict[int, dict] = {} + static_write_count = 0 + classified_write_count = 0 + + def record_for(record_id: int) -> dict: + if not (1 <= record_id < ALCHEMY_RECIPE_RECORD_SPAN): + raise ValueError( + f"{scr.path.name}: recipe id {record_id} outside reserved span" + ) + return records_by_id.setdefault(record_id, { + "id": record_id, + "fields": {}, + "record_fields": {}, + }) + + for ins in scr.instructions: + write = _static_global_write(ins) + if write is None: + if sys4load.display_label(ins.opcode) != "exit": + raise ValueError( + f"{scr.path.name}: unclassified instruction at 0x{ins.offset:x}" + ) + continue + static_write_count += 1 + destination, value = write + if not isinstance(value, int): + raise ValueError( + f"{scr.path.name}: non-static recipe value at 0x{ins.offset:x}" + ) + + for base in scalar_arrays: + record_id = destination - base + if 1 <= record_id < ALCHEMY_RECIPE_RECORD_SPAN: + _store_unique( + record_for(record_id)["fields"], + f"0x{base:x}", + value, + record_id, + ) + classified_write_count += 1 + break + else: + for base, (stride, _) in row_tables.items(): + relative = destination - base + if 0 <= relative < ALCHEMY_RECIPE_RECORD_SPAN * stride: + record_id, column = divmod(relative, stride) + record = record_for(record_id) + _store_unique( + record["record_fields"], + f"0x{base:x}/{stride}/{column}", + value, + record_id, + ) + classified_write_count += 1 + break + else: + raise ValueError( + f"{scr.path.name}: unclassified recipe write " + f"0x{destination:x} at 0x{ins.offset:x}" + ) + + item_records, _ = extract_name(sys4load.load(resolve("ITINIT"))) + item_names = {record["id"]: record["name"] for record in item_records} + records = [records_by_id[record_id] for record_id in sorted(records_by_id)] + required_scalar_keys = {f"0x{base:x}" for base in scalar_arrays} + output_key = f"0x{ALCHEMY_RECIPE_OUTPUT_ITEM_ARRAY_BASE:x}" + level_key = f"0x{ALCHEMY_RECIPE_MINIMUM_LEVEL_ARRAY_BASE:x}" + cost_key = f"0x{ALCHEMY_RECIPE_POINT_COST_ARRAY_BASE:x}" + ingredient_reference_count = 0 + joined_ingredient_reference_count = 0 + + for record in records: + if set(record["fields"]) != required_scalar_keys: + raise ValueError( + f"{scr.path.name}: recipe id {record['id']} has incomplete scalars" + ) + output_item_id = record["fields"][output_key] + if output_item_id not in item_names: + raise ValueError( + f"{scr.path.name}: recipe id {record['id']} has unknown " + f"output item {output_item_id}" + ) + record.update({ + "output_item_id": output_item_id, + "output_item_name": item_names[output_item_id], + "minimum_alchemy_level": record["fields"][level_key], + "point_cost": record["fields"][cost_key], + }) + for kind, base in ( + ("required", ALCHEMY_RECIPE_REQUIRED_FLAGS_BASE), + ("forbidden", ALCHEMY_RECIPE_FORBIDDEN_FLAGS_BASE), + ): + values = [ + record["record_fields"][f"0x{base:x}/2/{column}"] + for column in range(ALCHEMY_RECIPE_STORY_FLAG_STRIDE) + if f"0x{base:x}/2/{column}" in record["record_fields"] + ] + record[f"{kind}_story_flag_ids"] = values + + ingredients = [] + for slot in range(ALCHEMY_RECIPE_INGREDIENT_STRIDE): + item_key = ( + f"0x{ALCHEMY_RECIPE_INGREDIENT_ITEM_IDS_BASE:x}/" + f"{ALCHEMY_RECIPE_INGREDIENT_STRIDE}/{slot}" + ) + quantity_key = ( + f"0x{ALCHEMY_RECIPE_INGREDIENT_QUANTITIES_BASE:x}/" + f"{ALCHEMY_RECIPE_INGREDIENT_STRIDE}/{slot}" + ) + has_item = item_key in record["record_fields"] + has_quantity = quantity_key in record["record_fields"] + if has_item != has_quantity: + raise ValueError( + f"{scr.path.name}: recipe id {record['id']} has an " + f"unpaired ingredient slot {slot}" + ) + if not has_item: + continue + ingredient_item_id = record["record_fields"][item_key] + if ingredient_item_id not in item_names: + raise ValueError( + f"{scr.path.name}: recipe id {record['id']} has unknown " + f"ingredient item {ingredient_item_id} in slot {slot}" + ) + ingredient_reference_count += 1 + ingredient = { + "slot": slot, + "item_id": ingredient_item_id, + "item_name": item_names[ingredient_item_id], + "quantity": record["record_fields"][quantity_key], + } + joined_ingredient_reference_count += 1 + ingredients.append(ingredient) + record["ingredients"] = ingredients + + record_columns = sorted( + { + key + for record in records + for key in record.get("record_fields", {}) + }, + key=lambda key: tuple(int(part, 0) for part in key.split("/")), + ) + populated_ids = set(records_by_id) + return records, { + "record_span": ALCHEMY_RECIPE_RECORD_SPAN, + "populated_record_ids": sorted(populated_ids), + "populated_id_range": [min(populated_ids), max(populated_ids)], + "scalar_array_bases": { + role: f"0x{base:x}" for base, role in scalar_arrays.items() + }, + "row_tables": { + role: {"base": f"0x{base:x}", "stride": stride} + for base, (stride, role) in row_tables.items() + }, + "record_field_columns": record_columns, + "static_write_count": static_write_count, + "classified_static_write_count": classified_write_count, + "output_item_join_count": sum( + record["output_item_id"] in item_names for record in records + ), + "ingredient_reference_count": ingredient_reference_count, + "joined_ingredient_reference_count": joined_ingredient_reference_count, + "consumer_contract": { + "availability": ( + "ALCHEMY lists a recipe only when its minimum level, required " + "and forbidden story flags, point-capacity threshold, and " + "owned ingredient quantities pass." + ), + "synthesis": ( + "ALCHEMY removes each populated ingredient quantity, adds one " + "output item, deducts point_cost from the shared spendable " + "point pool, and advances alchemy-level progress." + ), + }, + } + + @cache def gallery_thumbnail_sheet_assets() -> dict[int, int]: """Read CGMODE's enabled thumbnail-sheet assets from INIT2.""" @@ -2771,6 +2985,11 @@ def write_data_index(data_dir: Path) -> None: "atlases, its atlas slot and variant ordinal, and the optional 112-by-84 preview", "used by SAVE and SELSTAGE. Raw global-array provenance remains beside these joins.", "", + "ALINIT's dedicated alchemy schema exposes 107 sparse recipes in a reserved 1,000-row", + "layout. Output and ingredient item ids join to ITINIT names; minimum alchemy level,", + "point cost, required/forbidden story flags, and four fixed ingredient slots retain", + "their raw parallel-array and row-table coordinates.", + "", "Mixed-mode tables preserve the sparse selector id, branch offset, condition strings,", "scalar fields, cells within preallocated buffers, and length-prefixed footer arrays.", "STINIT additionally joins confirmed parallel buffers into per-slot `object_placements`", @@ -2848,6 +3067,8 @@ def main() -> int: extractor = extract_condition_definitions elif mode == "numeric" and name == "CGINIT": extractor = extract_gallery_definitions + elif mode == "numeric" and name == "ALINIT": + extractor = extract_alchemy_recipes recs, meta = extractor(scr) if mode == "name" and name in MESSAGE_TABLES: message_name = MESSAGE_TABLES[name] diff --git a/tools/test_extract_init.py b/tools/test_extract_init.py index 4d80b52..4bbec5d 100644 --- a/tools/test_extract_init.py +++ b/tools/test_extract_init.py @@ -1034,6 +1034,106 @@ def test_gallery_definitions() -> None: ) +def test_alchemy_recipes() -> None: + scripts = paths.scripts() + script = sys4load.load(scripts["ALINIT.BIN"]) + check( + extract_init.detect_mode(script) == "numeric", + "ALINIT remains compatible with numeric-mode auto-detection", + ) + records, meta = extract_init.extract_alchemy_recipes(script) + by_id = {record["id"]: record for record in records} + check( + len(records) == 107 + and meta["record_span"] == 1000 + and meta["populated_id_range"] == [1, 467], + "ALINIT extracts 107 sparse recipes from its reserved 1,000 ids", + ) + check( + meta["static_write_count"] == 914 + and meta["classified_static_write_count"] == 914 + and len(meta["record_field_columns"]) == 10, + "ALINIT classifies every scalar and populated row-table write", + ) + check( + meta["output_item_join_count"] == 107 + and meta["ingredient_reference_count"] == 286 + and meta["joined_ingredient_reference_count"] == 286, + "ALINIT resolves every output and ingredient reference through ITINIT", + ) + check( + by_id[1]["output_item_id"] == 2 + and by_id[1]["output_item_name"] == "白銀の鍵" + and by_id[1]["minimum_alchemy_level"] == 2 + and by_id[1]["point_cost"] == 30 + and by_id[1]["required_story_flag_ids"] == [1902] + and by_id[1]["forbidden_story_flag_ids"] == [1903], + "ALINIT recipe 1 exposes its output, gating, and point cost", + ) + check( + [ + (ingredient["slot"], ingredient["item_id"], ingredient["quantity"]) + for ingredient in by_id[1]["ingredients"] + ] == [(1, 608, 1), (2, 625, 1)] + and by_id[14]["output_item_name"] == "シルバーコイン" + and [ + (ingredient["slot"], ingredient["item_id"], ingredient["quantity"]) + for ingredient in by_id[14]["ingredients"] + ] == [(0, 91, 5)], + "ALINIT preserves sparse ingredient slots and their paired quantities", + ) + + semantics = extract_init.field_semantics(records) + check( + semantics == { + "0x156214": "alchemy_recipe_output_item_ids", + "0x1565fc": "alchemy_recipe_minimum_levels", + "0x1569e4/2/0": ( + "alchemy_recipe_required_story_flags.required_flag_1" + ), + "0x1571b4/2/0": ( + "alchemy_recipe_forbidden_story_flags.forbidden_flag_1" + ), + "0x157d6c": "alchemy_recipe_point_costs", + "0x158154/4/0": ( + "alchemy_recipe_ingredient_item_ids.ingredient_1" + ), + "0x158154/4/1": ( + "alchemy_recipe_ingredient_item_ids.ingredient_2" + ), + "0x158154/4/2": ( + "alchemy_recipe_ingredient_item_ids.ingredient_3" + ), + "0x158154/4/3": ( + "alchemy_recipe_ingredient_item_ids.ingredient_4" + ), + "0x1590f4/4/0": ( + "alchemy_recipe_ingredient_quantities.ingredient_1" + ), + "0x1590f4/4/1": ( + "alchemy_recipe_ingredient_quantities.ingredient_2" + ), + "0x1590f4/4/2": ( + "alchemy_recipe_ingredient_quantities.ingredient_3" + ), + "0x1590f4/4/3": ( + "alchemy_recipe_ingredient_quantities.ingredient_4" + ), + }, + "ALINIT raw arrays join to their canonical semantic names", + ) + extract_init.attach_semantic_fields(records, semantics) + check( + by_id[1]["semantic_fields"][ + "alchemy_recipe_required_story_flags.required_flag_1" + ] == 1902 + and by_id[14]["semantic_fields"][ + "alchemy_recipe_ingredient_quantities.ingredient_1" + ] == 5, + "ALINIT retains raw cells beside one semantic recipe view", + ) + + def test_condition_definitions() -> None: scripts = paths.scripts() script = sys4load.load(scripts["ILINIT.BIN"]) @@ -1244,6 +1344,7 @@ if __name__ == "__main__": test_message_join() test_character_names() test_gallery_definitions() + test_alchemy_recipes() test_condition_definitions() test_field_semantics() if FAILS: diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 18589ff..590723a 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -2161,14 +2161,36 @@ depends_on = ["0x6718", "0x673c"] [[global]] address = "0x671b" -name = "" -category = "story-flag" +name = "shared_spendable_points" +category = "counter" type = "int" -value_domain = "one of {999}" -usage = "TODO: confirm. Branch-read in 0 scenes / 5 scripts; compared against [999]; writers=['ALCHEMY.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] +value_domain = "0..999" +usage = "Shared spendable point balance used by the alchemy, study, evolution, and summoning systems. ALCHEMY requires and deducts each recipe's alchemy_recipe_point_costs value; STAGECLEAR awards points to this balance and caps it at 999. The exact player-facing Japanese resource label remains unresolved." +source = "investigation" +confidence = "high" +depends_on = ["0x157d6c"] + +[[global]] +address = "0x671d" +name = "alchemy_level" +category = "counter" +type = "int" +value_domain = "0..6" +usage = "Current alchemy level. ALCHEMY hides recipes whose alchemy_recipe_minimum_levels value exceeds this level and raises it, up to 6, when alchemy_level_progress reaches the current threshold." +source = "investigation" +confidence = "high" +depends_on = ["0x1565fc", "0x6720"] + +[[global]] +address = "0x6720" +name = "alchemy_level_progress" +category = "counter" +type = "int" +value_domain = "non-negative synthesis progress" +usage = "Progress toward the next alchemy level. Each successful ALCHEMY synthesis increments it; reaching the current threshold raises alchemy_level and resets this counter." +source = "investigation" +confidence = "high" +depends_on = ["0x671d"] [[global]] address = "0x671e" @@ -3246,3 +3268,83 @@ usage = "Five negative prerequisites per stage enemy. FIELD subtracts one from e source = "investigation" confidence = "high" depends_on = ["0x6d3"] + +[[global]] +address = "0x156214" +name = "alchemy_recipe_output_item_ids" +category = "data-table" +type = "int[1000]" +value_domain = "ITINIT item ids; zero means no recipe" +usage = "Output item id indexed by sparse alchemy recipe id. ALCHEMY scans recipe ids 0..999, treats a nonzero cell as a populated recipe, and adds one copy of this item after a successful synthesis." +source = "investigation" +confidence = "high" + +[[global]] +address = "0x1565fc" +name = "alchemy_recipe_minimum_levels" +category = "data-table" +type = "int[1000]" +value_domain = "0..6" +usage = "Minimum alchemy level indexed by recipe id. ALCHEMY exposes a populated recipe only when this value is less than or equal to alchemy_level." +source = "investigation" +confidence = "high" +depends_on = ["0x156214", "0x671d"] + +[[global]] +address = "0x1569e4" +name = "alchemy_recipe_required_story_flags" +category = "data-table" +type = "int[1000][2]" +columns = { "0" = "required_flag_1", "1" = "required_flag_2" } +value_domain = "one-based story_event_flags ids; zero means no requirement" +usage = "Two positive story prerequisites per recipe. ALCHEMY subtracts one from every populated id and hides the recipe unless the corresponding story_event_flags cell equals 1; shipped ALINIT data populates only the first column." +source = "investigation" +confidence = "high" +depends_on = ["0x6d3", "0x156214"] + +[[global]] +address = "0x1571b4" +name = "alchemy_recipe_forbidden_story_flags" +category = "data-table" +type = "int[1000][2]" +columns = { "0" = "forbidden_flag_1", "1" = "forbidden_flag_2" } +value_domain = "one-based story_event_flags ids; zero means no exclusion" +usage = "Two negative story prerequisites per recipe. ALCHEMY subtracts one from every populated id and hides the recipe when the corresponding story_event_flags cell equals 1; shipped ALINIT data populates only the first column." +source = "investigation" +confidence = "high" +depends_on = ["0x6d3", "0x156214"] + +[[global]] +address = "0x157d6c" +name = "alchemy_recipe_point_costs" +category = "data-table" +type = "int[1000]" +value_domain = "5..100" +usage = "Spendable point cost indexed by recipe id. ALCHEMY checks recipe availability against its point-capacity condition, then requires and deducts this value from shared_spendable_points on synthesis." +source = "investigation" +confidence = "high" +depends_on = ["0x156214", "0x671b"] + +[[global]] +address = "0x158154" +name = "alchemy_recipe_ingredient_item_ids" +category = "data-table" +type = "int[1000][4]" +columns = { "0" = "ingredient_1", "1" = "ingredient_2", "2" = "ingredient_3", "3" = "ingredient_4" } +value_domain = "ITINIT item ids; zero means unused slot" +usage = "Up to four ingredient item ids per recipe. ALCHEMY checks total inventory for each populated slot and removes the paired alchemy_recipe_ingredient_quantities amount during synthesis." +source = "investigation" +confidence = "high" +depends_on = ["0x156214", "0x1590f4"] + +[[global]] +address = "0x1590f4" +name = "alchemy_recipe_ingredient_quantities" +category = "data-table" +type = "int[1000][4]" +columns = { "0" = "ingredient_1", "1" = "ingredient_2", "2" = "ingredient_3", "3" = "ingredient_4" } +value_domain = "positive item count; shipped values are 1, 2, 3, or 5" +usage = "Required ingredient quantities paired cell-for-cell with alchemy_recipe_ingredient_item_ids. ALCHEMY requires and consumes this many copies of each populated ingredient." +source = "investigation" +confidence = "high" +depends_on = ["0x158154"]