Compare commits

10 Commits

Author SHA1 Message Date
gamer147
71ec6e55ed Make Gitea core validation source-only 2026-08-03 13:28:59 -04:00
gamer147
4db2f416cf Record verified binary history purge 2026-08-03 13:12:19 -04:00
gamer147
a4f029c0f8 Remove unverified binary tools from tracking 2026-08-03 13:06:45 -04:00
gamer147
17f31dc728 Add Linux core validation workflow 2026-08-03 12:28:46 -04:00
gamer147
5f6e647114 Normalize tracked text for Linux CI 2026-08-03 12:07:13 -04:00
gamer147
21c1317b05 Add repository toolchain policy 2026-08-03 11:54:18 -04:00
gamer147
067a11440e Document repository policy audit 2026-08-03 11:45:08 -04:00
gamer147
9e59e98efa Move corpus gate logic to frontend 2026-08-03 11:37:06 -04:00
gamer147
728399cb8b Move FFmpeg runtime to frontend 2026-08-03 11:33:38 -04:00
gamer147
81d063da37 Move movie runtime support to frontend 2026-08-03 11:29:47 -04:00
63 changed files with 8090 additions and 7676 deletions

18
.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{c,cpp,cs,h,py,ps1,sh}]
indent_style = space
indent_size = 4
[*.{csproj,json,jsonl,props,targets,toml,tscn,tres,xml,yaml,yml}]
indent_style = space
indent_size = 2
[*.{bat,cmd}]
end_of_line = crlf

69
.gitattributes vendored
View File

@@ -1,5 +1,72 @@
* text=auto
.editorconfig text eol=lf
.gitattributes text eol=lf
.gdignore text eol=lf
.gitignore text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.c text eol=lf
*.cfg text eol=lf
*.cs text eol=lf
*.csproj text eol=lf
*.config text eol=lf
*.cpp text eol=lf
*.gd text eol=lf
*.godot text eol=lf
*.h text eol=lf
*.json text eol=lf
*.jsonl text eol=lf
*.md text eol=lf
*.props text eol=lf
*.targets text eol=lf
*.ps1 text eol=lf
*.py text eol=lf
*.sh text eol=lf
*.sln text eol=lf
*.toml text eol=lf
*.tscn text eol=lf
*.tres text eol=lf
*.uid text eol=lf
*.xml text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.7z binary
*.a binary
*.agf binary
*.avi binary
*.bin binary
*.bmp binary
*.cur binary
*.dll binary
*.dylib binary
*.exe binary
*.gif binary
*.ico binary
*.jpeg binary
*.jpg binary
*.lib binary
*.mp3 binary
*.mp4 binary
*.mpeg binary
*.mpg binary
*.ogg binary
*.otf binary
*.pdb binary
*.png binary
*.rar binary
*.so binary
*.ttf binary
*.wav binary
*.woff binary
*.woff2 binary
*.zip binary
# Generated references are ordinary canonical-LF text. Their whitespace
# exceptions preserve generator-owned formatting without disabling EOL policy.
docs/opcode-reference.md whitespace=trailing-space,space-before-tab,cr-at-eol
docs/global-reference.md whitespace=trailing-space,space-before-tab,cr-at-eol
docs/engine-ctx-reference.md whitespace=trailing-space,space-before-tab,cr-at-eol
tools/age_opcodes_himegari.py whitespace=trailing-space,space-before-tab,cr-at-eol
*.sh text eol=lf

View File

