From e2973f66b14b9b7d7a76354afe73c01bb95b857a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 21 Jul 2026 15:40:46 -0400 Subject: [PATCH] Document numeric HUD opcode contracts --- docs/engine-re.md | 32 ++++++++++++++++ docs/opcode-reference.md | 35 +++++++++++------ docs/phase-b-framework.md | 17 +++++++++ tools/age_opcodes_himegari.py | 3 ++ vm-map/opcodes.toml | 71 ++++++++++++++++++----------------- 5 files changed, 112 insertions(+), 46 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index a7ab547..9975b2b 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -2467,6 +2467,38 @@ Focused tests cover stability, overflow, and zero count. A real-script regressio SYSTEM4-to-SC0000 state into CHMENU and stops after `0x1db8`, proving selected slot 2 survives the first release roster sort without a fallback. No startup seed or menu-specific roster injection is involved. +### Numeric glyph styles/draws and half-byte string length — `0x13a` / `0x23b` / `0x1a6` (2026-07-21) + +The missing values in the first working `DEBUGMAP` field HUD are not missing globals or a failed texture +load. They are generated by a dedicated decimal-glyph subsystem whose two opcodes were still skipped by +the port: + +- `op_0x13a_register_numeric_glyph_style@0x421ab0` takes + `(style_index, surface_slot, atlas_x, atlas_y, digit_width, digit_height)`. It accepts style indices + `0..10` and stores the remaining five operands in the 20-byte record at + `EngineCtx+0x55180+style_index*0x14`; an invalid index raises the standard script error. The corpus has + 74 registrations in 24 scripts, including eight in `DRAWCHP.BIN`. +- `op_0x23b_draw_decimal_glyphs@0x424190` takes + `(base_handle, style_index, value, x, y, digit_capacity, flags)`. It first erases the destination handle + range, splits the signed integer through division/remainder by ten, and binds one retained object per + displayed digit using horizontally adjacent cells from the selected style record. Flag bit `0x1` + includes leading zeroes, bit `0x2` centers the used digits, and bit `0x4` left-aligns them; without an + alignment flag the value is right-aligned in the requested capacity. An invalid or unregistered style + raises the script error. The corpus has 147 draws in the same 24 scripts; `DRAWCHP.BIN` has 22 and uses + them for the field HUD's turn/control/mana/level/HP/SP/FS values. + +This path creates ordinary retained graphics objects, so the existing atlas decode and compositor are +already the correct backend. The missing engine model is the 11 style records plus the two VM dispatches; +it is not an immediate `GodotAdvHost.DrawTexture` raster operation. + +The absent unit and weapon names are a separate layout-compute gap. `DRAWCHP.BIN` does populate both +strings and calls `draw-string` at `0x9c6`/`0x9f5`, but first centers each one with opcode `0x1a6`. +`op_0x1a6_half_byte_strlen@0x427020` resolves the NUL-terminated engine byte string and writes +`strlen(bytes) >> 1`. The script multiplies that result by 21 and subtracts it from x=257 on a 263-pixel +scratch surface. With the opcode skipped, x remains 257 and Godot correctly clips nearly all of the text. +An implementation must preserve the original encoded byte count (or reproduce it with CP932 encoding), +not use the .NET UTF-16 character count. + --- ## Native walls backlog (targets for this loop) diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 2fcb920..2b16365 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -237,6 +237,13 @@ Implemented with domain-preserving addressed-array access, native signed 32-bit - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2 op_0x194_string_equals@0x426e20 fetches operands 2 and 3 through the string resolver, compares their byte ranges through FUN_004017a0, and writes compare_result==0 to integer operand 1. INIT2 and GAMESTART use it as a branch predicate for INPUTNAME/default-name handling; the natural Game Start diagnostic reached one GAMESTART call at 0x134c. The C# VM implements ordinal equality through the shared string resolver, covering literal, global, local, global-string-pointer, and local-string-pointer operands; the traced natural-boot regression proves the reached GAMESTART call no longer falls back. +### 0x1a6 `half-byte-string-length` (halve-strlen, argc 2) +- **summary:** Write half the resolved string's byte length, using integer truncation. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x1a6_half_byte_strlen@0x427020 resolves operand 2, scans to NUL, and vm_operand_write(1, strlen_bytes >> 1). DRAWCHP sites 0x9b3/0x9e2 multiply the result by 21 and subtract it from x=257 before draw-string. + +Native applies strlen to the NUL-terminated engine byte string and shifts the byte count right by one. DRAWCHP uses this Shift-JIS-oriented width proxy to center unit and weapon names before rasterizing them into a scratch surface. + ### 0x1b0 `copy-dwords` (copy-dwords, argc 3) - **summary:** (source)(destination)(count) - copy count consecutive 32-bit cells from source to destination. - **grounding:** source=investigation, confidence=high @@ -412,6 +419,14 @@ The handler uses an alpha step of 1 and timer interval=argument when argument <= - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x131_handler@0x4295e0 calls the settings getter with `message:MesWinAlpha` and writes the result. HISTORY.BIN and the shared ADV redraw path compute (16-value)<<4 for the control-strip alpha. +### 0x13a `register-numeric-glyph-style` (u0041F3A0, argc 6) +- **summary:** Register one of 11 decimal-glyph atlas styles as (surface slot, source x/y, digit width/height). +- **grounding:** source=investigation, confidence=high +- **depended on by:** 0x23b +- **evidence:** Ghidra /v2: op_0x13a_register_numeric_glyph_style@0x421ab0 writes operands 2..6 to the selected five-dword record at EngineCtx+0x55180 after enforcing style_index<11. Corpus: 74 sites in 24 scripts; DRAWCHP has eight registrations immediately before its stylized HUD-number draws. + +The five-dword definition is stored at EngineCtx+0x55180+style_index*0x14. Opcode 0x23b consumes it to turn an integer into retained draw objects, one atlas cell per decimal digit. An index outside [0,10] raises the engine's script error. + ### 0x1f6 `clear-retained-gfx-objects` (clear-retained-gfx-objects, argc 0) - **summary:** Clear the complete retained gfx-object registry while preserving allocated surface resources. Subsequent object queries return absent until draw/geometry operations recreate records. - **grounding:** source=investigation, confidence=high @@ -650,6 +665,14 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b - **grounding:** source=investigation, confidence=high - **evidence:** Native /v2 worker and gfx_object_apply_transform_channels decompile. The consumer advances target_frame cells over duration after delay, preserves the existing source-rect dimensions, and commits the endpoint. +### 0x23b `draw-decimal-glyphs` (u00422460, argc 7) +- **summary:** Draw an integer as decimal glyph objects from a style registered by opcode 0x13a. +- **grounding:** source=investigation, confidence=high +- **depends on:** 0x13a +- **evidence:** Ghidra /v2: op_0x23b_draw_decimal_glyphs@0x424190 reads the five-dword style record at EngineCtx+0x55180, erases the destination handle range, and calls gfx_object_bind_draw once per displayed digit. Corpus: 147 sites in 24 scripts; DRAWCHP contains 22 calls for the field HUD, including turn/control/mana/level/HP/SP/FS values visible as blank in the DEBUGMAP discrepancy. + +First erase digit_capacity objects beginning at base_handle. Then split value by signed division/modulo 10 and bind at most digit_capacity retained objects using adjacent digit-width cells from the registered atlas. Flags bit 0 zero-pads, bit 1 centers the used digits, and bit 2 left-aligns them; with no alignment bit the value is right-aligned in the capacity. Invalid or unregistered style indices raise the engine's script error. + ### 0x23d `release-transient-surfaces` (release-transient-surfaces, argc 0) - **summary:** Stop movie bindings and release transient gfx surface slots 42 through 999 inclusive, preserving system-owned slots 0 through 41. - **grounding:** source=investigation, confidence=high @@ -1075,10 +1098,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=low -### 0x13a `u0041F3A0` (u0041F3A0, argc 6) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x13f `check-bit` (check-bit, argc 3) - **summary:** — - **grounding:** source=kelebek, confidence=med @@ -1143,10 +1162,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=med -### 0x1a6 `halve-strlen` (halve-strlen, argc 2) -- **summary:** — -- **grounding:** source=kelebek, confidence=med - ### 0x1a7 `comment` (comment, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=med @@ -1219,10 +1234,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=low -### 0x23b `u00422460` (u00422460, argc 7) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x23c `u004162B0` (u004162B0, argc 0) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index dc474f3..e7f83bb 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -434,6 +434,23 @@ that value during synchronous movie-graph initialization and retains it per movi slots return -1; unavailable timing metadata warns and returns -1 rather than exposing native's undefined failure output. +The next visual comparison found all field-HUD numbers absent while their labels, bars, and source artwork +were present, plus apparently blank unit/weapon text. Native and script-side tracing resolves this into two +bounded opcode gaps rather than missing game state: + +- `0x13a` registers one of 11 `(surface, atlas x/y, digit width/height)` styles, and `0x23b` expands a + decimal value into retained per-digit objects with zero-pad/center/left/right layout flags. `DRAWCHP.BIN` + contains eight style registrations and 22 numeric draws covering the visible turn/control/mana/level and + HP/SP/FS fields. Both opcodes are currently unimplemented, which precisely explains the blank values. +- `DRAWCHP.BIN` already reads and submits the unit and weapon strings to `0x204`, but its two preceding + `0x1a6` centering calls are unimplemented. Native `0x1a6` returns `strlen(CP932_bytes) >> 1`; skipping it + places the strings at x=257 on a 263-pixel scratch surface, so the existing compositor clips them. + +The exact native contracts and addresses live in `docs/engine-re.md`; opcode-source metadata is in +`vm-map/opcodes.toml`. No runtime implementation was made in this investigation. The next field slice is +therefore to implement the shared 11-style numeric renderer and the encoded-byte-length calculation, then +recheck the same `DEBUGMAP` HUD before pursuing any state seeding. + ## Later Phase B breadth Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks: diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index 0fcc6d7..7041296 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -41,11 +41,13 @@ INFERRED: dict[int, dict] = { 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 one of 11 decimal-glyph atlas styles as (surface slot, source x/y, digit width/height).'), 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 the NATIVE video/transition service (*DAT_005c6018)(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). DAT_005c6018 is runtime-resolved (all xrefs READ) = SAME class as the DirectDraw workers we don't model. 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 video service. See engine-re.md §Scene-coroutine framework."), 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 active ADV fast-forward while preserving the user's persistent all-message Skip toggle."), 0x19c: dict(name='resume-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary='() - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel.'), + 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."), 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.'), @@ -71,6 +73,7 @@ INFERRED: dict[int, dict] = { 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 frame preserves draw-texture's source-rectangle width/height; frame=floor(elapsed/frame_period)%frame_count, src offset=(frame%columns*width, frame/columns*height). 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. The port retains this metadata during 0x236 graph initialization; unavailable metadata emits a warning and also returns -1.'), 0x258: dict(name='decl?', category='marker', noop=True, confidence='low', source='harness', summary='2 imm; runs in a chain right after script-entry 0x259, enumerating ids — prologue declaration/registration?'), 0x259: dict(name='script-entry', category='marker', noop=True, confidence='low', source='harness', summary='zero-arg; the first instruction of a script (offset 0), opens the decl chain that 0x258 continues — script/prologue entry marker, structural'), diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index fc318ca..d282383 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -2814,43 +2814,44 @@ argc = 6 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041F3A0" -category = "unknown" -summary = "" +name = "register-numeric-glyph-style" +category = "draw" +summary = "Register one of 11 decimal-glyph atlas styles as (surface slot, source x/y, digit width/height)." +details = "The five-dword definition is stored at EngineCtx+0x55180+style_index*0x14. Opcode 0x23b consumes it to turn an integer into retained draw objects, one atlas cell per decimal digit. An index outside [0,10] raises the engine's script error." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x13a_register_numeric_glyph_style@0x421ab0 writes operands 2..6 to the selected five-dword record at EngineCtx+0x55180 after enforcing style_index<11. Corpus: 74 sites in 24 scripts; DRAWCHP has eight registrations immediately before its stylized HUD-number draws." [[opcode.semantics.args]] i = 1 -role = "" +role = "style_index" observed_types = ["imm"] [[opcode.semantics.args]] i = 2 -role = "" +role = "surface_slot" observed_types = ["imm", "l-int"] [[opcode.semantics.args]] i = 3 -role = "" +role = "atlas_x" observed_types = ["imm"] [[opcode.semantics.args]] i = 4 -role = "" +role = "atlas_y" observed_types = ["imm"] [[opcode.semantics.args]] i = 5 -role = "" +role = "digit_width" observed_types = ["imm"] [[opcode.semantics.args]] i = 6 -role = "" +role = "digit_height" observed_types = ["imm"] [[opcode]] @@ -3559,23 +3560,24 @@ argc = 2 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "halve-strlen" -category = "unknown" -summary = "" +name = "half-byte-string-length" +category = "compute" +summary = "Write half the resolved string's byte length, using integer truncation." +details = "Native applies strlen to the NUL-terminated engine byte string and shifts the byte count right by one. DRAWCHP uses this Shift-JIS-oriented width proxy to center unit and weapon names before rasterizing them into a scratch surface." noop_headless = false -source = "kelebek" -confidence = "med" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x1a6_half_byte_strlen@0x427020 resolves operand 2, scans to NUL, and vm_operand_write(1, strlen_bytes >> 1). DRAWCHP sites 0x9b3/0x9e2 multiply the result by 21 and subtract it from x=257 before draw-string." [[opcode.semantics.args]] i = 1 -role = "" +role = "destination" observed_types = ["l-int"] [[opcode.semantics.args]] i = 2 -role = "" +role = "string" observed_types = ["l-str", "l-str-ptr"] [[opcode]] @@ -6299,48 +6301,49 @@ argc = 7 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00422460" -category = "unknown" -summary = "" +name = "draw-decimal-glyphs" +category = "draw" +summary = "Draw an integer as decimal glyph objects from a style registered by opcode 0x13a." +details = "First erase digit_capacity objects beginning at base_handle. Then split value by signed division/modulo 10 and bind at most digit_capacity retained objects using adjacent digit-width cells from the registered atlas. Flags bit 0 zero-pads, bit 1 centers the used digits, and bit 2 left-aligns them; with no alignment bit the value is right-aligned in the capacity. Invalid or unregistered style indices raise the engine's script error." noop_headless = false -source = "kelebek" -confidence = "low" -depends_on = [] -evidence = "" +source = "investigation" +confidence = "high" +depends_on = [0x13a] +evidence = "Ghidra /v2: op_0x23b_draw_decimal_glyphs@0x424190 reads the five-dword style record at EngineCtx+0x55180, erases the destination handle range, and calls gfx_object_bind_draw once per displayed digit. Corpus: 147 sites in 24 scripts; DRAWCHP contains 22 calls for the field HUD, including turn/control/mana/level/HP/SP/FS values visible as blank in the DEBUGMAP discrepancy." [[opcode.semantics.args]] i = 1 -role = "" +role = "base_handle" observed_types = ["imm", "l-int"] [[opcode.semantics.args]] i = 2 -role = "" +role = "style_index" observed_types = ["imm", "l-int"] [[opcode.semantics.args]] i = 3 -role = "" +role = "value" observed_types = ["imm", "g-int", "l-int", "l-ptr"] [[opcode.semantics.args]] i = 4 -role = "" +role = "destination_x" observed_types = ["imm", "l-int"] [[opcode.semantics.args]] i = 5 -role = "" +role = "destination_y" observed_types = ["imm", "l-int"] [[opcode.semantics.args]] i = 6 -role = "" +role = "digit_capacity" observed_types = ["imm"] [[opcode.semantics.args]] i = 7 -role = "" +role = "flags" observed_types = ["imm"] [[opcode]]