Decode ALINIT alchemy recipes

This commit is contained in:
gamer147
2026-07-23 19:26:49 -04:00
parent aa375d0b5e
commit 825b1b08fd
9 changed files with 490 additions and 29 deletions

View File

@@ -1,7 +1,7 @@
<!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build -->
# 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] |

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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 <MES> [OUTNAME]` | `<MES>.BIN``build/data/<OUTNAME>.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 <TABLE> [OUTNAME] [--mode …]` | `<TABLE>.BIN` plus EBINIT/SKINIT for CCINIT, OBINIT for STINIT, `build/callscript-names.json` for RTINIT/SCINIT, `build/scjump-decisions.json` for SCINIT, matching `<MES>.BIN` when supported, and `build/globals.json``build/data/<OUTNAME>.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 <TABLE> [--build] [--limit N] [--message-query REGEX]` | `build/data/<TABLE>.json` + corpus → stdout; with `--build`, `build/data/<TABLE>-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 |

View File

@@ -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)*

View File

@@ -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]

View File

@@ -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:

View File

@@ -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"]