Decode ALINIT alchemy recipes
This commit is contained in:
@@ -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] |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)*
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user