TH08 semantic reconstruction
Work only from the verified Japanese TH08 1.00d target and the current authored
source. Read AGENTS.md, docs/ARCHITECTURE.md, docs/RE_WORKFLOW.md,
docs/RE_HANDOFF.md, docs/SEMANTIC_RECONSTRUCTION.md, and
docs/SEMANTIC_PLAYBOOK.md before editing.
Preflight
Run the target/tracking preflight and keep config/claims.csv header-only. Use
scripts/analysis/report-semantic-debt.py as a candidate router, never as a
completion metric. Select one structure and one coherent field family.
Confirm every touched function in config/mapping.csv,
config/reccmp-functions.csv, and config/match-units.toml. Establish the
current accepted-unit baseline for every affected object before changing a
shared declaration.
Recover meaning
Record target facts, corroboration, inference, and unknowns separately. Inspect
all reads and writes, widths, signedness, bit operations, callers, strings, and
canonical object ownership. TH06/TH07 and upstream names are corroboration
only when TH08 evidence agrees. IDA evidence remains blocked until the active
database passes docs/IDA_MCP.md.
Treat ownership as four separate questions: storage owner, semantic owner,
translation-unit/COMDAT emission owner, and production/probe/portable build
owner. Evidence for one does not establish the others. Close the relevant
producer, consumer, and state-transition set before changing any of them.
Prefer real fields, aggregates, enums, bitfields, and member/index expressions.
Preserve opaque storage when only its extent is known. Add focused C_ASSERT
checks for relied-on sizes and offsets, remembering that layout assertions do
not prove names.
When one fixed storage range has several target-proven roles selected by a
discriminator, a translation-unit-local typed union overlay may be safer than
changing a widely included VC7 header. Anchor the single cast boundary to a
real member with offsetof/sizeof assertions, keep generic initialization
explicit, and use role-specific members everywhere else. This exception is
for evidenced variant storage, not a way to assign meaning to unknown bytes.
Do not create a new global for a view inside an existing aggregate, change
field width/signedness, hide uncertainty behind a union/accessor, or combine a
typed cleanup with unrelated control-flow refactoring. Serialization,
instruction decoding, and platform ABI glue may be correctly byte-oriented.
Close router-invisible protocol debt
The structural debt report does not find numeric interpreter cases, stable
resource/sound/effect IDs, repeated flag masks, or ABI-compatible vector casts.
Audit those surfaces explicitly after field/layout cleanup:
- Inventory every serialized opcode value and every numeric dispatch site.
Include comparisons, range arithmetic, and out-of-order handlers, not only
case labels. Give every observed value an explicit enum initializer.
Preserve the handler's physical order because VC7 switch emission can depend
on lexical placement. Use a neutral protocol name when the handler proves
storage or control flow but not a narrower gameplay term.
Do not stop after the primary opcode table: inventory operand selectors,
timeline opcodes, stage/background streams, curve/mode selectors, replay
synchronization flags, and other sibling dispatch tables. TH08's primary
ECL opcodes were readable while all 101 operand selectors and both secondary
script streams were still raw; the structural debt router did not report
any of them.
- Separate instruction selector bits from physical object flag bits. Two
masks with the same numeric value are not interchangeable namespaces. Name
shifts and whole-word masks when target code requires a shift/test or raw
dword update; do not rewrite them as prettier bitfield access if that changes
emission.
- Name only stable numeric IDs supported by a dispatcher/table and production
call roles. Resource slots may be named from load/get/release ownership;
sound/effect names need target call-site behavior, with adjacent games used
only as corroboration. Leave stage-script data and visually ambiguous
entries numeric or neutrally named rather than guessing from one animation.
- For representation-compatible views such as
Float3 * and
D3DXVECTOR3 *, prefer a macro whose expansion is the already accepted cast.
A normal inline accessor can introduce a call under /Ob0. Keep the cast at
a named boundary and validate both VC7 and the modern compiler.
Once a protocol surface is complete, add a source guard that checks its exact
value set, rejects numeric dispatch labels/writers on that surface, and proves
that every enum member is referenced by the decoder. Keep this scoped: array
indices, numeric quantities, and visually ambiguous per-file ANM script IDs are
not automatically protocols and should not receive invented names merely to
drive a literal count to zero.
At typed call boundaries, guard every spelling of a fixed literal: a direct
integer, a C-style enum cast, and static_cast<Enum>(integer). Match the call
surface rather than banning casts globally, because casts from runtime packet
fields and calculated indices remain legitimate. Cover the accepted and
rejected spellings with a small regression test; otherwise a narrow regex can
report zero while a different cast form remains in production source.
For serialized operand families, introduce typed wire schemas only after the
opcode names are stable. Pin size, offset, width, padding, and signedness with
C_ASSERT. In a large /Ob0 interpreter, a compile-time field-index macro is
often safer than adding an args pointer or an accessor: derive the existing
resolver's numeric index with offsetof(Type, member) / 4, but keep the exact
resolver and bit-cast expression tree intact.
Do not assume two equivalent-looking float reads emit identically. In the TH08
RunEcl readability pass, replacing
*reinterpret_cast<f32 *>(&raw_i) with a generic raw-float macro shortened the
function by one byte (0x6B06 to 0x6B05). Restoring that precise bit-cast
tree inside the typed field macro recovered exact output. Enable one operand
family at a time, rebuild EclRun.obj, and compare the complete function before
moving to the next schema.