Original Morrowind Name observer
Source-only implementation on predecessor 6ca7f90522100f3474de6b5264364b7f96110b09.
No original guest, browser, Boat, canonical module build or transport replay ran.
No historical Codex-to-cox cause is inferred.
test/run.js --observe-morrowind-name enables the optional observer. Without
that exact flag the module is not loaded, no EIP wrapper is installed, no API
logger is armed and no observer output is produced. It rejects parallel workers
and an existing EIP trace. The supported execution context is the main instance;
worker-instance observations are not implemented. This flag is instrumentation,
not authorization to execute original media.
The import wrapper is installed before WebAssembly instantiation captures its
function references. Attachment follows stageAndLoadPe and precedes DLL guest
initializers. Attachment checks the complete original EXE size/SHA-256, six
relevant interpreter source blob identities and loaded instruction anchors.
Relocation is deliberately fail-closed: only verified preferred base 0x400000
is supported. These source checks do not prove a prebuilt wasm module's provenance;
a separately admitted full-runner acceptance must bind that module to this source.
The actual production COM ordinal decoder calls the observer after resolving
the API name. Every API entry/exit has a per-thread nested frame (depth limit32).
Matching IDirectInputDevice_GetDeviceData requires return 0x406926, stride20,
flags0, input count1 and the original caller's exact output stack layout. Each
frame retains its unique invocation ID, thread, device, return boundary and
builder generation. pollCallerReturn distinguishes UI poll caller 0x58072b
from other original callers such as 0x5adc52; only the UI caller can authorize
the translation/Name callback chain.
09b-dispatch.wat calls log_api_exit after result storage and nonvolatile
restoration, before 06b-core-handlers.wat returns to the run loop. This is the
output snapshot, before the next guest instruction. HRESULT and returned count
are always recorded; the five20-byte-event dwords are read only for HRESULT0
and count1. The subsequent log_eip(0x406926) confirms the same ESP/result before
the guest's HRESULT test. Repeated return-boundary hits cannot reuse a delivery;
a new poll entry invalidates the old token even if another diagnostic disables
the API logger. Trace arming forces block transfers through the run-loop desk
and disables tiers that would hide those block boundaries.
Builder return 0x5e672d captures EAX before the guest copies it into EDI.
Translation 0x5807c2 captures EAX, UI destination, shift and modifiers.
Callback 0x638050 additionally requires the same thread, poll token, character,
key event ID and Name target pointer. A callback consumes that token. A new
builder generation or observed submission terminates the old association.
The interior CALL at 0x6385e6 is not a block-entry hook. The supported
pre-write observation is setter entry 0x5726c0, requiring ECX=Name target and
saved return 0x6385eb. It occurs before any setter guest instruction and labels
both its actual PC and logical callsite. The post-setter snapshot at 0x6385eb
requires the same callback and setter ESP plus20 (ret 0x10, not ret 4). Raw
text object/pointer/length, up to31bytes and a bounded cursor position are read
directly. No guest text getter, write, input injection or replay occurs.
0x6385ff is a shared epilogue, reached both from rejection branches and after
a successful setter; its record explicitly does not alone prove rejection.
Limits are127 ordinary records plus one explicit overflow record, then no more observation. Bad reads or diagnostic sink failure stop observation without propagating an exception into guest execution. The API nesting limit also stops observation explicitly. There is no unbounded record queue.
Verification and limitations
Run the focused contracts with the original read-only executable:
node --max-old-space-size=320 tools/probes/morrowind-name-observer-contracts.js \
/READ_ONLY/installed/Morrowind.exe
The test executes the exact production IMPORT/ATTACH regions, complete h.log,
h.log_i32 and h.log_api_exit function source in isolation, recording the runner
and transformation hashes. It checks initializer-route ordering, uses the real
ordinal name decoder and the unchanged production byte reader against synthetic
mapped memory, and tests actual installed hook functions. These are
synthetic source contracts, not full-runner or original game execution.
Controls cover disabled defaults, matching/nested/other-caller invocations,
wrong thread/target/generation, failed HRESULT/zero/excess count with stale
outputs, loaded-code/source identity, relocation, pointers/length/truncation,
overflow, shared epilogue and return-token invalidation. A mutant accepting
failed HRESULT records is rejected. Delivered five-character, missing-d and
complete-then-changed fixtures retain distinct records.
Reuse predecessor20static anchors and22negative controls unchanged from
scratch/usage-watch/morrowind-name-consumer-cause-oct28/receipt.json; the static
probe and report were not edited or rerun. The old17-message transport group was
not run. Exact final contract hashes/results and resource/cleanup/publication
receipts live in the task canonical receipt.
Two observation limits remain. Builder generation and observed submission do
not prove arbitrary destruction/reuse lifetime; no destructor hook exists in
this observer. The next poll can sample text changed since the last setter,
but the available hooks cannot retain the first arbitrary later writer or
its PC. A changed sample reports firstWriter:null; an overwrite restored
before the sample is invisible. No synthetic control claims otherwise.
The first still-unobserved original boundary is the matching buffered keyboard
completion at 0x406926 itself. Next gate: independent acceptance on the exact
production runner and source-matched retained module, including initializers,
before any separately authorized original-game run. Full runtime readiness is
not established by this source-only task. A writer diagnostic or destructor
coverage needs a separate bounded assignment if required by that gate.