@@ -0,0 +1,61 @@
name: Core validation
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:
permissions:
contents: read
concurrency:
group: core-${{ gitea.workflow }}-${{ gitea.ref }}
cancel-in-progress: true
jobs:
core:
name: Linux core gate
runs-on: ubuntu-latest
timeout-minutes: 15
env:
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
DOTNET_NOLOGO: "true"
NUGET_XMLDOC_MODE: skip
PYTHONUTF8: "1"
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python 3.11
uses: actions/setup-python@v6
with:
python-version: "3.11"
architecture: x64
- name: Set up .NET SDK
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Report toolchain
run: |
python --version
dotnet --version
- name: Run asset-independent validation
run: python -X utf8 tools/validate.py --level core
- name: Upload failure logs
if: failure()
uses: christopherhx/gitea-upload-artifact@v4
with:
name: core-validation-${{ gitea.run_id }}-${{ gitea.run_attempt }}
path: build/validation/**
if-no-files-found: ignore
retention-days: 7

3
bin/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!.gitignore
!README.md

15
bin/README.md Normal file
View File

@@ -0,0 +1,15 @@
# Local third-party tools
This directory is reserved for optional, machine-local binaries. Its contents are ignored by Git;
only this README and `.gitignore` are repository content.
The installed-data archive parity test can use `BinExtractALF.exe` here together with its matching
`LzssCpp.dll`. These files are not required by the runtime or the asset-independent test suite, and
their provenance and redistribution terms have not been verified. Do not commit them. The historical
local copies audited on 2026-08-03 had these SHA-256 values:
- `BinExtractALF.exe`: `46167CDF3DA1F02733CE1E7F0DA99D155E934EDE0987CA79C8927F48FEB25FE2`
- `LzssCpp.dll`: `596B9DDB07CA5C29CEE609E846F1512654EDEC56310A90D6C2A6951BE1E39FE1`
`pe-sieve32.exe` is no longer used. The failed IAT reconstruction experiment is retained in
`docs/engine-re.md`; the Frida import-map workflow replaced it.

View File

@@ -22,13 +22,17 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ └── *.exe (uninstallers), SAS0099.OGG … other shipped files.
├── extracted/ ← DERIVED (game-side) — extracted ALF contents,
│ │ ~3.9 GB, regenerable via age-reimpl/bin/BinExtractALF.
│ │ ~3.9 GB, regenerable with a local archive extractor.
│ └── DATA1/ … DATA5/ DATA1 = 481 .BIN scripts (the corpus we parse)
│ + AGF/BMP/WAV in the others.
└── age-reimpl/ ← OUR WORK (everything we made lives here)
├── README.md navigation-only repository front door; canonical facts stay in docs/
├── global.json pins the validated .NET 8 SDK feature band
├── .editorconfig / .gitattributes UTF-8, indentation, text/EOL, and binary-file policy;
│ tracked source and generated references use canonical LF
├── .gitea/workflows/core-validation.yml read-only Ubuntu source-only core gate for the private Gitea remote
├── run-godot.ps1 / run-godot.cmd tracked development launcher + Windows wrapper;
│ resolves Godot/game-root from parameters, environment,
│ PATH, and the conventional sibling install
@@ -56,10 +60,9 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ ├── movie-corpus-gate/ C# full-corpus FFmpeg decode/lifecycle acceptance tool
│ └── probe_*.py format reverse-engineering probes (historical)
├── bin/ 3rd-party binaries we use (not ours, not the game's)
│ ├── BinExtractALF.exe ALF archive extractor → produces extracted/
── LzssCpp.dll its LZSS codec dependency
│ └── pe-sieve32.exe (hollowsprocess) unpacked-PE dumper — engine-RE escalation (see docs/engine-re.md)
├── bin/ ignored home for optional machine-local tools
│ ├── README.md / .gitignore tracked policy; binaries never enter Git
── BinExtractALF.exe + LzssCpp.dll optional local archive oracle (unverified provenance)
├── vm-map/ VM / reverse-engineering reference artifacts
│ ├── opcodes.toml ★ CANONICAL opcode reference (hand-edited: ABI + semantics
@@ -118,9 +121,9 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings)
│ ├── Age.Engine/Persistence/ native S3SD/S4SD + S3RT codecs, shared payload/ReadTextDB,
│ layout-3 numbered state/history/gfx, DAT/STH pair + BMP codec,
│ and profile-owned state
│ ├── Age.Engine.Frontend/ platform-neutral frontend diagnostics/options project: page
│ locator, timeline/trace/step-limit diagnostics, VM/window launch
options, and performance-frame logging
│ ├── Age.Engine.Frontend/ platform-neutral frontend support: diagnostics/options, page
│ location, performance logging, movie runtime/FFmpeg decoding,
audio alignment, surface registry, and WAV sanitization
│ └── Age.Engine.Text.Windows/ optional exact Windows GDI gray-4 reference backend; kept
│ outside the platform-neutral VM/runtime core
├── native/ authored native runtime boundaries
@@ -154,8 +157,13 @@ full-width text entry.
Both `engine/AgeEngine.sln` and `godot/Himegari.sln` include the project; Godot and `Age.Engine.Tests` consume it
through project references rather than compiling duplicate linked sources. The first owned cluster contains
`PageLocatorState`, `GodotTimelineLog`, `GodotTraceSink`, `StepLimitDiagnosticFormatter`, `GodotVmOptions`,
`PerformanceFrameLog`, and `WindowLaunchOptions`. Movie runtime/decoder helpers remain under `godot/` until their
separate ownership slices.
`PerformanceFrameLog`, and `WindowLaunchOptions`. It also owns the internal movie decoder boundary/runtime,
audio-timeline alignment, movie-surface registry, and RIFF/WAVE sanitizer; only the `Himegari` and
`Age.Engine.Tests` assemblies receive explicit friend access to that internal API. FFmpeg native interop and the
asynchronous/paced decoder also live here; the `Age.MovieCorpusGate` tool receives friend access to the native
frame-source seam and references this project rather than compiling source links. Reusable movie-corpus discovery,
decode-gate, and report contracts live here as well; `tools/movie-corpus-gate/Program.cs` remains only the
command-line entry point and output writer.
`godot/GodotAdvHost.cs` retains cross-domain host coordination. Its partial-class companion
`godot/GodotAdvHost.AdvText.cs` owns live/retained ADV text, surface glyph rasterization and caching,
@@ -276,7 +284,8 @@ have no repository output tree and write their automatic maps below `user://diag
and under `extracted/DATA1/`, the game-folder copy (patch v1.03) wins. `paths.scripts()` resolves
this automatically (overrides win).
- **`build/` and `extracted/` are disposable.** `build/` regenerates via `tools/extract_phase2.py`
(or `sys4load.py`); `extracted/` regenerates via `bin/BinExtractALF.exe` on the `.ALF` files.
(or `sys4load.py`); `extracted/` regenerates from the `.ALF` files with a separately obtained local
extractor. `bin/README.md` records the optional local convention; no extractor binary is tracked.
Safe to delete and rebuild; do not hand-edit.
- **Opcode knowledge is edited ONLY in `vm-map/opcodes.toml`** (ABI + semantics + provenance +
`depends_on`). Run `tools/opcodes_build.py --build` to regenerate the shim (`tools/age_opcodes_himegari.py`),

View File

@@ -275,7 +275,7 @@ scene-local numeric addressing mode.
LZSS-or-raw header section, 4/8/truecolor source pixels, LZSS-or-raw pixel section, bottom-up row/stride
conversion, and optional `ACIF` LZSS alpha plane. Port only the algorithm and attribution into
platform-neutral .NET code; do not carry GARbro's WPF/GameRes dependencies. Kelebek's extractor and the
on-disk `BinExtractALF.exe` are validation references; the Kelebek repository exposes no clear license,
separately obtained local `BinExtractALF.exe` is a validation reference; the Kelebek repository exposes no clear license,
so its code should not be copied without clarification. The focused `LzssDecoder` is shared with
`Sys4AssetCatalog`; raw and compressed information/pixel/ACIF sections use the same bounded primitive.
4. **Runtime consumers (packed-id correction complete 2026-07-21).** Script loading, textures, voice,
@@ -461,7 +461,8 @@ VFS-B passes its bounded gates in `Sys4AssetStoreTests`: the installed AAI expan
`0x118` (expanded size at `0x110`, packed size at `0x114`) to one `APPEND01.ALF` archive and 81 80-byte
records. All records carry selector 1 and literal `$1$` names. The full directory has stable SHA-256
`23F0C104A45C099CEFB7D333362716EDE6F20B9EC53E4C3705A8E3A87063708E` over its ordered record fields.
The integration gate runs `BinExtractALF.exe` into a disposable directory, compares all 81 names, validates
The private installed-data integration gate can run a machine-local `bin/BinExtractALF.exe` into a disposable
directory, compares all 81 names, validates
every range and size, and byte-compares all 81 payloads. `Sys4ScriptProvider` resolves a real append script
through `0x01xxxxxx`; direct base-name lookup deliberately does not see append records.

View File

@@ -1,130 +1,130 @@
# EngineCtx field reference
> Generated from `vm-map/engine-ctx.toml` by `tools/engine_ctx_build.py --build`. Do not edit.
Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-handler `this` = `EngineCtx *`).
| offset | name | type | note |
|---|---|---|---|
| `0x4` | `host_ui_interface` | `void*` | host-facing UI interface used by the ctx+4 virtual-dispatch thunks; default slot +4 logs and presents AGE message boxes |
| `0x814` | `input_action_count` | `int` | logical action count (0..31); op 0xfe sets it, op 0x100 scans actions below it and uses callback slot count itself when the polled mask is empty |
| `0x898` | `joystick_physical_button_count` | `int` | WinMM JOYCAPS physical button count returned by op 0x106 |
| `0x89c` | `joystick_button_map` | `int` | base of 32-entry logical button-slot to physical joystick-button table; op 0x107 writes it, slot N emits action N+4 |
| `0x135c` | `mouse_button_action_map` | `int` | base of physical mouse-button to logical button-slot table; op 0x10b writes it and polling adds 4 to obtain the action index |
| `0x13dc` | `message_skip_enabled` | `int` | persistent all-message Skip flag; op 0x88 writes it and adv_interpreter_tick injects input bit 0x40 while nonzero |
| `0x1428` | `keyboard_vk_action_map` | `int` | base of 256-entry Win32 virtual-key to logical action table consumed by keyboard polling |
| `0x1828` | `dik_to_vk_table` | `int` | base of 256-entry DirectInput DIK scan-code to Win32 virtual-key translation used by op 0x10c |
| `0x1c34` | `mouse_wheel_delta` | `int` | signed WM_MOUSEWHEEL delta accumulated by age_main_window_proc; op 0x10d returns and clears it |
| `0x4d7c` | `shared_profile_state` | `void*` | embedded shared SAVE.DAT state object; owns profile integer/settings tables and container timing metadata |
| `0x5190` | `shared_profile_int_table` | `int` | open-addressing 12-byte string-key to 32-bit value table; op 0x1a2 stores, 0x1a3 loads, shared SAVE.DAT serializes it |
| `0x144e0` | `sfx_channel_resource_ids` | `int` | base of ten packed resource ids retained by the sound-effect facade; op 0xb4 loads a channel, op 0xb6 clears it, and numbered-save layouts restore then reopen every positive id |
| `0x14d54` | `adv_text_layout_ptr_table` | `void*` | text_manager at ctx+0x14940 plus layout pointer table +0x414; ops 0x212/0x213 configure each layout's wait-indicator handle and retained glyph-object range at layout+0x64/+0x68/+0x6c |
| `0x14e08` | `gfx_default_object_slot` | `int` | op 0x80 selected slot; op 0x1d9 substitutes it when its explicit object-slot operand is zero |
| `0x14e0c` | `text_primary_font_height` | `int` | primary LOGFONTA.lfHeight at text-manager+0x4cc; stored as a negative pixel height and updated by op 0x75 |
| `0x14e10` | `text_primary_font_width` | `int` | primary LOGFONTA.lfWidth at text-manager+0x4d0; op 0x75 stores negative half the requested font size |
| `0x14e1c` | `text_primary_font_weight` | `int` | primary LOGFONTA.lfWeight at text-manager+0x4dc; op 0x2bd writes 700 for enabled or zero for disabled |
| `0x14e84` | `text_antialias_enabled` | `int` | text-manager+0x544; message:UseAntiFont selects GGO_GRAY4_BITMAP when nonzero and GGO_BITMAP when zero |
| `0x14e88` | `text_antialias_version` | `int` | text-manager+0x548 loaded from set:AntiFontVersion; Himegari SYS4INI supplies version 3 |
| `0x14e8c` | `text_primary_color_bgr` | `uint` | text-manager+0x54c internal GDI BGR/COLORREF primary glyph color; op 0x76 converts script RGB into it |
| `0x14e90` | `text_effect_color_bgr` | `uint` | text-manager+0x550 internal GDI BGR/COLORREF effect glyph color; op 0x77 converts script RGB into it |
| `0x14e98` | `text_render_mode` | `int` | text-manager+0x558; op 0x78 selects primary-only, displaced shadow, reduced-coverage repeat, or sampled ellipse outline |
| `0x14ea0` | `text_line_spacing` | `int` | extra pixel leading between text lines; defaults to 6, op 0x8b writes it |
| `0x14ea4` | `text_effect_offset_x` | `int` | text-manager+0x564; op 0x1a4 horizontal displacement for mode 1 or ellipse radius for mode 3 |
| `0x14ea8` | `text_effect_offset_y` | `int` | text-manager+0x568; op 0x1a4 vertical displacement for mode 1 or ellipse radius for mode 3 |
| `0x14f45` | `script_frame_index` | `int` | call-script frame index (0x1e-dword frames) |
| `0x46188` | `renderer_device_state` | `int` | embedded D3D9 device/render state; op 0x20e passes this base to the target/depth clear worker |
| `0x46598` | `d3d9_device` | `void*` | IDirect3DDevice9 pointer at renderer_device_state+0x410; op 0x20e invokes vtable Clear +0xac |
| `0x46614` | `retained_gfx_manager` | `int` | embedded retained graphics manager passed by gfx opcode handlers; worker offsets are relative to this base |
| `0x46a1c` | `gfx_obj_registry` | `int` | retained_gfx_manager+0x408 std::map handle->object; 0x1f6 clears, geometry/draw get-or-create, 0x215 queries, 0x1f7 erases |
| `0x46d14` | `query_table_46d14` | `void*` | stride-0x14 table read by op 0x216 |
| `0x50ba4` | `gfx_render_target_table` | `void*` | retained_gfx_manager+0xa590 array of per-slot D3D texture wrappers selected by op 0x20d |
| `0x51b44` | `gfx_current_render_target` | `int` | retained_gfx_manager+0xb530 selected surface slot; -1 denotes device backbuffer |
| `0x51b64` | `frame_timer_current` | `int` | current shared millisecond presentation timestamp at retained_gfx_manager+0xb550; active outer-engine ticks and op 0x23c sample timeGetTime, and every retained animation channel samples it |
| `0x51b68` | `frame_timer_previous` | `int` | previous shared presentation timestamp at retained_gfx_manager+0xb554; shifted from current before each new sample, and op 0x231 compares current/previous source cells to raise redraw dirty only at cell changes |
| `0x51b6c` | `gfx_dirty_a` | `int` | retained_gfx_manager+0xb558 redraw-dirty flag; retained mutations and continuous channels set it, while op 0x231 sets it only when the sampled source cell differs across the shared current/previous frame timestamps; op 0x1f6 clears it |
| `0x51b70` | `gfx_force_complete` | `int` | retained_gfx_manager+0xb55c requests forced completion of one-shot channels; op 0x1f6 clears it |
| `0x51b74` | `gfx_dirty_b` | `int` | retained_gfx_manager+0xb560 blocking-presentation dirty flag; active finite object channels set it unless obj+0x2d0 bit 0 detaches them |
| `0x51b78` | `anim_clock_start` | `int` | start timestamp for the separate finite-animation service window at retained_gfx_manager+0xb564; seeded from frame_timer_current by gfx_animation_service_poll, while op 0x238, op 0x243, and object reset 0x1f6 zero it |
| `0x51b7c` | `anim_clock_duration` | `int` | global anim clock duration at retained_gfx_manager+0xb568; op 0x238 sets and object reset 0x1f6 zeroes it |
| `0x51b80` | `gfx_animation_service_flags` | `uint` | retained_gfx_manager+0xb56c service flags; bit 1 suppresses op 0x243 force-complete/clock-reset request |
| `0x52bd4` | `surfaces` | `void*` | surface array base [~1000 slots]; create/set-texture (0x1f8/0x1f9) allocate |
| `0x53d14` | `cur_ctx_index` | `uint` | current script-context index (curCtx); indexes 0x78-byte coroutine/frame records |
| `0x53d24` | `save_load_stack_restore_active` | `int` | set while a deserialized numbered-save script stack is being reconstructed; op 0xae restores saved frame PCs/contexts and clears it at the terminal saved context |
| `0x53d28` | `frame_codebase` | `void*` | current frame codebase (PC = codebase + off*4) |
| `0x53d2c` | `frame_pc` | `int` | current frame PC column (op = *(0x53d2c + curCtx*0x78)) |
| `0x53d60` | `ctx_record_base` | `void*` | 0x78-byte context-record array base (coroutine/script contexts) |
| `0x53d64` | `frame_script_resource_id` | `uint` | raw packed SYS4/AAI resource id for this 0x78-byte script frame; persisted ReadTextDB script key |
| `0x53d88` | `frame_instruction_word_count` | `int` | current decoded instruction length in dwords for each 0x78-byte script frame; interpreter advances PC by this value * 4 |
| `0x54fe8` | `owner_window_handle` | `void*` | native owner HWND forwarded to host UI/message-box calls, including op 0x1b4 diagnostic presentation |
| `0x54ff0` | `initial_root_run` | `int` | initialized to 1 at AGE context construction; queried by op 0x130 and cleared by op 0x9 before root script id 0 is reloaded |
| `0x550fc` | `message_skip_display_enabled` | `int` | persistent all-message Skip state returned by op 0x19a for the ADV control-strip active overlay |
| `0x55100` | `adv_skip_service_enabled` | `int` | ADV presentation lifecycle gate: op 0x19b clears it while preserving the persistent Skip toggle; op 0x19c sets it before recomputing active message/read skip |
| `0x55104` | `auto_message_enabled` | `int` | ADV Auto mode flag; op 0x1b6 reads, op 0x1b7 writes, adv_input_service_poll consumes |
| `0x55110` | `text_history_recording_suppressed` | `uint` | high bit suppresses ADV retained-history index/text/metadata/voice recording; op 0x1bb writes 0x80000000 or zero, and HISTORY.BIN brackets itself with disable/enable |
| `0x5511c` | `system_menu_show_delay_ms` | `uint` | op 0x149 replaces the TIMER_SHOWMENU top-edge dwell threshold; reset defaults to 0 and SYSTEM4 sets 1000; op 0x148 reads it |
| `0x55120` | `anti_tamper_a` | `int` | anti-tamper checksum operand |
| `0x55124` | `anti_tamper_b` | `int` | anti-tamper checksum operand |
| `0x5512c` | `anti_tamper_fp` | `int` | anti-tamper (import fn ptr / result) |
| `0x55130` | `int_queue_slots` | `void*` | base of ten scene-context-owned integer FIFO pointers used by ops 0x132-0x134; their <=10 validation bug makes id 10 alias int_stack_slots[0] |
| `0x55158` | `int_stack_slots` | `void*` | base of ten scene-context-owned integer LIFO pointers used by ops 0x137-0x139; their <=10 validation bug makes id 10 alias numeric_glyph_styles[0] |
| `0x55180` | `numeric_glyph_styles` | `int` | base of ten 20-byte decimal-glyph atlas records used by ops 0x13a/0x23b; opcode validation also admits one out-of-range record |
| `0x55248` | `ret_stack_a` | `void*` | per-frame return stack (op 0x8f call pushes) |
| `0x552e8` | `ret_stack_b` | `void*` | per-frame return stack (companion) |
| `0x5f304` | `sleep_timer` | `int` | sleep timer object (op 0xc8; +8 active, +0x14 start-ms, +0x18 duration) |
| `0x5f3ac` | `timed_callback_timer` | `int` | elapsed timer object armed by op 0xd5 and sampled by the timed callback scheduler |
| `0x5f64c` | `adv_wait_indicator_timer` | `int` | elapsed timer started by op 0x1ce and ADV wait setup to select the animated input-wait indicator frame |
| `0x5f690` | `timed_callback_abort_pc` | `int` | optional local fallback PC supplied to op 0xd5; -1 disables abort redirection |
| `0x5f694` | `timed_callback_entries_vector` | `int` | MSVC vector object base for 16-byte schedule entries; begin/end/capacity pointers follow |
| `0x5f698` | `timed_callback_entries_begin` | `void*` | begin pointer for 16-byte entries {deadline_ms, field_100, primary_pc, catchup_pc} |
| `0x5f69c` | `timed_callback_entries_end` | `void*` | end pointer for the timed callback entry vector |
| `0x5f6a0` | `timed_callback_entries_capacity` | `void*` | capacity-end pointer for the timed callback entry vector |
| `0x5f6a4` | `timed_callback_last_index` | `int` | last appended schedule index; op 0xd3 resets to -1 and op 0xd4 increments per entry |
| `0x5f6a8` | `timed_callback_cursor` | `int` | next schedule entry consumed by timed_callback_sequence_tick; op 0xd3 resets to zero |
| `0x5f6b4` | `timed_callback_script_resource_id` | `uint` | active frame script resource retained by op 0xd5 for local callback dispatch safety |
| `0x5f6b8` | `timed_callback_script_resource_guard` | `uint` | companion current-script identity checked before redirecting to a scheduled callback |
| `0x5f6c0` | `value_switch_table` | `int` | temporary formatted-value branch table reset by 0xa1, populated by 0xa2, queried by 0xa3; distinct from shared_profile_int_table |
| `0x5f72c` | `adv_wait_indicator_frame` | `int` | current animated input-wait indicator frame, reset to zero when the service is armed |
| `0x5f730` | `adv_wait_indicator_terminal_frame` | `int` | terminal frame returned by the indicator layout during ADV wait setup |
| `0x5f734` | `adv_wait_indicator_enabled_value` | `int` | raw enable operand last written by op 0x1ce; nonzero arms the ADV wait-indicator service |
| `0x6da6c` | `input_callback_scan_index` | `int` | op 0xff resets this to zero; op 0x100 advances it while scanning set action bits below input_action_count |
| `0x6da70` | `input_action_count_snapshot` | `int` | op 0xff copies input_action_count here when beginning a callback poll/dispatch pass |
| `0x6da78` | `message_skip_activation_guard` | `int` | set when op 0x88 enables Skip; suppresses the enabling click from immediately becoming a hotspot activation |
| `0x6da80` | `message_skip_cancel_click_phase` | `int` | press/release phase used only when set:CancelMesSkipOnClick is nonzero |
| `0x6da84` | `adv_wait_indicator_layout_slot` | `int` | ADV text layout whose wait indicator is armed or erased by input-wait setup and op 0x1ce |
| `0x6da88` | `coroutine_yield_a` | `void*` | op 0x7b yield-state save (op1 -> +ctxidx*4) |
| `0x6db28` | `coroutine_yield_b` | `void*` | op 0x7b yield-state save (op2 -> +ctxidx*4) |
| `0x6dbc8` | `coroutine_runstate` | `int` | op 0x7c resume gate (run-state bit 0x2000000) |
| `0x6dbcc` | `coroutine_resume_off` | `int` | op 0x7c resume PC offset |
| `0x6dbd4` | `adv_read_skip_state` | `int` | current ADV read/click-skip service state; queried by op 0x1cc, maintained from ReadTextSkip + per-PC read history |
| `0x6dbe4` | `adv_auto_voice_pending` | `int` | set by play-voice ops when voice service exists; Auto waits for voice completion then arms AutoMessageTime0; op 0x1bc resets |
| `0x6dbe8` | `voice_bgm_duck_saved_volume` | `int` | BGM volume saved by the voice-start duck helper before applying MusicFadeOnVoicePlayingVol |
| `0x6dbf0` | `voice_bgm_duck_control_flags` | `uint` | transient mask replaced by op 0x1cf; bit 0 suppresses automatic voice-triggered BGM attenuation |
| `0x6dbf4` | `message_skip_queued_voice_id` | `int` | latest voice resource deferred by op 0xc4 while run_state_flags message-skip bit is active |
| `0x6dbf8` | `message_skip_queued_voice_arg` | `int` | second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero |
| `0x6ddac` | `opcode_debug_metadata_table` | `void*` | base of 512 optional per-opcode debug-metadata pointers; Himegari's release image zero-initializes the table and has no writer, so diagnostic COMMAND names render as '-' |
| `0x6f798` | `script_source_line_table` | `void*` | base of 41 optional per-frame source-line-map pointers; Himegari's release image zero-initializes the table and has no writer, so diagnostic LINE values render as -1 |
| `0x6f880` | `diagnostic_text_buffer` | `int` | base of an embedded 28-byte MSVC string accumulator; op 0x1b2 appends formatted operand text, op 0x1b3 appends CRLF, and op 0x1b4 presents then clears it |
| `0x6f89c` | `script_launch_queue` | `int` | embedded integer FIFO used for engine-level auxiliary/root script launches; data pointer and queue indices follow |
| `0x6f8a0` | `script_launch_queue_data` | `void*` | dword storage for packed script ids or negative frame-resume ids |
| `0x6f8a4` | `script_launch_queue_read_cursor` | `int` | next queued launch consumed by script_launch_queue_dispatch_next |
| `0x6f8a8` | `script_launch_queue_write_cursor` | `int` | one-past-last queued launch; op 0x143 appends mounted selector record-zero ids here |
| `0x6f8ac` | `script_launch_queue_capacity` | `int` | allocated dword capacity; initialized to 0x100 |
| `0x6f8b0` | `script_launch_queue_growth` | `int` | capacity growth quantum; initialized to 0x100 |
| `0x6f8b4` | `script_launch_queue_high_water` | `int` | highest consumed cursor retained across FIFO compaction |
| `0x6f8b8` | `script_launch_dispatch_active` | `int` | suppresses immediate dispatch while op 0x143 batches mounted append record-zero ids and while a queued script is active |
| `0x9928c` | `save_frame_boundary_index` | `int` | highest script-frame index included by numbered-save layouts 2/3; -1 falls back to cur_ctx_index; op 0x1ad marks current frame and op 0x2 clears after unwinding below it |
| `0x99290` | `gfx_draw_mode` | `int` | loaded from set:DrawMode during runtime initialization; Himegari uses mode 1, selecting the cached GetGlyphOutline text path |
| `0x9b24c` | `dispatch_table` | `void*` | opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4) |
| `0x9c658` | `sys4ini_count` | `int` | SYS4INI record count at embedded FileDB+0x40c |
| `0x9c65c` | `archive_name_table` | `void*` | archive-name table base at embedded FileDB+0x410; arc_id*0x100 indexes it |
| `0x9c660` | `sys4ini_records` | `void*` | SYS4INI 80-byte record base at embedded FileDB+0x414; record = base + id*0x50 |
| `0x9f274` | `mounted_aai_catalogs` | `void*` | base of 256-entry selector-keyed AAI catalog-pointer table inside the embedded FileDB; op 0x143 scans slots 1..255 from +0x9f278 |
| `0x9f278` | `mounted_aai_catalog_selector_1` | `void*` | selector-one cell and op 0x143 scan start; subsequent dwords are selector 2..255 |
| `0xa0b84` | `current_bgm_track_id` | `int` | direct-name BGM track id retained by the music facade; op 0xbf starts/replaces it, ops 0xc0/0xc3 get/set it, and numbered-save layouts restore it |
| `0xa0b8c` | `current_bgm_start_mode` | `int` | logical playback mode retained by the music facade and forwarded to its backend: zero stops at decoder EOF, nonzero rewinds; ordinary op 0xbf and forced op 0xb7 use one, forced op 0xb9 uses zero |
| `0xa0cc0` | `screen_w` | `int` | logical screen width; constructor/default registry uses 640, then the SYS4INI SCREENX setting overrides it (Himegari 800) |
| `0xa0cc4` | `screen_h` | `int` | logical screen height; constructor/default registry uses 480, then the SYS4INI SCREENY setting overrides it (Himegari 600) |
| `0xa0cc8` | `screen_bpp` | `int` | screen bpp (8) |
| `0xa0ce4` | `run_state_flags` | `uint` | interpreter run-state flags (bit1 sleeping; 0x8000000 skip/fast-forward) |
| `0xa0ce8` | `timed_callback_abort_signal` | `int` | scheduler-polled abort/input signal cleared when op 0xd5 starts; redirects to abort PC when configured |
| `0xa0d10` | `system_menu_actions_enabled` | `int` | op 0x142 replaces this AGERC/IAGEService reentrancy guard; scene reset defaults it to 1, while CONFIG brackets its scripted settings UI with 0 then 1 |
# EngineCtx field reference
> Generated from `vm-map/engine-ctx.toml` by `tools/engine_ctx_build.py --build`. Do not edit.
Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-handler `this` = `EngineCtx *`).
| offset | name | type | note |
|---|---|---|---|
| `0x4` | `host_ui_interface` | `void*` | host-facing UI interface used by the ctx+4 virtual-dispatch thunks; default slot +4 logs and presents AGE message boxes |
| `0x814` | `input_action_count` | `int` | logical action count (0..31); op 0xfe sets it, op 0x100 scans actions below it and uses callback slot count itself when the polled mask is empty |
| `0x898` | `joystick_physical_button_count` | `int` | WinMM JOYCAPS physical button count returned by op 0x106 |
| `0x89c` | `joystick_button_map` | `int` | base of 32-entry logical button-slot to physical joystick-button table; op 0x107 writes it, slot N emits action N+4 |
| `0x135c` | `mouse_button_action_map` | `int` | base of physical mouse-button to logical button-slot table; op 0x10b writes it and polling adds 4 to obtain the action index |
| `0x13dc` | `message_skip_enabled` | `int` | persistent all-message Skip flag; op 0x88 writes it and adv_interpreter_tick injects input bit 0x40 while nonzero |
| `0x1428` | `keyboard_vk_action_map` | `int` | base of 256-entry Win32 virtual-key to logical action table consumed by keyboard polling |
| `0x1828` | `dik_to_vk_table` | `int` | base of 256-entry DirectInput DIK scan-code to Win32 virtual-key translation used by op 0x10c |
| `0x1c34` | `mouse_wheel_delta` | `int` | signed WM_MOUSEWHEEL delta accumulated by age_main_window_proc; op 0x10d returns and clears it |
| `0x4d7c` | `shared_profile_state` | `void*` | embedded shared SAVE.DAT state object; owns profile integer/settings tables and container timing metadata |
| `0x5190` | `shared_profile_int_table` | `int` | open-addressing 12-byte string-key to 32-bit value table; op 0x1a2 stores, 0x1a3 loads, shared SAVE.DAT serializes it |
| `0x144e0` | `sfx_channel_resource_ids` | `int` | base of ten packed resource ids retained by the sound-effect facade; op 0xb4 loads a channel, op 0xb6 clears it, and numbered-save layouts restore then reopen every positive id |
| `0x14d54` | `adv_text_layout_ptr_table` | `void*` | text_manager at ctx+0x14940 plus layout pointer table +0x414; ops 0x212/0x213 configure each layout's wait-indicator handle and retained glyph-object range at layout+0x64/+0x68/+0x6c |
| `0x14e08` | `gfx_default_object_slot` | `int` | op 0x80 selected slot; op 0x1d9 substitutes it when its explicit object-slot operand is zero |
| `0x14e0c` | `text_primary_font_height` | `int` | primary LOGFONTA.lfHeight at text-manager+0x4cc; stored as a negative pixel height and updated by op 0x75 |
| `0x14e10` | `text_primary_font_width` | `int` | primary LOGFONTA.lfWidth at text-manager+0x4d0; op 0x75 stores negative half the requested font size |
| `0x14e1c` | `text_primary_font_weight` | `int` | primary LOGFONTA.lfWeight at text-manager+0x4dc; op 0x2bd writes 700 for enabled or zero for disabled |
| `0x14e84` | `text_antialias_enabled` | `int` | text-manager+0x544; message:UseAntiFont selects GGO_GRAY4_BITMAP when nonzero and GGO_BITMAP when zero |
| `0x14e88` | `text_antialias_version` | `int` | text-manager+0x548 loaded from set:AntiFontVersion; Himegari SYS4INI supplies version 3 |
| `0x14e8c` | `text_primary_color_bgr` | `uint` | text-manager+0x54c internal GDI BGR/COLORREF primary glyph color; op 0x76 converts script RGB into it |
| `0x14e90` | `text_effect_color_bgr` | `uint` | text-manager+0x550 internal GDI BGR/COLORREF effect glyph color; op 0x77 converts script RGB into it |
| `0x14e98` | `text_render_mode` | `int` | text-manager+0x558; op 0x78 selects primary-only, displaced shadow, reduced-coverage repeat, or sampled ellipse outline |
| `0x14ea0` | `text_line_spacing` | `int` | extra pixel leading between text lines; defaults to 6, op 0x8b writes it |
| `0x14ea4` | `text_effect_offset_x` | `int` | text-manager+0x564; op 0x1a4 horizontal displacement for mode 1 or ellipse radius for mode 3 |
| `0x14ea8` | `text_effect_offset_y` | `int` | text-manager+0x568; op 0x1a4 vertical displacement for mode 1 or ellipse radius for mode 3 |
| `0x14f45` | `script_frame_index` | `int` | call-script frame index (0x1e-dword frames) |
| `0x46188` | `renderer_device_state` | `int` | embedded D3D9 device/render state; op 0x20e passes this base to the target/depth clear worker |
| `0x46598` | `d3d9_device` | `void*` | IDirect3DDevice9 pointer at renderer_device_state+0x410; op 0x20e invokes vtable Clear +0xac |
| `0x46614` | `retained_gfx_manager` | `int` | embedded retained graphics manager passed by gfx opcode handlers; worker offsets are relative to this base |
| `0x46a1c` | `gfx_obj_registry` | `int` | retained_gfx_manager+0x408 std::map handle->object; 0x1f6 clears, geometry/draw get-or-create, 0x215 queries, 0x1f7 erases |
| `0x46d14` | `query_table_46d14` | `void*` | stride-0x14 table read by op 0x216 |
| `0x50ba4` | `gfx_render_target_table` | `void*` | retained_gfx_manager+0xa590 array of per-slot D3D texture wrappers selected by op 0x20d |
| `0x51b44` | `gfx_current_render_target` | `int` | retained_gfx_manager+0xb530 selected surface slot; -1 denotes device backbuffer |
| `0x51b64` | `frame_timer_current` | `int` | current shared millisecond presentation timestamp at retained_gfx_manager+0xb550; active outer-engine ticks and op 0x23c sample timeGetTime, and every retained animation channel samples it |
| `0x51b68` | `frame_timer_previous` | `int` | previous shared presentation timestamp at retained_gfx_manager+0xb554; shifted from current before each new sample, and op 0x231 compares current/previous source cells to raise redraw dirty only at cell changes |
| `0x51b6c` | `gfx_dirty_a` | `int` | retained_gfx_manager+0xb558 redraw-dirty flag; retained mutations and continuous channels set it, while op 0x231 sets it only when the sampled source cell differs across the shared current/previous frame timestamps; op 0x1f6 clears it |
| `0x51b70` | `gfx_force_complete` | `int` | retained_gfx_manager+0xb55c requests forced completion of one-shot channels; op 0x1f6 clears it |
| `0x51b74` | `gfx_dirty_b` | `int` | retained_gfx_manager+0xb560 blocking-presentation dirty flag; active finite object channels set it unless obj+0x2d0 bit 0 detaches them |
| `0x51b78` | `anim_clock_start` | `int` | start timestamp for the separate finite-animation service window at retained_gfx_manager+0xb564; seeded from frame_timer_current by gfx_animation_service_poll, while op 0x238, op 0x243, and object reset 0x1f6 zero it |
| `0x51b7c` | `anim_clock_duration` | `int` | global anim clock duration at retained_gfx_manager+0xb568; op 0x238 sets and object reset 0x1f6 zeroes it |
| `0x51b80` | `gfx_animation_service_flags` | `uint` | retained_gfx_manager+0xb56c service flags; bit 1 suppresses op 0x243 force-complete/clock-reset request |
| `0x52bd4` | `surfaces` | `void*` | surface array base [~1000 slots]; create/set-texture (0x1f8/0x1f9) allocate |
| `0x53d14` | `cur_ctx_index` | `uint` | current script-context index (curCtx); indexes 0x78-byte coroutine/frame records |
| `0x53d24` | `save_load_stack_restore_active` | `int` | set while a deserialized numbered-save script stack is being reconstructed; op 0xae restores saved frame PCs/contexts and clears it at the terminal saved context |
| `0x53d28` | `frame_codebase` | `void*` | current frame codebase (PC = codebase + off*4) |
| `0x53d2c` | `frame_pc` | `int` | current frame PC column (op = *(0x53d2c + curCtx*0x78)) |
| `0x53d60` | `ctx_record_base` | `void*` | 0x78-byte context-record array base (coroutine/script contexts) |
| `0x53d64` | `frame_script_resource_id` | `uint` | raw packed SYS4/AAI resource id for this 0x78-byte script frame; persisted ReadTextDB script key |
| `0x53d88` | `frame_instruction_word_count` | `int` | current decoded instruction length in dwords for each 0x78-byte script frame; interpreter advances PC by this value * 4 |
| `0x54fe8` | `owner_window_handle` | `void*` | native owner HWND forwarded to host UI/message-box calls, including op 0x1b4 diagnostic presentation |
| `0x54ff0` | `initial_root_run` | `int` | initialized to 1 at AGE context construction; queried by op 0x130 and cleared by op 0x9 before root script id 0 is reloaded |
| `0x550fc` | `message_skip_display_enabled` | `int` | persistent all-message Skip state returned by op 0x19a for the ADV control-strip active overlay |
| `0x55100` | `adv_skip_service_enabled` | `int` | ADV presentation lifecycle gate: op 0x19b clears it while preserving the persistent Skip toggle; op 0x19c sets it before recomputing active message/read skip |
| `0x55104` | `auto_message_enabled` | `int` | ADV Auto mode flag; op 0x1b6 reads, op 0x1b7 writes, adv_input_service_poll consumes |
| `0x55110` | `text_history_recording_suppressed` | `uint` | high bit suppresses ADV retained-history index/text/metadata/voice recording; op 0x1bb writes 0x80000000 or zero, and HISTORY.BIN brackets itself with disable/enable |
| `0x5511c` | `system_menu_show_delay_ms` | `uint` | op 0x149 replaces the TIMER_SHOWMENU top-edge dwell threshold; reset defaults to 0 and SYSTEM4 sets 1000; op 0x148 reads it |
| `0x55120` | `anti_tamper_a` | `int` | anti-tamper checksum operand |
| `0x55124` | `anti_tamper_b` | `int` | anti-tamper checksum operand |
| `0x5512c` | `anti_tamper_fp` | `int` | anti-tamper (import fn ptr / result) |
| `0x55130` | `int_queue_slots` | `void*` | base of ten scene-context-owned integer FIFO pointers used by ops 0x132-0x134; their <=10 validation bug makes id 10 alias int_stack_slots[0] |
| `0x55158` | `int_stack_slots` | `void*` | base of ten scene-context-owned integer LIFO pointers used by ops 0x137-0x139; their <=10 validation bug makes id 10 alias numeric_glyph_styles[0] |
| `0x55180` | `numeric_glyph_styles` | `int` | base of ten 20-byte decimal-glyph atlas records used by ops 0x13a/0x23b; opcode validation also admits one out-of-range record |
| `0x55248` | `ret_stack_a` | `void*` | per-frame return stack (op 0x8f call pushes) |
| `0x552e8` | `ret_stack_b` | `void*` | per-frame return stack (companion) |
| `0x5f304` | `sleep_timer` | `int` | sleep timer object (op 0xc8; +8 active, +0x14 start-ms, +0x18 duration) |
| `0x5f3ac` | `timed_callback_timer` | `int` | elapsed timer object armed by op 0xd5 and sampled by the timed callback scheduler |
| `0x5f64c` | `adv_wait_indicator_timer` | `int` | elapsed timer started by op 0x1ce and ADV wait setup to select the animated input-wait indicator frame |
| `0x5f690` | `timed_callback_abort_pc` | `int` | optional local fallback PC supplied to op 0xd5; -1 disables abort redirection |
| `0x5f694` | `timed_callback_entries_vector` | `int` | MSVC vector object base for 16-byte schedule entries; begin/end/capacity pointers follow |
| `0x5f698` | `timed_callback_entries_begin` | `void*` | begin pointer for 16-byte entries {deadline_ms, field_100, primary_pc, catchup_pc} |
| `0x5f69c` | `timed_callback_entries_end` | `void*` | end pointer for the timed callback entry vector |
| `0x5f6a0` | `timed_callback_entries_capacity` | `void*` | capacity-end pointer for the timed callback entry vector |
| `0x5f6a4` | `timed_callback_last_index` | `int` | last appended schedule index; op 0xd3 resets to -1 and op 0xd4 increments per entry |
| `0x5f6a8` | `timed_callback_cursor` | `int` | next schedule entry consumed by timed_callback_sequence_tick; op 0xd3 resets to zero |
| `0x5f6b4` | `timed_callback_script_resource_id` | `uint` | active frame script resource retained by op 0xd5 for local callback dispatch safety |
| `0x5f6b8` | `timed_callback_script_resource_guard` | `uint` | companion current-script identity checked before redirecting to a scheduled callback |
| `0x5f6c0` | `value_switch_table` | `int` | temporary formatted-value branch table reset by 0xa1, populated by 0xa2, queried by 0xa3; distinct from shared_profile_int_table |
| `0x5f72c` | `adv_wait_indicator_frame` | `int` | current animated input-wait indicator frame, reset to zero when the service is armed |
| `0x5f730` | `adv_wait_indicator_terminal_frame` | `int` | terminal frame returned by the indicator layout during ADV wait setup |
| `0x5f734` | `adv_wait_indicator_enabled_value` | `int` | raw enable operand last written by op 0x1ce; nonzero arms the ADV wait-indicator service |
| `0x6da6c` | `input_callback_scan_index` | `int` | op 0xff resets this to zero; op 0x100 advances it while scanning set action bits below input_action_count |
| `0x6da70` | `input_action_count_snapshot` | `int` | op 0xff copies input_action_count here when beginning a callback poll/dispatch pass |
| `0x6da78` | `message_skip_activation_guard` | `int` | set when op 0x88 enables Skip; suppresses the enabling click from immediately becoming a hotspot activation |
| `0x6da80` | `message_skip_cancel_click_phase` | `int` | press/release phase used only when set:CancelMesSkipOnClick is nonzero |
| `0x6da84` | `adv_wait_indicator_layout_slot` | `int` | ADV text layout whose wait indicator is armed or erased by input-wait setup and op 0x1ce |
| `0x6da88` | `coroutine_yield_a` | `void*` | op 0x7b yield-state save (op1 -> +ctxidx*4) |
| `0x6db28` | `coroutine_yield_b` | `void*` | op 0x7b yield-state save (op2 -> +ctxidx*4) |
| `0x6dbc8` | `coroutine_runstate` | `int` | op 0x7c resume gate (run-state bit 0x2000000) |
| `0x6dbcc` | `coroutine_resume_off` | `int` | op 0x7c resume PC offset |
| `0x6dbd4` | `adv_read_skip_state` | `int` | current ADV read/click-skip service state; queried by op 0x1cc, maintained from ReadTextSkip + per-PC read history |
| `0x6dbe4` | `adv_auto_voice_pending` | `int` | set by play-voice ops when voice service exists; Auto waits for voice completion then arms AutoMessageTime0; op 0x1bc resets |
| `0x6dbe8` | `voice_bgm_duck_saved_volume` | `int` | BGM volume saved by the voice-start duck helper before applying MusicFadeOnVoicePlayingVol |
| `0x6dbf0` | `voice_bgm_duck_control_flags` | `uint` | transient mask replaced by op 0x1cf; bit 0 suppresses automatic voice-triggered BGM attenuation |
| `0x6dbf4` | `message_skip_queued_voice_id` | `int` | latest voice resource deferred by op 0xc4 while run_state_flags message-skip bit is active |
| `0x6dbf8` | `message_skip_queued_voice_arg` | `int` | second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero |
| `0x6ddac` | `opcode_debug_metadata_table` | `void*` | base of 512 optional per-opcode debug-metadata pointers; Himegari's release image zero-initializes the table and has no writer, so diagnostic COMMAND names render as '-' |
| `0x6f798` | `script_source_line_table` | `void*` | base of 41 optional per-frame source-line-map pointers; Himegari's release image zero-initializes the table and has no writer, so diagnostic LINE values render as -1 |
| `0x6f880` | `diagnostic_text_buffer` | `int` | base of an embedded 28-byte MSVC string accumulator; op 0x1b2 appends formatted operand text, op 0x1b3 appends CRLF, and op 0x1b4 presents then clears it |
| `0x6f89c` | `script_launch_queue` | `int` | embedded integer FIFO used for engine-level auxiliary/root script launches; data pointer and queue indices follow |
| `0x6f8a0` | `script_launch_queue_data` | `void*` | dword storage for packed script ids or negative frame-resume ids |
| `0x6f8a4` | `script_launch_queue_read_cursor` | `int` | next queued launch consumed by script_launch_queue_dispatch_next |
| `0x6f8a8` | `script_launch_queue_write_cursor` | `int` | one-past-last queued launch; op 0x143 appends mounted selector record-zero ids here |
| `0x6f8ac` | `script_launch_queue_capacity` | `int` | allocated dword capacity; initialized to 0x100 |
| `0x6f8b0` | `script_launch_queue_growth` | `int` | capacity growth quantum; initialized to 0x100 |
| `0x6f8b4` | `script_launch_queue_high_water` | `int` | highest consumed cursor retained across FIFO compaction |
| `0x6f8b8` | `script_launch_dispatch_active` | `int` | suppresses immediate dispatch while op 0x143 batches mounted append record-zero ids and while a queued script is active |
| `0x9928c` | `save_frame_boundary_index` | `int` | highest script-frame index included by numbered-save layouts 2/3; -1 falls back to cur_ctx_index; op 0x1ad marks current frame and op 0x2 clears after unwinding below it |
| `0x99290` | `gfx_draw_mode` | `int` | loaded from set:DrawMode during runtime initialization; Himegari uses mode 1, selecting the cached GetGlyphOutline text path |
| `0x9b24c` | `dispatch_table` | `void*` | opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4) |
| `0x9c658` | `sys4ini_count` | `int` | SYS4INI record count at embedded FileDB+0x40c |
| `0x9c65c` | `archive_name_table` | `void*` | archive-name table base at embedded FileDB+0x410; arc_id*0x100 indexes it |
| `0x9c660` | `sys4ini_records` | `void*` | SYS4INI 80-byte record base at embedded FileDB+0x414; record = base + id*0x50 |
| `0x9f274` | `mounted_aai_catalogs` | `void*` | base of 256-entry selector-keyed AAI catalog-pointer table inside the embedded FileDB; op 0x143 scans slots 1..255 from +0x9f278 |
| `0x9f278` | `mounted_aai_catalog_selector_1` | `void*` | selector-one cell and op 0x143 scan start; subsequent dwords are selector 2..255 |
| `0xa0b84` | `current_bgm_track_id` | `int` | direct-name BGM track id retained by the music facade; op 0xbf starts/replaces it, ops 0xc0/0xc3 get/set it, and numbered-save layouts restore it |
| `0xa0b8c` | `current_bgm_start_mode` | `int` | logical playback mode retained by the music facade and forwarded to its backend: zero stops at decoder EOF, nonzero rewinds; ordinary op 0xbf and forced op 0xb7 use one, forced op 0xb9 uses zero |
| `0xa0cc0` | `screen_w` | `int` | logical screen width; constructor/default registry uses 640, then the SYS4INI SCREENX setting overrides it (Himegari 800) |
| `0xa0cc4` | `screen_h` | `int` | logical screen height; constructor/default registry uses 480, then the SYS4INI SCREENY setting overrides it (Himegari 600) |
| `0xa0cc8` | `screen_bpp` | `int` | screen bpp (8) |
| `0xa0ce4` | `run_state_flags` | `uint` | interpreter run-state flags (bit1 sleeping; 0x8000000 skip/fast-forward) |
| `0xa0ce8` | `timed_callback_abort_signal` | `int` | scheduler-polled abort/input signal cleared when op 0xd5 starts; redirects to abort PC when configured |
| `0xa0d10` | `system_menu_actions_enabled` | `int` | op 0x142 replaces this AGERC/IAGEService reentrancy guard; scene reset defaults it to 1, while CONFIG brackets its scripted settings UI with 0 then 1 |

View File

@@ -34,7 +34,7 @@ Related: `docs/scjump-progression.md` (the SCJUMP decoder that hit this wall), `
→ `run_analysis`.
- **Load sanity check (AGF-decoder landmark):** at VA `0x474f23`, `CMP word ptr [ESI + 0x4], 0x4d42`
(the `BM`/BMP-magic check) confirms the image is correctly based + decoded.
- IAT reconstruction — **tried, DOESN'T WORK on this binary (2026-07-09):** `bin/pe-sieve32.exe /pid
- IAT reconstruction — **tried, DOESN'T WORK on this binary (2026-07-09):** a local PE-sieve 0.4.1.1 `/pid
<PID> /imp 3 /dmode 3 /dir build/pe-sieve` (run from **PowerShell**, not Git Bash — it mangles
`/flags`) ran fine but the game is packed with a **zeroed IAT** resolved via `GetProcAddress` at load,
so there is no conventional import table to rebuild. Of 363 "imports" it emitted, only ~17 are genuine
@@ -46,7 +46,8 @@ Related: `docs/scjump-progression.md` (the SCJUMP decoder that hit this wall), `
30×, `in_main:0`, non-terminated). ⇒ **do not graft pe-sieve output** — grafting the noise would inject
wrong import names. The game's hot APIs (`ReadFile`/`CreateFileA`/`timeGetTime`/d3d9 device methods) are
`GetProcAddress`-resolved into private pointer tables, invisible to a static IAT scan. Report/dump left
at `build/pe-sieve/process_<pid>/` (disposable).
at `build/pe-sieve/process_<pid>/` (disposable). The executable was removed from the repository and purged
from reachable history on 2026-08-03; it is obsolete and is not needed to reproduce the accepted workflow.
**→ The Frida import-map approach — ✅ DONE 2026-07-09 (replaced pe-sieve).** Named the
dynamically-resolved APIs at their call sites via the LIVE process. `tools/frida/map_imports.py`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -52,10 +52,10 @@ claiming portable exports.
| ADV font discovery/raster fidelity | `Age.Engine.Text` remains OS-neutral. On compatible ACP-932 Windows, `auto` selects the separate exact-to-request `Age.Engine.Text.Windows` GDI gray-4 backend. Otherwise Godot TextServer supplies Unicode masks and metrics from ordered system-font substitutions in `godot/config/himegari-text-rendering.json`; this policy is explicitly not GDI pixel-exact. Immediate `0x204`/`0x205`, live ADV, retained History, and the wait atlas all enter ordinary RGBA surfaces/retained handles | Three Mincho/Gothic regular/bold samples byte-match an independent Unicode GDI oracle. A live `SC0000@0x869` trace proves that the `AGE Patch.exe`/`jprun.dll` wrapper overrides authored Mincho to MS Gothic: 13 captured masks match Gothic byte-for-byte and Mincho 0/13. This is wrapper behavior, not the true-native default. Forced exact and forced portable self-tests cover immediate, live, and History rasterization, bounded caching, complete-line materialization, retained ordering, alpha/tint/fade, transforms, clipping, capture, HIDEWIN suspension, partial erase/republication, reset, and teardown. Gameplay `Label` pools and overlay projection are gone | Optionally expose realized-face overrides through future launch profiles when wrapper compatibility is desired; independently produce and smoke-test Linux/macOS exports, confirm installed family availability/fallback selection there, and keep the GDI project out of non-Windows deliverables |
| Filesystem semantics | Several filename and containment comparisons use `OrdinalIgnoreCase`; installed assets are conventionally uppercase | Needs validation on case-sensitive filesystems; may hide casing or containment mistakes | Add Linux/macOS tests with mixed-case synthetic roots and use filesystem-appropriate containment rules |
| Save/profile/settings storage | `Sys4PersistencePaths` models AGE's independent `SAVEPATH` and `REGFILEPATH` resolutions. Godot replaces Himegari's related profile directory with `user://`, yielding `user://SAVE` for native S3SD/S4SD/S3RT files and thumbnails plus `user://SYS4REG.INI` for the BOM-less CP932 options file; the preserving writer changes only its nine `[sound]` keys | Save payloads and engine options are isolated together without changing either native format. Native/drop-in resolution remains available through `USEAPPDATAFOLDER` plus both SYS4INI paths. A single-root override is rejected when `SAVEPATH` is not beneath `REGFILEPATH`, preventing cross-profile guesses | Expose explicit profile/native selection through the future launcher and allow independent overrides for profiles whose two native paths are unrelated. Validate CP932 availability, replace/flush, case, permissions, and interrupted-write behavior on each export target |
| Game-install, runtime metadata, and repository discovery | `GameRootSelection` accepts `--game-root`, then probes the executable directory and current working directory for `SYS4INI.BIN`; on Unix the frontend prefers inherited shell `PWD` because Godot may change the process directory during project startup. Godot injects the selected root into its catalog and loose-first ALF store. `Himegari.csproj` embeds generated `build/opcodes.json`, and the frontend loads that assembly resource rather than `Paths.OpcodesJson`. `Paths.cs` remains a development/test locator; its page-map use is compiled only for `TOOLS` builds | Installed game data and packaged VM metadata no longer depend on the workspace sibling layout. The Linux export passed an explicit absolute game root outside the repository and read the installed archives. Exported automatic page maps use `user://diagnostics/page-maps`; editor/development runs preserve `build/page-map-<SCENE>.jsonl` | Let the future profile launcher pass `--game-root`, and run executable-directory/CWD plus case/permission smoke gates on Linux and macOS |
| Archive parity oracle | One integration test launches `bin/BinExtractALF.exe` | Windows-only test helper, not a shipped runtime dependency | Skip/replace on non-Windows CI; runtime ALF/AAI readers do not depend on it |
| Native RE tools | Frida/Ghidra helpers target the original `AGE.EXE`; supporting utilities include Windows executables and Windows command conventions | Development/research only | Keep separate from export requirements; document platform prerequisites per tool |
| Python workflow | Operating guide uses Windows `py -3.11` invocation | Developer workflow only | Add equivalent `python3` instructions if non-Windows development becomes active |
| Game-install, runtime metadata, and repository discovery | `GameRootSelection` accepts `--game-root`, then probes the executable directory and current working directory for `SYS4INI.BIN`; on Unix the frontend prefers inherited shell `PWD` because Godot may change the process directory during project startup. Godot injects the selected root into its catalog and loose-first ALF store. `Himegari.csproj` embeds generated `build/opcodes.json`, and the frontend loads that assembly resource rather than `Paths.OpcodesJson`. Development/test `Paths.cs` finds the repository by tracked `global.json`, solution, and opcode-map markers rather than a checkout-directory name | Installed game data and packaged VM metadata no longer depend on the workspace sibling layout. The Linux export passed an explicit absolute game root outside the repository and read the installed archives; a source-only core proof also passes from an arbitrary repository directory name. Exported automatic page maps use `user://diagnostics/page-maps`; editor/development runs preserve `build/page-map-<SCENE>.jsonl` | Let the future profile launcher pass `--game-root`, and run executable-directory/CWD plus case/permission smoke gates on Linux and macOS |
| Archive parity oracle | One integration test can launch a machine-local `bin/BinExtractALF.exe`; the binary and matching DLL are ignored and have unverified redistribution terms | Windows-only installed-data helper, not a shipped runtime dependency | Keep it in an explicit private workspace gate; runtime ALF/AAI readers do not depend on it |
| Native RE tools | Frida/Ghidra helpers target the original `AGE.EXE`; PE-sieve was tried and replaced by the Frida import-map workflow | Development/research only; no native RE executable is tracked | Keep separate from export requirements; document platform prerequisites per tool |
| Python workflow and core CI | The operating guide uses Windows `py -3.11`; `validate.py` launches child tools through `sys.executable` and has POSIX process-group cleanup plus `ps`-based leak auditing | The 590 engine cases are explicitly split into 502 source-only core cases and 88 `Workspace` installed-data/native-oracle cases; both selections pass with zero skips. The opcode-bootstrap unit injects synthetic observations while production bootstrap retains real-corpus scanning. `.gitea/workflows/core-validation.yml` uses the target server's demonstrated `ubuntu-latest`, checkout/setup-dotnet v4, setup-python v6, and Gitea artifact action baseline. A standalone arbitrary-name, corpus-free repository passes the complete core driver | Confirm the first real Linux run after the rewritten history is pushed. Retain Windows-native GDI parity and private installed-game/runtime checks as separate trusted gates |
No authored runtime code currently calls native DirectSound, Direct3D, Vulkan, Metal, or OpenGL. Mentions of
those APIs in `docs/engine-re.md` describe the original AGE implementation. Both the software-oracle display

View File

@@ -834,6 +834,27 @@ do not mix mechanical moves with semantic changes.
test source links without changing public types, namespaces, behavior, or export configuration. Both solution
builds and runtime validation remain green.
The second bounded build-graph slice moved the unchanged internal movie decoder boundary/runtime, audio
alignment policy, movie-surface registry, and RIFF/WAVE sanitizer from `godot/` into `Age.Engine.Frontend`.
Five more direct test source links are gone. Explicit friend access is limited to `Himegari` and
`Age.Engine.Tests`, so the move preserves the intentionally internal API instead of widening it; FFmpeg remains
in Godot while implementing the moved boundary through that friend access. Both solution builds and runtime
validation remain green.
The third bounded build-graph slice moved the unchanged FFmpeg native interop and asynchronous/paced decoder
from `godot/` into `Age.Engine.Frontend`. The two remaining movie test source links and the corpus tool's native
source link were replaced by project references. Friend access now also names only the `Age.MovieCorpusGate`
assembly required by the native frame-source seam; native ABI resolution, decoder pacing, seek/audio behavior,
failure handling, and teardown remain unchanged. Engine, Godot, and corpus-gate builds plus runtime validation
remain green.
The fourth bounded build-graph slice moved the unchanged reusable movie-corpus discovery, decode-gate, and
report implementation from `tools/movie-corpus-gate/` into `Age.Engine.Frontend`. The tool retains only its
command-line entry point and consumes the internal gate through its existing friend/project boundary; tests use
the same production implementation through their project reference. The final cross-project production `.cs`
source link is gone, completing the planned build-graph ownership cleanup. Engine, Godot, and corpus-gate
builds plus runtime validation remain green.
5. **Close repository-policy and storage gaps.** Add an explicit project license, third-party notices and
checksums/provenance for committed binaries, consistent editor/build policy, a pinned .NET SDK, and a
CI entry point built on the same validation driver. Remove the empty workspace-root `.git` directory and
@@ -841,6 +862,104 @@ do not mix mechanical moves with semantic changes.
only after active work is committed. Configure an off-machine remote or equivalent backup before relying
on local history as the sole recovery path.
**Read-only audit (2026-08-03):**
- **License/provenance:** the repository has no project `LICENSE`, `NOTICE`, third-party notice, or artifact
manifest. The three committed binaries are unsigned and have no dedicated artifact records; audit hashes are
`BinExtractALF.exe` 0.8
(`46167cdf3da1f02733ce1e7f0da99d155e934ede0987ca79c8927f48feb25fe2`), metadata-free `LzssCpp.dll`
(`596b9ddb07ca5c29cee609e846f1512654edec56310a90d6c2a6951be1e39fe1`), and PE-sieve 0.4.1.1
(`ee684b34b37af24d1c1e9ca80ceeee6878cc80bb24c0c3793840c9acf905bd59`). PE-sieve's upstream is
[BSD-2-Clause](https://github.com/hasherezade/pe-sieve/blob/master/LICENSE), but that license is not carried
here; the extractor/DLL provenance and redistribution terms remain unverified. The two committed Kelebek
source snapshots and transcribed opcode data come from the
[upstream repository](https://github.com/Kelebek1/Eushully-Decompiler), where no license file was found, so
their redistribution status also remains unresolved. FFmpeg is the good model: Windows/Linux manifests pin
provider, release, URL, version, variant, and SHA-256, while build/export scripts stage its LGPL license.
NuGet/Godot dependencies are versioned in project files but have no consolidated notice inventory.
- **Editor/build/CI policy:** there is no `.editorconfig`, `global.json`, `Directory.Build.*`, package lock,
Python environment manifest, or CI configuration. `.gitattributes` only fixes LF for shell scripts and
whitespace exceptions for generated references. Projects target `net8.0`; the Godot SDK is pinned to 4.7.0
and NuGet versions are explicit. The locally validated tools are .NET SDK 8.0.408 and Python 3.11.4, but only
the Python 3.11 invocation is documented. `tools/validate.py --level core` is already the asset-independent
CI entry point; workspace/runtime/full levels deliberately require the private game corpus and/or Godot.
- **Repository/storage:** `age-reimpl/` is the sole real worktree. The workspace parent contains an empty
`.git` directory, and the repository has an empty `.godot/logs` nest plus an accidental ignored
`godot/godot/build` nest (3.7 MB). All inspected ignored outputs total 6.43 GiB across 12,190 files: 4.38 GB
under `build/`, 1.29 GB under `godot/.godot`, 1.12 GB under historical `godot/build`, and the remainder in
.NET/tool caches. The real `.git` holds 6,100 loose objects (71 MiB), no packs and no reported garbage; defer
compaction until backup and cleanup decisions are complete.
- **Recovery:** `main` is the only ref and has no remote, upstream, additional worktree, or repository/parent
bundle. An unrelated off-machine backup cannot be disproved from repository state, but none is configured or
evidenced here. Do not publish, delete ignored data, remove the empty parent `.git`, or run Git compaction
until the user chooses the corresponding legal, retention, and backup policy.
**Bounded execution order:** (1) add tracked SDK/editor/line-ending policy without reformatting existing files;
(2) add an asset-independent CI workflow that invokes the existing core validation driver; (3) obtain the
user's project-license choice and resolve, replace, or exclude every unverified third-party artifact before
adding the project license/notice/manifest set; (4) configure a user-selected private/off-machine backup or
remote; (5) after explicit retention approval, remove only the verified empty/obsolete ignored nests and any
selected regenerable caches; (6) compact the real repository only after a clean committed state and verified
backup. Public distribution remains blocked on item 3; destructive cleanup and compaction remain blocked on
item 4 plus explicit target approval.
**Policy baseline (2026-08-03):** item 1 is complete. `global.json` pins the validated .NET 8.0.408 feature
band to stable patch releases; `.editorconfig` establishes UTF-8, final-newline, trailing-whitespace, and
indentation defaults; and `.gitattributes` classifies repository text, Windows command wrappers, and binary
assets. Generator-owned references that already commit CRLF are explicit byte-preserving exceptions with
textual diffs, so rebuilding them does not create whole-file normalization churn. No existing source or
project file was reformatted or renormalized. Both solution builds and the asset-independent core validation
gate remain green under the tracked policy. Item 2, a CI workflow calling that same core gate, is the next
safe slice.
**Linux CI preparation (2026-08-03):** the eight legacy text blobs that prevented a clean Linux checkout or
generated-reference comparison are now deliberately normalized to canonical LF: the engine solution, three
engine project files, three generated Markdown references, and the generated Himegari opcode module. The
generated files are once again ordinary text under the repository EOL policy; their existing whitespace
exceptions do not disable normalization. Regeneration through all three owning tools is clean, Windows core
validation remains green, and a Linux Git comparison reports no unstaged EOL drift. The first hosted CI job
can therefore target Linux rather than carrying forward a Windows-runner dependency.
**Initial Linux core workflow (2026-08-03, superseded below):** item 2 was first implemented as
`.github/workflows/core-validation.yml`. One read-only `ubuntu-24.04` job provisions Python 3.11 and the SDK selected by `global.json`, reports both resolved
versions, and invokes the existing asset-independent core driver. Pull requests, `main` pushes, and manual
dispatch share the job; branch-local concurrency cancels superseded runs, and seven-day validation-log upload
occurs only after failure. The workflow has no cache, secrets, private corpus, Godot runtime, packaging, or
deployment access. All four official actions are pinned to full immutable release SHAs. `actionlint` accepts
the workflow and the underlying Python 3.11 core gate remains green locally. Because no remote is configured,
the tracked workflow is dormant and its first hosted Linux execution remains a publication-time confirmation,
not a condition hidden by this commit.
**Private-repository preflight and binary purge (2026-08-03):** a clean source-only checkout disproved the
earlier asset-independent claim: after opcode generation, 501/590 engine cases pass while 89 installed-corpus
or native-oracle cases fail, and `test_opcodes.py` still scans the external script corpus. The target Gitea
server demonstrates a working `ubuntu-latest` runner with checkout/setup-dotnet v4 and Gitea-specific artifact
upload. CI must explicitly separate those workspace cases and inject a synthetic opcode fixture before Actions
is enabled; missing private data will not be converted into green skips.
The unverified `bin/BinExtractALF.exe`, `bin/LzssCpp.dll`, and obsolete `bin/pe-sieve32.exe` are removed from
tracking and purged from every reachable commit before the first remote is added. The rewritten tip is
`a4f029c`; `git fsck --full --strict` is clean, the three paths have no reachable object or path history, and
compaction leaves one 3.00 MiB pack with no loose or garbage objects. The complete pre-rewrite recovery bundle
is `../age-reimpl-pre-binary-purge-20260803.bundle` (8,824,649 bytes; SHA-256
`E157D7D2404DD2FC6E19FB4B65D08FB9784F415DAB1A2CFF6850121A5F305E5E`) and was verified again after the rewrite.
`bin/` now tracks policy only and ignores optional machine-local extractor files; PE-sieve remains documented
solely as a failed historical experiment. This resolves the committed-binary portion of item 3, but
Kelebek-derived source/data and the user-owned project license choice remain unresolved for wider distribution.
**Hermetic Gitea core workflow (2026-08-03):** the correction is complete. The 590 engine cases are now
explicitly partitioned into 502 source-only cases and 88 `Workspace` installed-data/native-oracle cases;
both selections pass with zero skips in the populated workspace. The one accidentally coupled synthetic
asset-store test now builds its catalog fixture instead of being excluded. `test_opcodes.py` injects 248
synthetic observations derived from the canonical observed-opcode set, while production `--bootstrap` still
scans the real corpus by default. Development/test repository discovery uses tracked marker files rather than
the checkout directory name.
The workflow moved to `.gitea/workflows/core-validation.yml` and matches the same-server baseline:
`ubuntu-latest`, checkout/setup-dotnet v4, setup-python v6, and Gitea-specific artifact upload. A standalone
source-only repository under an arbitrary name passes the complete core driver without sibling game/extracted
data. The first actual Linux runner result remains pending until the user adds and pushes the selected remote.
**Not cleanup targets:** generated `build/` output, the two intentional solution files, historical
`docs/superpowers/` plans/specifications, and fidelity-specific complexity that is directly covered by the
native ABI. Reorganization is successful when ownership and reproduction become clearer, not when the raw
@@ -1187,10 +1306,9 @@ layer's rendering diverges from ADV; save layout.
---
## 8. Immediate next step
Begin step 4 of the **codebase consolidation** maintenance slice: make the build graph express source ownership
without changing behavior. Move the platform-neutral movie abstractions and runtime support—decoder interfaces,
movie/audio runtime state, movie-surface registry, and RIFF sanitization—from `godot/` into
`Age.Engine.Frontend` next. Replace their test source links while preserving the intentionally internal API through
explicit friend-assembly access; leave FFmpeg interop/decoder ownership for the following slice.
Continue step 5 of the **codebase consolidation** maintenance slice by adding and pushing the user-selected
private Gitea remote, then confirm the first hosted Linux core result. After that, make the user-owned project-
license and Kelebek-derived-material decisions required before wider distribution. Do not infer a license choice
or configure/publish a remote without the user's explicit direction.
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
not replace Phase B gameplay validation or the open cross-platform gates.

View File

@@ -26,7 +26,7 @@ whenever a tool's inputs/outputs change.**
| Tool | Purpose | Run | Reads → Writes |
|---|---|---|---|
| `validate.py` | Layered project validation front door. `core` regenerates/runtime-checks opcode metadata, lints canonical registries, runs pure Python tooling tests and all .NET engine tests, checks generated opcode references, and runs `git diff --check`. `workspace` adds corpus-derived global generation, the real-data Python suites, full SYS4 decode, and Python RECOVER. `runtime` adds the Godot C# build and forced-portable threaded self-test. `full` combines all phases and adds the booted faithful-wait C# scene sweep. Selected prerequisites are strict: an unavailable game/corpus/Godot requirement fails before execution instead of becoming a green skip. Each gate has a timeout and UTF-8 log under `build/validation/validate-<timestamp>/`; Godot receives an isolated validation-owned user-data/log root there so it cannot read or modify the developer's saves/settings. The final table reports results/durations and a before/after Godot-process leak audit. | `validate.py` (defaults to `--level full`) · `--level core|workspace|runtime|full` · `--godot <console>` · `--game-root <install>` · `--verbose` · `--fail-fast` | sources + selected toolchain/game/corpus prerequisites → console summary + ⚙ `build/validation/validate-*/<gate>.log` |
| `validate.py` | Layered project validation front door. `core` regenerates/runtime-checks opcode metadata, lints canonical registries, runs pure Python tooling tests and the 502 .NET cases without the `Workspace` trait, checks generated opcode references, and runs `git diff --check`. `workspace` adds the 88 explicitly traited installed-data/native-oracle cases, corpus-derived global generation, real-data Python suites, full SYS4 decode, and Python RECOVER. `runtime` adds the Godot C# build and forced-portable threaded self-test. `full` combines all phases and adds the booted faithful-wait C# scene sweep. Selected prerequisites are strict: an unavailable game/corpus/Godot requirement fails before execution instead of becoming a green skip. Each gate has a timeout and UTF-8 log under `build/validation/validate-<timestamp>/`; Godot receives an isolated validation-owned user-data/log root there so it cannot read or modify the developer's saves/settings. The final table reports results/durations and a before/after Godot-process leak audit. | `validate.py` (defaults to `--level full`) · `--level core|workspace|runtime|full` · `--godot <console>` · `--game-root <install>` · `--verbose` · `--fail-fast` | sources + selected toolchain/game/corpus prerequisites → console summary + ⚙ `build/validation/validate-*/<gate>.log` |
| `test_validate.py` | Pure tests for launcher-equivalent explicit/environment/PATH/conventional resolution precedence, invalid-explicit hard failure, level composition, and final gate ordering. | `test_validate.py` | temporary files only |
Levels are cumulative around `core`: `workspace` means core+workspace-corpus, `runtime` means core+Godot,
@@ -35,6 +35,23 @@ by a failed preflight; rebuild each through its owning tool in this reference. R
`--godot`, then `AGE_GODOT_CONSOLE`, then `godot4`/`godot`/`godot-mono` on `PATH`. Game-root resolution uses
`--game-root`, then `AGE_GAME_ROOT`, then the conventional sibling install and always requires `SYS4INI.BIN`.
The hosted wrapper is `.gitea/workflows/core-validation.yml`. It matches the target server's demonstrated
`ubuntu-latest`, `actions/checkout@v4`, `actions/setup-dotnet@v4`, and
`christopherhx/gitea-upload-artifact@v4` baseline, adds Python 3.11, and runs only `--level core`. A standalone
source-only repository under an arbitrary directory name passes that exact driver without sibling game or
extracted data. Repository discovery uses tracked marker files rather than requiring the checkout directory to
be named `age-reimpl`. The workflow has no secrets, private corpus, Godot runtime, cache, packaging, or deployment
access; failure logs are retained for seven days. Its first actual Linux/Gitea execution remains pending until
the rewritten repository is pushed.
## Optional local binary tools
`bin/` is an ignored machine-local tool directory, not a source or artifact distribution directory. A local
`BinExtractALF.exe` plus its matching `LzssCpp.dll` may be used by the installed-data archive parity test and to
regenerate `../extracted/`; neither is a runtime dependency. Their known local hashes and unverified provenance
are recorded in `bin/README.md`. PE-sieve is obsolete and is not retained; its historical experiment remains in
`docs/engine-re.md`.
## Container parse / disassemble
| Tool | Purpose | Run | Reads → Writes |
@@ -51,7 +68,7 @@ All opcode knowledge (ABI, semantics, provenance, `depends_on`) is hand-edited *
|---|---|---|---|
| `opcodes_build.py` | Generator + linter for the opcode reference. `--bootstrap` appends entries observed in the Himegari corpus; `--bootstrap-age` appends every missing opcode from the pristine 548-entry AGE catalog as an unobserved compatibility stub. Catalog stubs retain Kelebek's ABI label/argument count so other AGE scripts decode past them, but remain `noop_headless=false`: the VM currently traces/skips them, while coverage continues to report them as unresolved rather than semantically safe. | `--build` · `--lint` · `--bootstrap` · `--bootstrap-age` | `vm-map/opcodes.toml` → ⚙ `tools/age_opcodes_himegari.py`, ⚙ `build/opcodes.json`, ⚙ `docs/opcode-reference.md`, ⚙ `build/opcode-coverage.md` |
| `opcodes_model.py` | In-memory model + loader + linter (dangling-ref / confidence-ceiling / vocabulary / dependents). | *Imported by `opcodes_build.py`.* | `vm-map/opcodes.toml` → — |
| `test_opcodes.py` | Unit tests for the opcode tooling. | `test_opcodes.py` | — |
| `test_opcodes.py` | Unit tests for the opcode tooling. Bootstrap coverage injects 248 synthetic observations derived from the canonical observed-opcode set; it does not read the private script corpus. Production `--bootstrap` still scans the real corpus by default. | `test_opcodes.py` | `vm-map/opcodes.toml` → temporary files only |
| `opcode_context.py` | Read-only evidence gatherer for classifying unnamed opcodes (frequency, argc, operand-type signature, neighbours, disasm snippets, Kelebek comment). | `--top 20` · `opcode_context.py 0x1f4 0x71 …` | corpus → stdout |
| `validate_opcode_table.py` | Definitive decode-coverage validator (replicates Kelebek's `data_array_end` code/data split). | `validate_opcode_table.py` | corpus → stdout |
| `validate_opcode_table_naive.py` | Naïve variant of the above (baseline comparison). | `validate_opcode_table_naive.py` | corpus → stdout |

View File

@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Age.Engine\Age.Engine.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Age.Engine\Age.Engine.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,5 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Age.Engine.Tests")]
[assembly: InternalsVisibleTo("Age.MovieCorpusGate")]
[assembly: InternalsVisibleTo("Himegari")]

View File

@@ -156,6 +156,7 @@ public class AdvTextHistoryTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealSc0000FirstPagePopulatesTheRetainedBacklogBeforeItsWait()
{
var scripts = Sys4ScriptProvider.Load(Table);

View File

@@ -259,6 +259,7 @@ public class AdvTextOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealMamesResearchDescriptionUsesImmediateRetainedTextPath()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -310,6 +311,7 @@ public class AdvTextOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void System4BootstrapReplaysAllResetCursorAndBoundsConfigurations()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -1,37 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Age.Engine\Age.Engine.csproj" />
<ProjectReference Include="..\Age.Engine.Frontend\Age.Engine.Frontend.csproj" />
<ProjectReference Include="..\Age.Engine.Text.Windows\Age.Engine.Text.Windows.csproj" />
<Compile Include="..\..\godot\IMovieDecoder.cs" Link="IMovieDecoder.cs" />
<Compile Include="..\..\godot\MovieRuntime.cs" Link="MovieRuntime.cs" />
<Compile Include="..\..\godot\MovieAudioTimeline.cs" Link="MovieAudioTimeline.cs" />
<Compile Include="..\..\godot\FfmpegMovieNative.cs" Link="FfmpegMovieNative.cs" />
<Compile Include="..\..\godot\FfmpegMovieDecoder.cs" Link="FfmpegMovieDecoder.cs" />
<Compile Include="..\..\godot\MovieSurfaceRegistry.cs" Link="MovieSurfaceRegistry.cs" />
<Compile Include="..\..\godot\RiffWaveSanitizer.cs" Link="RiffWaveSanitizer.cs" />
<Compile Include="..\..\tools\movie-corpus-gate\MovieCorpusGate.cs" Link="MovieCorpusGate.cs" />
</ItemGroup>
</Project>
</Project>

View File

@@ -71,6 +71,7 @@ public class AgfDecoderTests
}
[Fact]
[Trait("Category", "Workspace")]
public void InstalledSo001HasExpectedAlphaBearingDimensions()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -87,6 +88,7 @@ public class AgfDecoderTests
[InlineData(0x32db, "SO007.AGF")]
[InlineData(0x32dc, "SO008A.AGF")]
[InlineData(0x32dd, "SO007A.AGF")]
[Trait("Category", "Workspace")]
public void InstalledFieldMapSheetsResolveAndDecodeByRawCatalogIndex(int rawId, string name)
{
var resources = ResourceMap.Load();

View File

@@ -160,6 +160,7 @@ public class AudioMixerOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void NativePersistencePathsComeFromIndependentSys4IniProfileValues()
{
Sys4AssetCatalog catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -177,6 +178,7 @@ public class AudioMixerOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void ProfileOverrideRedirectsSaveAndSettingsAsOneNativeLayout()
{
Sys4AssetCatalog catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);

View File

@@ -3,6 +3,7 @@ using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
[Trait("Category", "Workspace")]
public class CallScriptIntegrationTests
{
private sealed class NullHost : IHost

View File

@@ -97,6 +97,7 @@ public class CoroutineHostModelTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000EntryRunsSetupAndFillsDistinctTextureSlots()
{
var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], Table);

View File

@@ -6,6 +6,7 @@ using Xunit;
public class CurDecoderTests
{
[Fact]
[Trait("Category", "Workspace")]
public void HimegariCursor_DecodesPixelsAndHotspot()
{
var resources = ResourceMap.Load();
@@ -24,6 +25,7 @@ public class CurDecoderTests
}
[Fact]
[Trait("Category", "Workspace")]
public void HimegariPanCursor_DecodesFourBitPixelsAndHotspot()
{
var resources = ResourceMap.Load();

View File

@@ -22,6 +22,7 @@ public class FrameYieldTests
}
[Fact]
[Trait("Category", "Workspace")]
public void FrameYield_CalledOncePerStep()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -61,6 +61,7 @@ public class GameSessionTests
}
[Fact]
[Trait("Category", "Workspace")]
public void SC0000ViaSessionMatchesSingleRun()
{
var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], Table);
@@ -77,6 +78,7 @@ public class GameSessionTests
}
[Fact]
[Trait("Category", "Workspace")]
public void BootingSkinitPopulatesDataTableGlobals()
{
// Running the SKINIT data script through the session populates the real skill table into the
@@ -103,6 +105,7 @@ public class GameSessionTests
}
[Fact]
[Trait("Category", "Workspace")]
public void BootedStateSurvivesSnapshot()
{
var s = new GameSession();
@@ -115,6 +118,7 @@ public class GameSessionTests
}
[Fact]
[Trait("Category", "Workspace")]
public void SeedingFormFlagChangesBehavior()
{
// Lily's lines are gated on form flags G[0xa57/8/9]; unseeded => all skipped (0 voices on her lines).

View File

@@ -62,6 +62,7 @@ public class HistoryDataOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void LoaderRetainsRealHistoryFooterArraysForRuntimeCopy()
{
var history = Sys4Loader.Load(Paths.Scripts()["HISTORY.BIN"], Table);

View File

@@ -179,6 +179,7 @@ public class HistoryInteractionOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealHistoryWheelUpNavigatesToOlderRetainedRows()
{
var scripts = Sys4ScriptProvider.Load(Table);
@@ -196,6 +197,7 @@ public class HistoryInteractionOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealHistoryRendersMultipleRowsAfterSeveralSc0000Messages()
{
var scripts = Sys4ScriptProvider.Load(Table);
@@ -282,6 +284,7 @@ public class HistoryInteractionOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealSc0000HistoryButtonRendersAndClosesWithoutAdvancingThePageWait()
{
var scripts = Sys4ScriptProvider.Load(Table);
@@ -323,6 +326,7 @@ public class HistoryInteractionOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealHistoryVoicedRowDispatchesItsRetainedVoicePair()
{
var scripts = Sys4ScriptProvider.Load(Table);

View File

@@ -109,6 +109,7 @@ public class HistoryPresentationOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealHistoryScriptBuildsVisibleRowsFromARealSc0000Page()
{
var scripts = Sys4ScriptProvider.Load(Table);

View File

@@ -255,6 +255,7 @@ public class HotspotInputTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000AdvChromeBootstrap_VisitsAllFiveVisibleButtonRegistrations()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -274,6 +275,7 @@ public class HotspotInputTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000FirstWait_DispatchesRealHistoryHoverCallback()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -436,6 +438,7 @@ public class HotspotInputTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000MessageSkipButton_EnablesPersistentServiceState()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -471,6 +474,7 @@ public class HotspotInputTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000HideWindowButton_RunsRealHidewinAndReturnsToAdvWait()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -497,6 +501,7 @@ public class HotspotInputTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000ActionSeven_EntersRealMenuAndRestoresParentAdvHotspots()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -48,6 +48,7 @@ public class InputBindingTests
}
[Fact]
[Trait("Category", "Workspace")]
public void System4BootstrapReplaysAllSixteenInputConfigurationCalls()
{
var scripts = Sys4ScriptProvider.Load(Table);
@@ -72,6 +73,7 @@ public class InputBindingTests
[InlineData(0x08)] // retained native Backspace default
[InlineData(0x11)] // SYSTEM4 LeftCtrl binding
[InlineData(0x43)] // SYSTEM4 C binding
[Trait("Category", "Workspace")]
public void LogicalActionSixDrivesHeldAdvFastForward(int virtualKey)
{
var script = ScriptAssembler.Assemble(Table, "ADV_FAST_FORWARD", new List<(int, Operand[])>
@@ -99,6 +101,7 @@ public class InputBindingTests
}
[Fact]
[Trait("Category", "Workspace")]
public void HeldLogicalActionSixSurvivesPersistentSkipLifecycleSuspension()
{
var script = ScriptAssembler.Assemble(Table, "HELD_SKIP_LIFECYCLE", new List<(int, Operand[])>
@@ -122,6 +125,7 @@ public class InputBindingTests
}
[Fact]
[Trait("Category", "Workspace")]
public void ReleasingPhysicalFastForwardDoesNotClearPersistentSkip()
{
var script = ScriptAssembler.Assemble(Table, "ADV_PERSISTENT_AND_HELD_SKIP", new List<(int, Operand[])>

View File

@@ -87,6 +87,7 @@ public class IntegerQueueOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RegisteredRealMvseek_ExpandsMovementCostsThroughTheSystem4ServiceAbi()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -111,6 +112,7 @@ public class IntegerQueueOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealMvseek_ExpandsMovementCostsBeyondTheOrigin()
{
var vm = RealSearchVm("MVSEEK.BIN");
@@ -126,6 +128,7 @@ public class IntegerQueueOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RealAtseek_ExpandsAttackDistancesBeyondTheOrigin()
{
var vm = RealSearchVm("ATSEEK.BIN");

View File

@@ -554,6 +554,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000ResumesImmediatelyAfterMovieOpcodeAtBytecodeOffset13d1()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -756,6 +757,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000MoviePayloadReadsFromArchiveVfsAndIsMpegProgramStream()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -771,6 +773,7 @@ public class MovieOpcodeTests
[Theory]
[InlineData(0x335f, "LOGO.AGF")]
[InlineData(0x3364, "OP.AGF")]
[Trait("Category", "Workspace")]
public void ModalMoviePayloadResolvesFromUniversalPackedCatalog(int resourceId, string expectedName)
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -783,6 +786,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void DebugTestMovieDecodesToExactGreenMaskDimensions()
{
if (!OperatingSystem.IsWindows()) return;
@@ -807,6 +811,7 @@ public class MovieOpcodeTests
[Theory]
[InlineData(0x335f, "LOGO.AGF")]
[InlineData(0x3364, "OP.AGF")]
[Trait("Category", "Workspace")]
public void ModalMovieOpeningFramesAreOpaqueAndHaveContinuousCadence(
int resourceId, string expectedName)
{
@@ -841,6 +846,7 @@ public class MovieOpcodeTests
[InlineData(0x2b94, "MVB238.AGF", 280, 352, 866)]
[InlineData(0x2bc2, "MVB908.AGF", 400, 400, 333)]
[InlineData(0x33, "CHAPTER.AGF", 800, 600, 12016)]
[Trait("Category", "Workspace")]
public void FfmpegShimDecodesRepresentativeVfsMovie(int resourceId, string expectedName,
int expectedWidth, int expectedHeight,
long expectedStopTimeMs)
@@ -879,6 +885,7 @@ public class MovieOpcodeTests
[InlineData(0x33, "CHAPTER.AGF")]
[InlineData(0x2bf1, "MVS001.AGF")]
[InlineData(0x335f, "LOGO.AGF")]
[Trait("Category", "Workspace")]
public void FfmpegShimDecodesRepresentativeMpegAudio(int resourceId, string expectedName)
{
if (!OperatingSystem.IsWindows()) return;
@@ -912,6 +919,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void FfmpegShimSeeksBothVideoAndAudioNearRequestedPosition()
{
if (!OperatingSystem.IsWindows()) return;
@@ -943,6 +951,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void FfmpegShimRejectsTruncatedMovieWithBoundedDiagnostic()
{
if (!OperatingSystem.IsWindows()) return;
@@ -956,6 +965,7 @@ public class MovieOpcodeTests
}
[Fact]
[Trait("Category", "Workspace")]
public void FfmpegShimSupportsRepeatedOpenAndClose()
{
if (!OperatingSystem.IsWindows()) return;
@@ -974,6 +984,7 @@ public class MovieOpcodeTests
[Theory]
[InlineData(0x2be3, 280, 500, 450)]
[InlineData(0x2bc2, 400, 333, 300)]
[Trait("Category", "Workspace")]
public void FfmpegPacedDecoderKeepsRealMovieAliveThroughItsStopTime(
int resourceId, int expectedWidth, long expectedStopTimeMs, long minimumElapsedMs)
{

View File

@@ -4,6 +4,7 @@ using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
[Trait("Category", "Workspace")]
public class NaturalBootIntegrationTests
{
private sealed class ReachedSc0000Exception : Exception { }

View File

@@ -64,6 +64,7 @@ public class ReadTextDatabaseTests
}
[Fact]
[Trait("Category", "Workspace")]
public void InstalledNativeRtDatRoundTripsItsLogicalRecordsWhenPresent()
{
string path = Path.Combine(

View File

@@ -9,6 +9,7 @@ public class RecoverTests
private static long G(VirtualMachine vm, int k) => vm.Globals.TryGetValue(k, out var v) ? v : 0;
[Fact]
[Trait("Category", "Workspace")]
public void RecoverUnitTestPasses()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -48,6 +48,7 @@ public class RiffWaveSanitizerTests
}
[Fact]
[Trait("Category", "Workspace")]
public void InstalledSc0000GlowSoundDropsCp932InfoBlockWithoutChangingPcmData()
{
var resources = ResourceMap.Load();
@@ -63,6 +64,7 @@ public class RiffWaveSanitizerTests
}
[Fact]
[Trait("Category", "Workspace")]
public void InstalledFirstBossSoundStopsAtFirstDeclaredRiff()
{
var resources = ResourceMap.Load();

View File

@@ -85,6 +85,7 @@ public class RootReloadTests
}
[Fact]
[Trait("Category", "Workspace")]
public void HimegariRawScriptZeroIsSystem4()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -5,6 +5,7 @@ using Age.Engine.Persistence;
using Age.Engine.Sys4;
using Age.Engine.Vm;
[Trait("Category", "Workspace")]
public class SaveUiIntegrationTests
{
private sealed class MenuReadyException : Exception;

View File

@@ -265,6 +265,7 @@ public class SharedProfileTests
}
[Fact]
[Trait("Category", "Workspace")]
public void InstalledSharedProfileUnlocksKnownCgAndHSceneResourcesWhenPresent()
{
string root = Path.Combine(

View File

@@ -8,6 +8,7 @@ using Xunit;
public class Sys4AssetStoreTests
{
[Fact]
[Trait("Category", "Workspace")]
public void InstalledAppendCatalogHasNativePackSelectionAndStableDirectory()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -40,6 +41,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void CompletePackedAssetEnumerationPreservesBaseThenAppendOrder()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -57,6 +59,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void CompleteAppendDirectoryAndPayloadsMatchBinExtractAlf()
{
string temp = Path.Combine(Path.GetTempPath(), "age-vfsb-oracle-" + Guid.NewGuid().ToString("N"));
@@ -105,6 +108,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void RuntimeCatalogMatchesDiagnosticCatalogAndSceneViews()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -154,6 +158,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void EveryCatalogRangeFitsAndRepresentativePayloadsMatchExtractedData()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -186,6 +191,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0000RoomAndTitleAudioPayloadsReadDirectlyFromTheirNativeAddressSpaces()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -222,6 +228,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void Sc0010LowOperandsAreAlreadyUniversalPackedIds()
{
var resources = ResourceMap.Load();
@@ -236,6 +243,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void TypedResourceResolutionPreservesAppendPackSelector()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -249,6 +257,7 @@ public class Sys4AssetStoreTests
}
[Fact]
[Trait("Category", "Workspace")]
public void AllInstalledLooseScriptOverridesShadowArchiveCopies()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -281,7 +290,7 @@ public class Sys4AssetStoreTests
try
{
File.WriteAllBytes(Path.Combine(archives, "DATA1.ALF"), new byte[] { 9, 8, 1, 2, 3, 7 });
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
var catalog = Sys4AssetCatalog.Parse(Sys4StartupSettingsTests.BuildCatalog(includeTrailer: true));
var entry = new AssetEntry("TEST.BIN", "DATA1.ALF", 2, 3);
var store = new Sys4AssetStore(catalog, archives, loose);

View File

@@ -4,6 +4,7 @@ using Xunit;
public class Sys4LoaderTests
{
[Fact]
[Trait("Category", "Workspace")]
public void ParsesMenuBinLikeSys4load()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -2,6 +2,7 @@ using Age.Engine.Sys4;
using Age.Engine.Diagnostics;
using Xunit;
[Trait("Category", "Workspace")]
public class Sys4ScriptProviderTests
{
[Fact]

View File

@@ -5,6 +5,7 @@ using Age.Engine.Sys4;
public class Sys4StartupSettingsTests
{
[Fact]
[Trait("Category", "Workspace")]
public void InstalledCatalogExposesOrderedStartupSettingsAndLogicalCanvas()
{
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
@@ -100,7 +101,7 @@ public class Sys4StartupSettingsTests
params (string Key, string Value)[] pairs)
=> WrapCatalog(BuildExpanded(pairs));
private static byte[] BuildCatalog(bool includeTrailer)
internal static byte[] BuildCatalog(bool includeTrailer)
=> WrapCatalog(BuildExpanded(Array.Empty<(string, string)>(), includeTrailer));
private static byte[] BuildExpanded(

View File

@@ -24,6 +24,7 @@ public class TextureOpsTests
}
[Fact]
[Trait("Category", "Workspace")]
public void SC0000FiresTextureOpsWithAssignedFullScreenSlot()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);

View File

@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.9" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.9" />
</ItemGroup>
</Project>

View File

@@ -19,10 +19,16 @@ public static class Paths
private static string FindRepo()
{
var d = new DirectoryInfo(AppContext.BaseDirectory);
while (d != null && d.Name != "age-reimpl") d = d.Parent;
if (d == null) throw new DirectoryNotFoundException(
"age-reimpl root not found above " + AppContext.BaseDirectory);
return d.FullName;
while (d != null)
{
if (File.Exists(Path.Combine(d.FullName, "global.json"))
&& File.Exists(Path.Combine(d.FullName, "engine", "AgeEngine.sln"))
&& File.Exists(Path.Combine(d.FullName, "vm-map", "opcodes.toml")))
return d.FullName;
d = d.Parent;
}
throw new DirectoryNotFoundException(
"repository root not found above " + AppContext.BaseDirectory);
}
/// name(UPPER).BIN -> path; game-dir loose overrides shadow extracted/DATA1 (mirrors paths.scripts()).

View File

@@ -1,46 +1,46 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Engine", "Age.Engine\Age.Engine.csproj", "{DD04FB21-F83D-4547-BBEC-97295F30623A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Engine.Frontend", "Age.Engine.Frontend\Age.Engine.Frontend.csproj", "{2B018D3E-4057-465E-89CB-4CC77FDD60E2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Cli", "Age.Cli\Age.Cli.csproj", "{795573A0-A283-4A28-B26C-606C1AA5E1FE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Engine.Tests", "Age.Engine.Tests\Age.Engine.Tests.csproj", "{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}"
EndProject
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Engine.Tests", "Age.Engine.Tests\Age.Engine.Tests.csproj", "{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Age.Engine.Text.Windows", "Age.Engine.Text.Windows\Age.Engine.Text.Windows.csproj", "{74F3B4B8-5F8D-45F4-AA6B-7C2C1A54CB90}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DD04FB21-F83D-4547-BBEC-97295F30623A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD04FB21-F83D-4547-BBEC-97295F30623A}.Debug|Any CPU.Build.0 = Debug|Any CPU
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DD04FB21-F83D-4547-BBEC-97295F30623A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD04FB21-F83D-4547-BBEC-97295F30623A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD04FB21-F83D-4547-BBEC-97295F30623A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD04FB21-F83D-4547-BBEC-97295F30623A}.Release|Any CPU.Build.0 = Release|Any CPU
{2B018D3E-4057-465E-89CB-4CC77FDD60E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B018D3E-4057-465E-89CB-4CC77FDD60E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B018D3E-4057-465E-89CB-4CC77FDD60E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B018D3E-4057-465E-89CB-4CC77FDD60E2}.Release|Any CPU.Build.0 = Release|Any CPU
{795573A0-A283-4A28-B26C-606C1AA5E1FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{795573A0-A283-4A28-B26C-606C1AA5E1FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{795573A0-A283-4A28-B26C-606C1AA5E1FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{795573A0-A283-4A28-B26C-606C1AA5E1FE}.Release|Any CPU.Build.0 = Release|Any CPU
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Release|Any CPU.Build.0 = Release|Any CPU
{795573A0-A283-4A28-B26C-606C1AA5E1FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{795573A0-A283-4A28-B26C-606C1AA5E1FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{795573A0-A283-4A28-B26C-606C1AA5E1FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{795573A0-A283-4A28-B26C-606C1AA5E1FE}.Release|Any CPU.Build.0 = Release|Any CPU
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2ED171C8-DA87-40FD-A6FB-3B3F6501029F}.Release|Any CPU.Build.0 = Release|Any CPU
{74F3B4B8-5F8D-45F4-AA6B-7C2C1A54CB90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{74F3B4B8-5F8D-45F4-AA6B-7C2C1A54CB90}.Debug|Any CPU.Build.0 = Debug|Any CPU
{74F3B4B8-5F8D-45F4-AA6B-7C2C1A54CB90}.Release|Any CPU.ActiveCfg = Release|Any CPU
{74F3B4B8-5F8D-45F4-AA6B-7C2C1A54CB90}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
EndGlobalSection
EndGlobal

7
global.json Normal file
View File

@@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.408",
"rollForward": "latestPatch",
"allowPrerelease": false
}
}

View File

@@ -1,89 +1,89 @@
# DO NOT EDIT -- generated from vm-map/opcodes.toml by tools/opcodes_build.py --build
"""Inferred Himegari opcode semantics (generated). sys4load reads INFERRED[op]['name']."""
from __future__ import annotations
INFERRED: dict[int, dict] = {
0x21: dict(name='fade-surface-in-from-black', category='draw', noop=False, confidence='high', source='investigation', summary='(surface_slot)(timing_argument) — block while fading from black to a captured full-frame surface. The captured surface remains the terminal frame.'),
0x22: dict(name='fade-surface-out-to-black', category='draw', noop=False, confidence='high', source='investigation', summary='(surface_slot)(timing_argument) — block while fading a captured full-frame surface to black. Black remains the terminal frame.'),
0x23: dict(name='fade-surface-in-from-white', category='draw', noop=False, confidence='high', source='investigation', summary='(surface_slot)(timing_argument) — broader-AGE mode-2 sibling of 0x21: block while fading from white to a captured full-frame surface.'),
0x24: dict(name='fade-surface-out-to-white', category='draw', noop=False, confidence='high', source='investigation', summary='(surface_slot)(timing_argument) — broader-AGE mode-3 sibling of 0x22: block while fading a captured full-frame surface to white.'),
0x25: dict(name='crossfade-surfaces', category='draw', noop=False, confidence='high', source='investigation', summary='(source_surface)(target_surface)(interval_argument) — blocking legacy full-frame mode-4 alpha transition. The native engine composites the captured target surface over the captured source while an 8-bit alpha accumulator advances from 0 to 256.'),
0x60: dict(name='random-modulo', category='compute', noop=False, confidence='high', source='investigation', summary="(destination)(bound) — write CRT rand() % bound. A bound of zero first writes zero and then raises the engine's script error."),
0x6c: dict(name='zero-int-range', category='compute', noop=False, confidence='high', source='investigation', summary='(destination)(count) — write logical integer zero to count consecutive VM cells beginning at the resolved destination.'),
0x7b: dict(name='coroutine-save-yield-handlers', category='control', noop=False, confidence='high', source='investigation', summary='(handler1_pc)(handler2_pc) — scene-coroutine: save the two per-frame yield/resume handler PCs. Native writes op1→ctx[0x6da88+idx*4], op2→ctx[0x6db28+idx*4] (idx=ctx[0x53d14] script-context index); its generic handler prologue records the 5-dword instruction length. SC0000 0x79: `0x7b label_3c9 label_41e` registers the ADV per-frame render→poll→yield handlers. Part of the scene-coroutine framework (see engine-re.md §Scene-coroutine framework); pairs with 0x7c (resume) + 0x140 (loop iterator).'),
0x7c: dict(name='coroutine-resume', category='control', noop=False, confidence='high', source='investigation', summary='() — scene-coroutine RESUME point. Native requires run-state bit 0x2000000 (ctx[0x6dbc8]) set — THROWS (__CxxThrowException) if unset, so it is only ever reached on a scheduler-driven re-entry, NEVER on a cold first pass (cold flow jmps over it). Restores PC=ctx[0x53d28]+ctx[0x6dbcc]*4, clears the run-bit (ctx+0xa0ce4 &= ~0x2000000), resets input/line state. SC0000 0x443 (falls into the main loop label_444). See engine-re.md §Scene-coroutine framework.'),
0x7f: dict(name='get-message-glyph-delay', category='adv', noop=False, confidence='high', source='investigation', summary='(out) - read the current per-glyph ADV text reveal delay in milliseconds.'),
0x80: dict(name='set-default-gfx-object-slot', category='draw', noop=False, confidence='high', source='investigation', summary='(slot) - select the retained graphics-object slot used when op 0x1d9 receives explicit slot zero.'),
0x86: dict(name='set-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='(resource_id) - load an indexed cursor asset and install it as the active custom cursor.'),
0x87: dict(name='clear-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='Clear the active custom cursor and refresh the OS cursor when the game window is active.'),
0x88: dict(name='set-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set persistent all-message Skip state. Nonzero makes the interpreter inject ADV fast-forward input every tick; zero stops injection and clears the transient skip run-state bit.'),
0x8b: dict(name='set-text-line-spacing', category='adv', noop=False, confidence='high', source='investigation', summary='(pixels) - set the extra pixel spacing added between text lines.'),
0x90: dict(name='register-hotspot-callbacks', category='input', noop=True, confidence='high', source='investigation', summary="(x)(y)(w)(h)(on_enter_pc)(on_leave_pc)(on_activate_pc) - append an inclusive cursor rectangle and three callback PCs to the current script frame's native input registry. This opcode only registers; the ADV input service dispatches callbacks later."),
0x93: dict(name='cancel-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary="Reset the current frame's hotspot registry/input wait and clear native run-state bit 0x00800000. Used before opening History, Menu, or HIDEWIN flows."),
0x94: dict(name='arm-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary='Arm native hotspot input processing after the script has registered its rectangles.'),
0x97: dict(name='bind-hotspot-key', category='input', noop=True, confidence='high', source='investigation', summary='(x)(y)(w)(h)(logical_action) - find the already-registered rectangle with identical bounds and bind a logical input action to its activation callback.'),
0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(packed_raw_resource_id)(channel) — synchronously open the universal SYS4INI/AAI catalog entry and replace the channel's decoded sound buffer without starting playback. A zero high byte is a raw SYS4INI index; a nonzero high byte selects an append catalog and uses the low 24-bit index. Native manager supports channels 0..12; SC0000 uses 0..9."),
0xb5: dict(name='sfx-start', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) — start the already-loaded channel once (logical loop=false). DirectSound publishes synchronously through Play(0,0,DSBPLAY_LOOPING); the low-level flag loops only the streaming ring, while decoder EOF stops logical playback.'),
0xb6: dict(name='sfx-release', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) - stop/destroy the channel decoder and DirectSound buffer, clear its retained resource id, and leave the slot empty. Idempotent for an unused channel.'),
0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', summary='(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.'),
0xcc: dict(name='register-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='(poll_interval_ms)(target_pc) - register a timed per-frame mouse callback in the current script.'),
0xcd: dict(name='dispatch-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch the registered mouse callback when its polling interval elapses.'),
0xd3: dict(name='begin-timed-callback-sequence', category='control', noop=False, confidence='high', source='investigation', summary="Clear and initialize the current script frame's timed local-callback sequence."),
0xd4: dict(name='append-relative-timed-callbacks', category='control', noop=False, confidence='high', source='investigation', summary="Append repeated relative deadlines and their on-time/catch-up local callback targets to the current frame's timed sequence."),
0xd5: dict(name='run-timed-callback-sequence', category='control', noop=False, confidence='high', source='investigation', summary='Start or service the frame-local timed sequence, dispatching scheduled local callbacks through the penultimate entry and selecting the catch-up target when the following deadline is already late.'),
0xd9: dict(name='clear-run-state-0x1000', category='control', noop=True, confidence='high', source='investigation', summary='Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.'),
0xfb: dict(name='register-joy-callback', category='input', noop=False, confidence='high', source='investigation', summary='(input_index)(target_pc) - register one of 32 per-frame joy/input callback targets.'),
0xfe: dict(name='set-input-action-count', category='input', noop=False, confidence='high', source='investigation', summary='(count) - set the logical action scan bound; op 0x100 uses callback slot count itself when the polled mask is empty.'),
0xff: dict(name='poll-joy-callback-input', category='input', noop=False, confidence='high', source='investigation', summary='Poll the configured keyboard/mouse/joystick action mask and initialize the op 0x100 dispatch scan.'),
0x100: dict(name='dispatch-joy-callbacks', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch each set logical action below the configured count, or callback slot count when no action is held.'),
0x101: dict(name='reset-message-skip-input', category='input', noop=False, confidence='med', source='investigation', summary="Reset transient message-skip/input service state after an ADV chrome action without clearing op 0x88's persistent all-message Skip flag."),
0x107: dict(name='map-joystick-button', category='input', noop=False, confidence='high', source='investigation', summary='(button_slot)(physical_button) - map a logical button slot to a physical joystick button; slot N emits logical action N+4.'),
0x108: dict(name='get-mouse-button-state', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current mouse-button state bitmask.'),
0x109: dict(name='get-cursor-virtual', category='input', noop=False, confidence='high', source='investigation', summary="(out_x)(out_y) - read the OS cursor and convert it into AGE's virtual-screen coordinates."),
0x10a: dict(name='set-cursor-virtual', category='input', noop=False, confidence='high', source='investigation', summary='(x)(y) - convert AGE virtual-screen coordinates to client/screen coordinates and move the OS cursor.'),
0x10b: dict(name='map-mouse-button', category='input', noop=False, confidence='high', source='investigation', summary='(button_slot)(physical_button) - map a physical mouse button to a logical button slot; polling emits the slot as logical action slot+4.'),
0x10c: dict(name='map-keyboard-scancode', category='input', noop=False, confidence='high', source='investigation', summary='(action)(dik_scan_code) - map a DirectInput keyboard scan code to a logical input action.'),
0x10d: dict(name='consume-mouse-wheel-delta', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the accumulated signed mouse-wheel delta and clear it.'),
0x13a: dict(name='register-numeric-glyph-style', category='draw', noop=False, confidence='high', source='investigation', summary='Register a decimal-glyph atlas style as (surface slot, source x/y, digit width/height); the handler admits style indices 0..10.'),
0x140: dict(name='coroutine-label-yield', category='control', noop=False, confidence='med', source='investigation', summary="(out)(name_str)(sub_str)(in) — scene-coroutine LOOP ITERATOR / labeled yield. Handler copies name/sub strings + the int operand and calls AGERC host command 8 through g_pfnAgercHostCallback(8, ctx[0x54fe8], &{name,sub,in}); writes the returned PC-like value to operand 1. In SC0000 label_462 'ループ開始' (@0x46d): `out=G[0x6be]=LABEL('J',G[0x6be])`; loop runs the intro-setup body (incl. call label_125bd = slot-table fill G[0x3239..0x324e]=4..11) and jmps back until out==G[0x6c3] (a per-scene exit-PC immediate) → mov aba5c 0 → content. The gate G[0xaba5c]==1 that opens this loop is NATIVE scene-entry state (no script sets it to 1). PORT = HOST-MODEL IMPLEMENTED: synthesize the ADV scene-entry gate, run the LABEL/J setup body once, then return the structurally discovered per-scene terminal; do not emulate the service dialog. See engine-re.md §Scene-coroutine framework."),
0x143: dict(name='run-mounted-append-autoruns', category='control', noop=False, confidence='high', source='investigation', summary='() - enqueue record zero from every mounted nonzero AAI selector in ascending selector order, then execute those packed scripts serially before resuming the caller.'),
0x199: dict(name='yield-adv-coroutine', category='control', noop=False, confidence='high', source='investigation', summary='Yield/re-enter the registered ADV coroutine handler. The fifth standard chrome button uses this transition to enter the HIDEWIN/window-hidden flow.'),
0x19a: dict(name='get-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current all-message skip state set by op 0x88.'),
0x19b: dict(name='suspend-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary="() - suspend persistent/read-driven ADV fast-forward while preserving the user's all-message Skip toggle; physically held logical action 6 is independent."),
0x19c: dict(name='resume-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary='() - enable the persistent/read ADV skip service and recompute active fast-forward from all-message Skip or the live read-skip channel; physical action 6 remains independent.'),
0x1a6: dict(name='half-byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write half the resolved string's byte length, using integer truncation."),
0x1a8: dict(name='instruction-marker-noop', category='marker', noop=True, confidence='high', source='investigation', summary="Zero-operand structural marker. The native shared 0xaf/0x1a8 handler only records this instruction's one-dword length and returns."),
0x1b2: dict(name='append-diagnostic-value', category='control', noop=False, confidence='high', source='investigation', summary="Format operand 1 with AGE's generic operand-to-string conversion and append its exact bytes to the context diagnostic accumulator. This does not present or clear the accumulated text."),
0x1b3: dict(name='append-diagnostic-newline', category='control', noop=False, confidence='high', source='investigation', summary='Append the literal CRLF byte pair to the context diagnostic accumulator without presenting or clearing it.'),
0x1b4: dict(name='show-and-clear-diagnostic', category='control', noop=False, confidence='high', source='investigation', summary='Show the accumulated text plus native script-location context in a synchronous owner-window information message box, then erase the complete accumulator.'),
0x1b5: dict(name='set-message-glyph-delay', category='adv', noop=False, confidence='high', source='investigation', summary='(milliseconds) - set the per-glyph ADV text reveal delay; zero makes retained UI text publish immediately.'),
0x1b6: dict(name='get-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return whether automatic message advance is enabled.'),
0x1b7: dict(name='set-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - enable or disable automatic message advance.'),
0x1b8: dict(name='get-auto-message-time', category='input', noop=False, confidence='high', source='investigation', summary='(selector)(out) - read an Auto-message delay from engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.'),
0x1b9: dict(name='set-auto-message-time', category='input', noop=False, confidence='high', source='investigation', summary='(selector)(milliseconds) - write an Auto-message delay to engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.'),
0x1bc: dict(name='reset-message-voice-state', category='input', noop=False, confidence='high', source='investigation', summary='Reset the per-message queued-voice flag used by ADV Auto timing.'),
0x1bd: dict(name='play-history-voice', category='audio', noop=False, confidence='high', source='investigation', summary='Replay a voice id selected from retained ADV text history using native playback/history variant 1, preserving normal Skip and Auto-voice state behavior.'),
0x1bf: dict(name='call-end', category='marker', noop=True, confidence='med', source='inference', summary='zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker'),
0x1ca: dict(name='set-read-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set the engine setting `message:ReadTextSkip`, which skips only previously read text.'),
0x1cb: dict(name='get-read-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - read the engine setting `message:ReadTextSkip`.'),
0x1ce: dict(name='set-adv-wait-indicator-enabled', category='adv', noop=False, confidence='high', source='investigation', summary='(enabled) - explicitly start or stop the animated ADV input-wait indicator service.'),
0x1d1: dict(name='render-text-history', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot)(record_index)(flags)(color_a)(color_b) - render retained ADV text records into a selected text layout/surface.'),
0x1d5: dict(name='cond-block', category='marker', noop=True, confidence='high', source='inference', summary='zero-arg; ALWAYS follows jcc — marks conditional body entry'),
0x1f4: dict(name='stmt-begin', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; opens scripts, pairs with stmt-end 0x1f5'),
0x1f5: dict(name='stmt-end', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; precedes exit/next-stmt, pairs with 0x1f4'),
0x1fd: dict(name='gfx-set-scale-current', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(scale_x_percent)(scale_y_percent)(scale_z_percent) — immediately replace the retained object's current scale matrix at obj+0x6c. The handler divides each integer operand by 100.0 before calling matrix4_make_scale; this is distinct from 0x21e's delayed one-shot target scale."),
0x1ff: dict(name='set-current-translation', category='draw', noop=False, confidence='high', source='investigation', summary="0x1ff (handle)(x)(y)(z) — immediately replace the retained object's current translation matrix at obj+0x16c. This is the direct-current companion to 0x220's delayed target at obj+0x1ac."),
0x205: dict(name='draw-formatted-integer', category='draw', noop=False, confidence='high', source='investigation', summary='Format an integer into a fixed-width decimal field and rasterize it immediately into a numbered graphics surface using the current font/color/effect state.'),
0x20a: dict(name='publish-adv-text-layout', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame.'),
0x21b: dict(name='line-id?', category='marker', noop=True, confidence='med', source='harness', summary='1 imm; mov->0x21b->stmt-end; near save/load-messkip — likely line/stmt id, verify not msg-control'),
0x228: dict(name='query-gfx-translation-target', category='draw', noop=False, confidence='high', source='investigation', summary='0x228 query translation target (succ)(handle)(outX)(outY)(outZ): clone the retained object, decompose its target translation matrix at obj+0x17c, and return matrix translation obj+0x1ac/+0x1b0/+0x1b4. Returns succ=0 when found; when absent, writes succ=1 and leaves outputs untouched. The C# VM queries TranslationTarget independently of V24. See docs/engine-re.md §SC0000 anim cluster.'),
0x22f: dict(name='set-gfx-position-current', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(control)(x)(y)(z) — set the object's current/base translation matrix. The native worker also retains operand 2 as channel control state; the port currently applies x/y/z but ignores that control operand."),
0x230: dict(name='reset-gfx-cyclic-animations', category='draw', noop=False, confidence='high', source='investigation', summary='(handle) — get or create a retained gfx object, clear its cyclic-animation active flag, and zero all five looping-channel start/period pairs without changing current/base transforms.'),
0x231: dict(name='animate-gfx-srcrect-loop', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(frame_period_ms)(frame_count)(column_count) — loop row-major through the spritesheet. Every sample preserves draw-texture's source-rectangle width/height; frame=floor((shared_frame_time-object_start)/frame_period)%frame_count, src offset=(frame%columns*width, frame/columns*height). All objects use the retained manager's shared current/previous millisecond timestamps but retain their own start and period. The native consumer raises redraw dirty only when the current and previous samples select different cells. Worker gfx_worker_anim_srcrect @0x47eec0; consumer gfx_object_anim_interpolate @0x473ed0."),
0x232: dict(name='animate-gfx-color-loop', category='draw', noop=False, confidence='high', source='investigation', summary='0x232 anim-color (handle)(period)(alpha)(color): ping-pong the temporary packed ARGB passed to the normal object blit. Handler resolves negative alpha/RGB from static color obj+0x60 and clamps alpha above 255. Blend selector obj+0x30 is unchanged: mode 0 keeps default blending (animated alpha is inert; RGB is vertex modulation), while mode 1 uses sampled ARGB alpha as the SRCALPHA scale for additive composition. Fresh static color is 0xffffffff. The C# VM resolves sentinels and consumes sampled ARGB through the unchanged mode-specific path. See docs/engine-re.md §SC0000 anim cluster.'),
0x239: dict(name='animate-gfx-srcrect-target', category='draw', noop=False, confidence='high', source='investigation', summary='(handle)(delay_ms)(duration_ms)(frame_count)(column_count)(target_frame) — one-shot row-major source-rectangle cell channel. Worker gfx_worker_set_srcrect_cell @0x47ed90 stores timing at obj+0x48/+0x5c, layout at +0x238/+0x23c, and target at +0x234. C# currently retains the endpoint cell immediately.'),
0x23b: dict(name='draw-decimal-glyphs', category='draw', noop=False, confidence='high', source='investigation', summary='Draw an integer as decimal glyph objects from a style registered by opcode 0x13a.'),
0x23f: dict(name='query-surface-stop-time-ms', category='draw', noop=False, confidence='high', source='investigation', summary='(out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. Port-only host decoder failure is modeled as an explicitly completed, zero-duration movie.'),
0x2c5: dict(name='byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write the resolved NUL-terminated engine string's raw byte length."),
}
# DO NOT EDIT -- generated from vm-map/opcodes.toml by tools/opcodes_build.py --build
"""Inferred Himegari opcode semantics (generated). sys4load reads INFERRED[op]['name']."""
from __future__ import annotations
INFERRED: dict[int, dict] = {
0x21: dict(name='fade-surface-in-from-black', category='draw', noop=False, confidence='high', source='investigation', summary='(surface_slot)(timing_argument) — block while fading from black to a captured full-frame surface. The captured surface remains the terminal frame.'),
0x22: dict(name='fade-surface-out-to-black', category='draw', noop=False, confidence='high', source='investigation', summary='(surface_slot)(timing_argument) — block while fading a captured full-frame surface to black. Black remains the terminal frame.'),
0x23: dict(name='fade-surface-in-from-white', category='draw', noop=False, confidence='high', source='investigation', summary='(surface_slot)(timing_argument) — broader-AGE mode-2 sibling of 0x21: block while fading from white to a captured full-frame surface.'),
0x24: dict(name='fade-surface-out-to-white', category='draw', noop=False, confidence='high', source='investigation', summary='(surface_slot)(timing_argument) — broader-AGE mode-3 sibling of 0x22: block while fading a captured full-frame surface to white.'),
0x25: dict(name='crossfade-surfaces', category='draw', noop=False, confidence='high', source='investigation', summary='(source_surface)(target_surface)(interval_argument) — blocking legacy full-frame mode-4 alpha transition. The native engine composites the captured target surface over the captured source while an 8-bit alpha accumulator advances from 0 to 256.'),
0x60: dict(name='random-modulo', category='compute', noop=False, confidence='high', source='investigation', summary="(destination)(bound) — write CRT rand() % bound. A bound of zero first writes zero and then raises the engine's script error."),
0x6c: dict(name='zero-int-range', category='compute', noop=False, confidence='high', source='investigation', summary='(destination)(count) — write logical integer zero to count consecutive VM cells beginning at the resolved destination.'),
0x7b: dict(name='coroutine-save-yield-handlers', category='control', noop=False, confidence='high', source='investigation', summary='(handler1_pc)(handler2_pc) — scene-coroutine: save the two per-frame yield/resume handler PCs. Native writes op1→ctx[0x6da88+idx*4], op2→ctx[0x6db28+idx*4] (idx=ctx[0x53d14] script-context index); its generic handler prologue records the 5-dword instruction length. SC0000 0x79: `0x7b label_3c9 label_41e` registers the ADV per-frame render→poll→yield handlers. Part of the scene-coroutine framework (see engine-re.md §Scene-coroutine framework); pairs with 0x7c (resume) + 0x140 (loop iterator).'),
0x7c: dict(name='coroutine-resume', category='control', noop=False, confidence='high', source='investigation', summary='() — scene-coroutine RESUME point. Native requires run-state bit 0x2000000 (ctx[0x6dbc8]) set — THROWS (__CxxThrowException) if unset, so it is only ever reached on a scheduler-driven re-entry, NEVER on a cold first pass (cold flow jmps over it). Restores PC=ctx[0x53d28]+ctx[0x6dbcc]*4, clears the run-bit (ctx+0xa0ce4 &= ~0x2000000), resets input/line state. SC0000 0x443 (falls into the main loop label_444). See engine-re.md §Scene-coroutine framework.'),
0x7f: dict(name='get-message-glyph-delay', category='adv', noop=False, confidence='high', source='investigation', summary='(out) - read the current per-glyph ADV text reveal delay in milliseconds.'),
0x80: dict(name='set-default-gfx-object-slot', category='draw', noop=False, confidence='high', source='investigation', summary='(slot) - select the retained graphics-object slot used when op 0x1d9 receives explicit slot zero.'),
0x86: dict(name='set-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='(resource_id) - load an indexed cursor asset and install it as the active custom cursor.'),
0x87: dict(name='clear-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='Clear the active custom cursor and refresh the OS cursor when the game window is active.'),
0x88: dict(name='set-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set persistent all-message Skip state. Nonzero makes the interpreter inject ADV fast-forward input every tick; zero stops injection and clears the transient skip run-state bit.'),
0x8b: dict(name='set-text-line-spacing', category='adv', noop=False, confidence='high', source='investigation', summary='(pixels) - set the extra pixel spacing added between text lines.'),
0x90: dict(name='register-hotspot-callbacks', category='input', noop=True, confidence='high', source='investigation', summary="(x)(y)(w)(h)(on_enter_pc)(on_leave_pc)(on_activate_pc) - append an inclusive cursor rectangle and three callback PCs to the current script frame's native input registry. This opcode only registers; the ADV input service dispatches callbacks later."),
0x93: dict(name='cancel-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary="Reset the current frame's hotspot registry/input wait and clear native run-state bit 0x00800000. Used before opening History, Menu, or HIDEWIN flows."),
0x94: dict(name='arm-hotspot-wait', category='input', noop=True, confidence='high', source='investigation', summary='Arm native hotspot input processing after the script has registered its rectangles.'),
0x97: dict(name='bind-hotspot-key', category='input', noop=True, confidence='high', source='investigation', summary='(x)(y)(w)(h)(logical_action) - find the already-registered rectangle with identical bounds and bind a logical input action to its activation callback.'),
0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(packed_raw_resource_id)(channel) — synchronously open the universal SYS4INI/AAI catalog entry and replace the channel's decoded sound buffer without starting playback. A zero high byte is a raw SYS4INI index; a nonzero high byte selects an append catalog and uses the low 24-bit index. Native manager supports channels 0..12; SC0000 uses 0..9."),
0xb5: dict(name='sfx-start', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) — start the already-loaded channel once (logical loop=false). DirectSound publishes synchronously through Play(0,0,DSBPLAY_LOOPING); the low-level flag loops only the streaming ring, while decoder EOF stops logical playback.'),
0xb6: dict(name='sfx-release', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) - stop/destroy the channel decoder and DirectSound buffer, clear its retained resource id, and leave the slot empty. Idempotent for an unused channel.'),
0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', summary='(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.'),
0xcc: dict(name='register-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='(poll_interval_ms)(target_pc) - register a timed per-frame mouse callback in the current script.'),
0xcd: dict(name='dispatch-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch the registered mouse callback when its polling interval elapses.'),
0xd3: dict(name='begin-timed-callback-sequence', category='control', noop=False, confidence='high', source='investigation', summary="Clear and initialize the current script frame's timed local-callback sequence."),
0xd4: dict(name='append-relative-timed-callbacks', category='control', noop=False, confidence='high', source='investigation', summary="Append repeated relative deadlines and their on-time/catch-up local callback targets to the current frame's timed sequence."),
0xd5: dict(name='run-timed-callback-sequence', category='control', noop=False, confidence='high', source='investigation', summary='Start or service the frame-local timed sequence, dispatching scheduled local callbacks through the penultimate entry and selecting the catch-up target when the following deadline is already late.'),
0xd9: dict(name='clear-run-state-0x1000', category='control', noop=True, confidence='high', source='investigation', summary='Clear native run/service bit 0x1000; if the secondary context is active, clear the same bit there. SC0000 executes it once after the initial SFX-channel reset, with no VM-visible result.'),
0xfb: dict(name='register-joy-callback', category='input', noop=False, confidence='high', source='investigation', summary='(input_index)(target_pc) - register one of 32 per-frame joy/input callback targets.'),
0xfe: dict(name='set-input-action-count', category='input', noop=False, confidence='high', source='investigation', summary='(count) - set the logical action scan bound; op 0x100 uses callback slot count itself when the polled mask is empty.'),
0xff: dict(name='poll-joy-callback-input', category='input', noop=False, confidence='high', source='investigation', summary='Poll the configured keyboard/mouse/joystick action mask and initialize the op 0x100 dispatch scan.'),
0x100: dict(name='dispatch-joy-callbacks', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch each set logical action below the configured count, or callback slot count when no action is held.'),
0x101: dict(name='reset-message-skip-input', category='input', noop=False, confidence='med', source='investigation', summary="Reset transient message-skip/input service state after an ADV chrome action without clearing op 0x88's persistent all-message Skip flag."),
0x107: dict(name='map-joystick-button', category='input', noop=False, confidence='high', source='investigation', summary='(button_slot)(physical_button) - map a logical button slot to a physical joystick button; slot N emits logical action N+4.'),
0x108: dict(name='get-mouse-button-state', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current mouse-button state bitmask.'),
0x109: dict(name='get-cursor-virtual', category='input', noop=False, confidence='high', source='investigation', summary="(out_x)(out_y) - read the OS cursor and convert it into AGE's virtual-screen coordinates."),
0x10a: dict(name='set-cursor-virtual', category='input', noop=False, confidence='high', source='investigation', summary='(x)(y) - convert AGE virtual-screen coordinates to client/screen coordinates and move the OS cursor.'),
0x10b: dict(name='map-mouse-button', category='input', noop=False, confidence='high', source='investigation', summary='(button_slot)(physical_button) - map a physical mouse button to a logical button slot; polling emits the slot as logical action slot+4.'),
0x10c: dict(name='map-keyboard-scancode', category='input', noop=False, confidence='high', source='investigation', summary='(action)(dik_scan_code) - map a DirectInput keyboard scan code to a logical input action.'),
0x10d: dict(name='consume-mouse-wheel-delta', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the accumulated signed mouse-wheel delta and clear it.'),
0x13a: dict(name='register-numeric-glyph-style', category='draw', noop=False, confidence='high', source='investigation', summary='Register a decimal-glyph atlas style as (surface slot, source x/y, digit width/height); the handler admits style indices 0..10.'),
0x140: dict(name='coroutine-label-yield', category='control', noop=False, confidence='med', source='investigation', summary="(out)(name_str)(sub_str)(in) — scene-coroutine LOOP ITERATOR / labeled yield. Handler copies name/sub strings + the int operand and calls AGERC host command 8 through g_pfnAgercHostCallback(8, ctx[0x54fe8], &{name,sub,in}); writes the returned PC-like value to operand 1. In SC0000 label_462 'ループ開始' (@0x46d): `out=G[0x6be]=LABEL('J',G[0x6be])`; loop runs the intro-setup body (incl. call label_125bd = slot-table fill G[0x3239..0x324e]=4..11) and jmps back until out==G[0x6c3] (a per-scene exit-PC immediate) → mov aba5c 0 → content. The gate G[0xaba5c]==1 that opens this loop is NATIVE scene-entry state (no script sets it to 1). PORT = HOST-MODEL IMPLEMENTED: synthesize the ADV scene-entry gate, run the LABEL/J setup body once, then return the structurally discovered per-scene terminal; do not emulate the service dialog. See engine-re.md §Scene-coroutine framework."),
0x143: dict(name='run-mounted-append-autoruns', category='control', noop=False, confidence='high', source='investigation', summary='() - enqueue record zero from every mounted nonzero AAI selector in ascending selector order, then execute those packed scripts serially before resuming the caller.'),
0x199: dict(name='yield-adv-coroutine', category='control', noop=False, confidence='high', source='investigation', summary='Yield/re-enter the registered ADV coroutine handler. The fifth standard chrome button uses this transition to enter the HIDEWIN/window-hidden flow.'),
0x19a: dict(name='get-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current all-message skip state set by op 0x88.'),
0x19b: dict(name='suspend-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary="() - suspend persistent/read-driven ADV fast-forward while preserving the user's all-message Skip toggle; physically held logical action 6 is independent."),
0x19c: dict(name='resume-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary='() - enable the persistent/read ADV skip service and recompute active fast-forward from all-message Skip or the live read-skip channel; physical action 6 remains independent.'),
0x1a6: dict(name='half-byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write half the resolved string's byte length, using integer truncation."),
0x1a8: dict(name='instruction-marker-noop', category='marker', noop=True, confidence='high', source='investigation', summary="Zero-operand structural marker. The native shared 0xaf/0x1a8 handler only records this instruction's one-dword length and returns."),
0x1b2: dict(name='append-diagnostic-value', category='control', noop=False, confidence='high', source='investigation', summary="Format operand 1 with AGE's generic operand-to-string conversion and append its exact bytes to the context diagnostic accumulator. This does not present or clear the accumulated text."),
0x1b3: dict(name='append-diagnostic-newline', category='control', noop=False, confidence='high', source='investigation', summary='Append the literal CRLF byte pair to the context diagnostic accumulator without presenting or clearing it.'),
0x1b4: dict(name='show-and-clear-diagnostic', category='control', noop=False, confidence='high', source='investigation', summary='Show the accumulated text plus native script-location context in a synchronous owner-window information message box, then erase the complete accumulator.'),
0x1b5: dict(name='set-message-glyph-delay', category='adv', noop=False, confidence='high', source='investigation', summary='(milliseconds) - set the per-glyph ADV text reveal delay; zero makes retained UI text publish immediately.'),
0x1b6: dict(name='get-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return whether automatic message advance is enabled.'),
0x1b7: dict(name='set-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - enable or disable automatic message advance.'),
0x1b8: dict(name='get-auto-message-time', category='input', noop=False, confidence='high', source='investigation', summary='(selector)(out) - read an Auto-message delay from engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.'),
0x1b9: dict(name='set-auto-message-time', category='input', noop=False, confidence='high', source='investigation', summary='(selector)(milliseconds) - write an Auto-message delay to engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.'),
0x1bc: dict(name='reset-message-voice-state', category='input', noop=False, confidence='high', source='investigation', summary='Reset the per-message queued-voice flag used by ADV Auto timing.'),
0x1bd: dict(name='play-history-voice', category='audio', noop=False, confidence='high', source='investigation', summary='Replay a voice id selected from retained ADV text history using native playback/history variant 1, preserving normal Skip and Auto-voice state behavior.'),
0x1bf: dict(name='call-end', category='marker', noop=True, confidence='med', source='inference', summary='zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker'),
0x1ca: dict(name='set-read-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set the engine setting `message:ReadTextSkip`, which skips only previously read text.'),
0x1cb: dict(name='get-read-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - read the engine setting `message:ReadTextSkip`.'),
0x1ce: dict(name='set-adv-wait-indicator-enabled', category='adv', noop=False, confidence='high', source='investigation', summary='(enabled) - explicitly start or stop the animated ADV input-wait indicator service.'),
0x1d1: dict(name='render-text-history', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot)(record_index)(flags)(color_a)(color_b) - render retained ADV text records into a selected text layout/surface.'),
0x1d5: dict(name='cond-block', category='marker', noop=True, confidence='high', source='inference', summary='zero-arg; ALWAYS follows jcc — marks conditional body entry'),
0x1f4: dict(name='stmt-begin', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; opens scripts, pairs with stmt-end 0x1f5'),
0x1f5: dict(name='stmt-end', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; precedes exit/next-stmt, pairs with 0x1f4'),
0x1fd: dict(name='gfx-set-scale-current', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(scale_x_percent)(scale_y_percent)(scale_z_percent) — immediately replace the retained object's current scale matrix at obj+0x6c. The handler divides each integer operand by 100.0 before calling matrix4_make_scale; this is distinct from 0x21e's delayed one-shot target scale."),
0x1ff: dict(name='set-current-translation', category='draw', noop=False, confidence='high', source='investigation', summary="0x1ff (handle)(x)(y)(z) — immediately replace the retained object's current translation matrix at obj+0x16c. This is the direct-current companion to 0x220's delayed target at obj+0x1ac."),
0x205: dict(name='draw-formatted-integer', category='draw', noop=False, confidence='high', source='investigation', summary='Format an integer into a fixed-width decimal field and rasterize it immediately into a numbered graphics surface using the current font/color/effect state.'),
0x20a: dict(name='publish-adv-text-layout', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame.'),
0x21b: dict(name='line-id?', category='marker', noop=True, confidence='med', source='harness', summary='1 imm; mov->0x21b->stmt-end; near save/load-messkip — likely line/stmt id, verify not msg-control'),
0x228: dict(name='query-gfx-translation-target', category='draw', noop=False, confidence='high', source='investigation', summary='0x228 query translation target (succ)(handle)(outX)(outY)(outZ): clone the retained object, decompose its target translation matrix at obj+0x17c, and return matrix translation obj+0x1ac/+0x1b0/+0x1b4. Returns succ=0 when found; when absent, writes succ=1 and leaves outputs untouched. The C# VM queries TranslationTarget independently of V24. See docs/engine-re.md §SC0000 anim cluster.'),
0x22f: dict(name='set-gfx-position-current', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(control)(x)(y)(z) — set the object's current/base translation matrix. The native worker also retains operand 2 as channel control state; the port currently applies x/y/z but ignores that control operand."),
0x230: dict(name='reset-gfx-cyclic-animations', category='draw', noop=False, confidence='high', source='investigation', summary='(handle) — get or create a retained gfx object, clear its cyclic-animation active flag, and zero all five looping-channel start/period pairs without changing current/base transforms.'),
0x231: dict(name='animate-gfx-srcrect-loop', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(frame_period_ms)(frame_count)(column_count) — loop row-major through the spritesheet. Every sample preserves draw-texture's source-rectangle width/height; frame=floor((shared_frame_time-object_start)/frame_period)%frame_count, src offset=(frame%columns*width, frame/columns*height). All objects use the retained manager's shared current/previous millisecond timestamps but retain their own start and period. The native consumer raises redraw dirty only when the current and previous samples select different cells. Worker gfx_worker_anim_srcrect @0x47eec0; consumer gfx_object_anim_interpolate @0x473ed0."),
0x232: dict(name='animate-gfx-color-loop', category='draw', noop=False, confidence='high', source='investigation', summary='0x232 anim-color (handle)(period)(alpha)(color): ping-pong the temporary packed ARGB passed to the normal object blit. Handler resolves negative alpha/RGB from static color obj+0x60 and clamps alpha above 255. Blend selector obj+0x30 is unchanged: mode 0 keeps default blending (animated alpha is inert; RGB is vertex modulation), while mode 1 uses sampled ARGB alpha as the SRCALPHA scale for additive composition. Fresh static color is 0xffffffff. The C# VM resolves sentinels and consumes sampled ARGB through the unchanged mode-specific path. See docs/engine-re.md §SC0000 anim cluster.'),
0x239: dict(name='animate-gfx-srcrect-target', category='draw', noop=False, confidence='high', source='investigation', summary='(handle)(delay_ms)(duration_ms)(frame_count)(column_count)(target_frame) — one-shot row-major source-rectangle cell channel. Worker gfx_worker_set_srcrect_cell @0x47ed90 stores timing at obj+0x48/+0x5c, layout at +0x238/+0x23c, and target at +0x234. C# currently retains the endpoint cell immediately.'),
0x23b: dict(name='draw-decimal-glyphs', category='draw', noop=False, confidence='high', source='investigation', summary='Draw an integer as decimal glyph objects from a style registered by opcode 0x13a.'),
0x23f: dict(name='query-surface-stop-time-ms', category='draw', noop=False, confidence='high', source='investigation', summary='(out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. Port-only host decoder failure is modeled as an explicitly completed, zero-duration movie.'),
0x2c5: dict(name='byte-string-length', category='compute', noop=False, confidence='high', source='investigation', summary="Write the resolved NUL-terminated engine string's raw byte length."),
}

View File

@@ -8,6 +8,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\engine\Age.Engine\Age.Engine.csproj" />
<Compile Include="..\..\godot\FfmpegMovieNative.cs" Link="FfmpegMovieNative.cs" />
<ProjectReference Include="..\..\engine\Age.Engine.Frontend\Age.Engine.Frontend.csproj" />
</ItemGroup>
</Project>

View File

@@ -100,8 +100,9 @@ def skeleton_toml(op: int, label: str, argc: int, argtypes_for_op: dict,
f"observed_types = [{obs}]"]
return "\n".join(lines) + "\n"
def bootstrap(toml_path: Path) -> None:
used, argtypes = scan_corpus()
def bootstrap(toml_path: Path, *, corpus_scan=None) -> None:
"""Append observed skeletons from a real scan or an injected synthetic scan fixture."""
used, argtypes = corpus_scan if corpus_scan is not None else scan_corpus()
present = set(M.load(toml_path).opcodes) if toml_path.exists() else set()
blocks = []
for op in sorted(used):

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Standalone tests for the opcode reference tooling. Run: py -3.11 -X utf8 tools/test_opcodes.py"""
import os, sys, tempfile
from collections import Counter
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import opcodes_model as M
@@ -120,14 +121,21 @@ def test_bootstrap():
import opcodes_build as B
from age_opcodes import OPCODES
from pathlib import Path
canonical = M.load(Path(__file__).resolve().parents[1] / "vm-map" / "opcodes.toml")
observed = sorted(op for op, entry in canonical.opcodes.items() if entry.observed_in_himegari)
synthetic_scan = (
Counter({op: 1 for op in observed}),
{op: {i: {0} for i in range(OPCODES[op][1])} for op in observed},
)
fd, p = tempfile.mkstemp(suffix=".toml"); os.close(fd); os.remove(p)
tp = Path(p)
B.bootstrap(tp) # first run: meta + all skeletons
B.bootstrap(tp, corpus_scan=synthetic_scan) # first run: meta + synthetic observed skeletons
m = M.load(tp)
check(len(m.opcodes) >= 240, f"bootstrap seeded ~248 opcodes (got {len(m.opcodes)})")
check(len(m.opcodes) == len(observed),
f"bootstrap seeded all {len(observed)} synthetic observations (got {len(m.opcodes)})")
check(0x90 in m.opcodes and m.opcodes[0x90].argc == 7, "0x90 seeded with argc 7")
n1 = len(m.opcodes)
B.bootstrap(tp) # idempotent: appends nothing new
B.bootstrap(tp, corpus_scan=synthetic_scan) # idempotent: appends nothing new
check(len(M.load(tp).opcodes) == n1, "second bootstrap adds no duplicates")
e, w = M.lint(m)
check(e == [], f"bootstrapped file lints clean (errors: {e[:3]})")

View File

@@ -75,13 +75,21 @@ class GatePlanTests(unittest.TestCase):
self.assertNotIn("sys4-corpus-validate", keys)
self.assertNotIn("godot-selftest", keys)
self.assertNotIn("age-cli-sweep", keys)
engine = next(gate for gate in validate.build_gate_plan("core") if gate.key == "engine-tests")
self.assertIn("Category!=Workspace", engine.command)
def test_workspace_extends_core(self) -> None:
core = {gate.key for gate in validate.build_gate_plan("core")}
workspace = {gate.key for gate in validate.build_gate_plan("workspace")}
self.assertLess(core, workspace)
self.assertIn("sys4-corpus-validate", workspace)
self.assertIn("engine-workspace-tests", workspace)
self.assertNotIn("godot-selftest", workspace)
installed = next(
gate for gate in validate.build_gate_plan("workspace")
if gate.key == "engine-workspace-tests"
)
self.assertIn("Category=Workspace", installed.command)
def test_runtime_requires_resolved_paths(self) -> None:
with self.assertRaisesRegex(ValueError, "resolved Godot"):

View File

@@ -152,16 +152,27 @@ def build_gate_plan(
gates.append(
Gate(
"engine-tests",
".NET engine tests",
".NET engine core tests",
(
"dotnet", "test", "engine/AgeEngine.sln", "--nologo", "--verbosity", "minimal",
"-p:UseSharedCompilation=false",
"--filter", "Category!=Workspace", "-p:UseSharedCompilation=false",
),
300,
)
)
if "workspace" in phases:
gates.append(
Gate(
"engine-workspace-tests",
".NET installed-data and native-oracle tests",
(
"dotnet", "test", "engine/AgeEngine.sln", "--nologo", "--verbosity", "minimal",
"--filter", "Category=Workspace", "-p:UseSharedCompilation=false",
),
300,
)
)
gates.append(
Gate("globals-build", "Global registry build", _python_tool("globals_build.py", "--build"))
)