Documentation progress

This commit is contained in:
MaddoScientisto 2026-04-08 00:03:10 +02:00
commit 5cc5612f4e
14 changed files with 244 additions and 814 deletions

View file

@ -232,8 +232,57 @@ When manual class work starts, the safest order for this family is:
Do not start by forcing one complete 0x520-byte monolithic class.
## Live Ghidra Authoring Status
Verified first live `EntityDispatchEntry` shell batch landed on 2026-04-07.
- Created class owner `Remorse::EntityDispatchEntry` in the active `CRUSADER.EXE` database.
- Created provisional base datatype `/Remorse/EntityDispatchEntryBase` with the current stable field block through `+0x18`:
- `type_or_kind`
- `slot_index_or_count`
- `source_type`
- `event_type_or_list_ptr_lo`
- `group_id_byte`
- `link_or_state_word_0a..10`
- `target_farptr`
- `flags1`
- `flags2`
- Created provisional vtable datatype `/Remorse/EntityDispatchEntryVtable` with only the two currently verified callback slots exposed:
- `+0x14 = update_callback_slot14`
- `+0x28 = dispatch_callback_slot28`
- Kept the remaining vtable lanes explicit as unresolved padding instead of inventing slot names too early.
- Did not move methods yet. The current source note still anchors this family through the older `0008:` / `000d:` address notation, and those entrypoints are not yet mapped into the active live `CRUSADER.EXE` session strongly enough to justify owner moves by guesswork.
- The current live MCP `apply_class_layout(...)` path also rejected the minimal shell bind with an undocumented required `methods` property, so this first shell pass used the now-working live `run_write_script(...)` fallback to author the class namespace and datatypes directly.
This means the family is now started in-session rather than remaining note-only, but it is still in the pre-method phase.
That is no longer true after the next live pass on 2026-04-07.
- The older `0008:ba00` pilot cluster is now re-anchored in the live `CRUSADER.EXE` session as the `11e0:` process-substrate segment by direct offset mapping from the decompiler's embedded original-segment metadata.
- The first strong base-method batch is now moved under `Remorse::EntityDispatchEntry`:
- `11e0:0000` -> `InitBase` (older note anchor `0008:ba00`)
- `11e0:01b6` -> `SetSourceType` (older note anchor `0008:bbb6`)
- `11e0:0227` -> `SetEventTypeChecked` (older note anchor `0008:bc27`)
- `11e0:02a8` -> `SetGroupId` (older note anchor `0008:bca8`)
- `11e0:0353` -> `Unlink` (older note anchor `0008:bd53`)
- `11e0:0405` -> `IncrementGroupId` (older note anchor `0008:be05`)
- Those six methods now carry provisional `EntityDispatchEntryBase * this` signatures in-session plus decompiler comments recording the old `0008:` provenance so later note cleanup does not have to re-derive the mapping.
- The current live surface is still deliberately conservative. The decompiler still shows the underlying `struct_Process` substrate in several bodies, so this batch should be treated as class ownership plus field-layout alignment, not proof that every inherited process-style helper name is final.
- The next derived-family batch is now landed too. The older runtime-state pair from the note's `000d:` anchors is re-anchored in the live `1440:` fade/palette cluster by explicit decompiler segment metadata and matching offset delta:
- `1440:0000 FadeProcess_Create` -> `Remorse::EntityDispatchEntry::InitRuntimeState` (older note anchor `000d:7e00`)
- `1440:0278 FUN_1440_0278` -> `Remorse::EntityDispatchEntry::ReleaseRuntimeState` (older note anchor `000d:8078`)
- Created `/Remorse/EntityDispatchEntryRuntimeState` as a provisional overlay datatype. It preserves the stable base block through `+0x18`, keeps `+0x1a..+0x3f` explicit as unresolved subtype overlay space, and names the recovered runtime-state tail fields:
- `+0x40 = hold_token`
- `+0x41 = runtime_state_flag_41`
- `+0x42 = runtime_state_counter_42`
- `+0x44 = runtime_state_delta_44`
- `+0x46/+0x48 = owned_buffer_a`
- `+0x4a/+0x4c = owned_buffer_b`
- Those two runtime-state methods now carry provisional `EntityDispatchEntryRuntimeState * this` signatures and in-session comments tying them back to the older `000d:` evidence, which is enough to treat the runtime-state lane as class-authored rather than only documented.
## Questions To Close Later
- the remaining live `CRUSADER.EXE` method mapping for the note's old `0008:` / `000d:` anchors after the first `11e0:` base-method port and the `1440:` runtime-state port, especially `entity_word_list_destroy` and the timed/periodic constructors around old `0008:cefb` / `0008:d214`
- whether `+0x00` should be modeled as a literal `kind` field in all variants or only in some factory-built subtypes
- exact ownership split between the base object and the embedded surfaces at `+0x1e` and `+0x28`
- whether the seg126 startup/display subtype is truly part of the same inheritance family or only shares a lower-level dispatch-entry substrate

View file

@ -94,6 +94,7 @@ The live `CRUSADER.EXE` class-authoring lane is no longer just a plan.
Current authored `Remorse` classes in the active database are:
- `EntityDispatchEntry`
- `EntityVmOwnerResource`
- `EntityVmRuntime`
- `EntityVmContext`
@ -101,6 +102,10 @@ Current authored `Remorse` classes in the active database are:
The VM lane is still the furthest along in actual Ghidra authoring. Recent live batches added the bounded `EntityVmSlotEntry` class owner plus more owned `EntityVmRuntime` methods (`GetSlotChunkPtrAtOffset`, `ReleaseSlotChunkRef`, `TryUnloadSlotChunk`, `DebugDumpSlotMemory`, `ApplyToMatchingOwnerRows`) rather than stopping at free-function naming.
The next planned pilot family is no longer purely preparatory either. `Remorse::EntityDispatchEntry` now exists as a real class owner in-session with a first provisional `/Remorse/EntityDispatchEntryBase` datatype covering the stable field block through `+0x18` and a matching `/Remorse/EntityDispatchEntryVtable` datatype exposing only the verified `+0x14` and `+0x28` callback slots. The first base-method batch has also landed from the old `0008:` note cluster after re-anchoring that range onto the live `11e0:` process-substrate segment: `InitBase`, `SetSourceType`, `SetEventTypeChecked`, `SetGroupId`, `Unlink`, and `IncrementGroupId` now live under the class owner with provenance comments preserved.
That family also has its first derived slice now. The old `000d:7e00/8078` runtime-state pair is re-anchored in the live `1440:` fade/palette cluster as `InitRuntimeState` and `ReleaseRuntimeState`, and `/Remorse/EntityDispatchEntryRuntimeState` now exists as a provisional overlay datatype with the recovered `+0x40..+0x4c` runtime-state tail fields. That is a meaningful pause point because the pilot family now has a class owner, a base datatype, a vtable shell, a first base-method batch, and one concrete derived/runtime-state batch rather than just one isolated constructor lane.
The latest signature-recovery pass also tightened two of those runtime methods materially:
- `GetSlotChunkPtrAtOffset(runtime_farptr, slot_index, chunk_index, intra_chunk_offset)` now reads as a real slot-chunk accessor instead of a five-word anonymous wrapper.
@ -112,7 +117,11 @@ That VM-side gap is now closed too: `AcquireSlotForEntity` returns `EntityVmSlot
The next family switch has also landed in the live database: `Remorse::UsecodeDebuggerBreakState` now exists as a real class owner with a provisional `0x2f2` datatype and a stronger method batch (`Create`, `MaybeBreakOnCurrentLine`, `BreakpointInsertSorted`, `BreakpointRemove`, `HasBreakpoint`, `CallstackPushFrame`, `CallstackPushEntry`, `CallstackPopEntry`, `EnableSingleStep`, `ClearStepState`, `CurrentEntryGetUnitName`).
That debugger family is no longer just a top-level shell. The internal record shapes are now recovered and applied live well enough to treat the two tables as real fixed-size arrays in-session: breakpoint entries are `0x0b` bytes with `unit_name_inline[9] + line_number`, and callstack entries are `0x15` bytes with `unit_name_inline[9]` plus the currently safest trailing fields `source_stream_target_farptr`, `current_frame_payload_farptr`, and still-neutral `aux_farptr`.
That debugger family is no longer just a top-level shell. The internal record shapes are now recovered and applied live well enough to treat the two tables as real fixed-size arrays in-session: breakpoint entries are `0x0b` bytes with `unit_name_inline[9] + line_number`, and callstack entries are `0x15` bytes with `unit_name_inline[9]` plus the currently safest trailing fields `source_stream_cursor_farptr`, `current_frame_payload_farptr`, and still-neutral `aux_farptr`.
The next debugger pass tightened the bounded helper and callback edges too. `1408:0230` now lives under `Remorse::UsecodeDebuggerBreakState::BreakpointFindFirstForUnitAtOrAfterLine` instead of as an anonymous seg1408 helper, and the retail vtable root at `1478:65ab` is no longer a blind spot: slot 0 resolves to `OnBreakTriggeredNoop` and slot 1 resolves to `VtableSlot1ReturnZero`, which keeps the class surface honest about the shipped build's inert break callbacks while leaving non-retail behavior open.
The follow-up seg109 consumer pass is also done now. `13a0:0291` and its local helper `13a0:045c` are documented in-session as the first concrete consumers of the current callstack entry's trailing payload: they read entry `+0x09` as a descriptor/source-stream cursor and entry `+0x0d` as the current-frame payload context while formatting debugger dump/watch text. That cursor name is now promoted into the live `/Remorse/UsecodeDebuggerCallstackEntry` datatype and the `CallstackPushFrame` signature too, which means the remaining open callstack question is mostly the unused `aux_farptr` lane rather than the first two dwords.
The VM lane also advanced one more selective step without overpromoting inheritance: `Remorse::EntityVmContext::CreateFromSlotIndex` now has a caller-backed mixed parameter pack (`owner_source_farptr`, `pitemno_farptr`, `mode_flags`, `slot_index`, `value_add_offset`, `intra_chunk_offset`, `ucparam_farptr`, `ucparamsize`) and an explicit far return restored in `AX:DX`, even though the current live endpoint still textualizes that repaired signature conservatively as plain `dword __cdecl`.

View file

@ -0,0 +1,146 @@
# `Using map patch file.` in retail `CRUSADER.EXE`
This note records the current evidence-backed read of the startup line:
`Using map patch file.`
Short version:
- the line is printed during `Init_Everything` startup
- it is gated by a file-existence check for `static\fixed.dat`
- it does **not** mean the `-u` usecode override is active
- it means the game found an alternate fixed-map archive and will prefer that archive for later fixed-map loading
- current recovered behavior looks like `replacement-preferred fixed-map source`, not `merge a few patched records into the base archive`
## Exact Startup Print Site
The live exported Ghidra C for `Init_Everything` at `1048:039b` shows this sequence:
1. `LoadConfigFile()`
2. `Init_CheckFreeDiskSpace()`
3. `File_Exists(s_static_fixed_dat)`
4. if present, `ConsolePrintf(..., 0x9cc, 0x78)`
5. print the generic loading/progress strings
The relevant string symbols in the live export are:
- `1478:09bb` = `s_static\fixed.dat`
- `1478:09cc` = `s_Using_map_patch_file.`
- `1478:09e4` = `s_Loading:_[`
- `1478:09ef` = `s__]`
- `1478:09fd` = `s_dot`
So the startup line is not speculative or inferred from neighboring strings. The init code really does:
- check whether `static\fixed.dat` exists
- and print `Using map patch file.` only when that file is present
## What The File Is
The nearby fixed-data loader path now closes this pretty tightly.
`ItemCacheFixedDat_Init()` is called later in startup from the same `Init_Everything` body. That wrapper immediately calls `ItemCache_InitAndLoadFixedDat()` at `10a8:163a`.
`ItemCache_InitAndLoadFixedDat()` does all of the following:
- initializes item data
- loads the normal base fixed-map archive through `FixedDat_LoadData()` at `10b8:0616`
- checks again for `static\fixed.dat`
- if present, loads that second archive into `DAT_1478_1064`
- resets `g_currentMapArray = 0xffff`
`FixedDat_LoadData()` itself resolves the standard asset path from `g_fixedDatFilenamePtr` and opens the regular `fixed.dat` resource.
That means the printed line is specifically about an alternate fixed-map archive, not a config file, not a save file, and not a usecode package.
## What It Does At Runtime
The strongest currently recovered consumer is the fixed-map load path in the same item-cache family.
In the exported Ghidra C, the map-load logic does this:
- if `DAT_1478_1064 == 0`, use `DAT_1478_1060` (the normal `fixed.dat` handle)
- otherwise use `DAT_1478_1064` (the `static\fixed.dat` handle)
That is stronger than a vague `patch support` claim. The current recovered shape is:
- base `fixed.dat` is always loaded
- `static\fixed.dat` is conditionally loaded when present
- later fixed-map reads prefer the `static\fixed.dat` handle when it exists
Current safest interpretation:
- the message means the game found an alternate fixed-map archive under `STATIC\FIXED.DAT`
- subsequent fixed-object/map loading will use that archive as the active source
## Merge Or Fallback Status
Current evidence does **not** show a merge path.
What is directly recovered:
- the game keeps two archive handles: base `DAT_1478_1060` and optional patch `DAT_1478_1064`
- the main fixed-map load path chooses exactly one handle before reading the map entry
- if `DAT_1478_1064` is non-null, that chooser uses it instead of `DAT_1478_1060`
- no currently recovered outer code does `try patch, then fall back to base if missing`
- no currently recovered outer code reads both archives and concatenates item lists
So the strongest current read is:
- `static\fixed.dat` is a replacement-preferred archive source
- not a recovered additive overlay system
That means the current evidence does **not** support claims like:
- `missing map entries in static\fixed.dat are automatically read from base fixed.dat`
- `items missing from the patch archive are filled from the base archive`
- `the patch archive appends extra records onto the base archive at load time`
Could there still be fallback hidden inside the archive object's internal methods? Possibly, but there is no evidence of that in the currently recovered call shape. The visible loader logic selects one archive handle up front and passes only that handle into the read methods.
Current safest practical conclusion:
- if `static\fixed.dat` is present, the game behaves like it is loading maps from that archive instead of the base archive
- using it to `add a few extra things to the base one` is not a recovered supported mode
- using it as a full replacement archive is the behavior the current code most clearly supports
## What It Probably Does Not Mean
Current evidence argues against a few common misreads.
It is **not** currently the `-u` startup override:
- the retail `-u` lane targets the EUSECODE runtime root, documented separately in `docs/usecode-startup-override.md`
- this startup print is in the fixed-map/archive lane instead
It is also **not** currently evidenced as a merge-style overlay:
- the recovered map-load chooser selects one handle or the other
- this consumer does not show `check patch archive first, then fall back per-record to base`
So the safest current wording is `replacement-preferred alternate FIXED.DAT source`, not `per-entry patch merge`.
## Practical Meaning
When retail No Remorse prints `Using map patch file.` at startup, it means:
- `STATIC\FIXED.DAT` was found
- the game has enabled its alternate fixed-map archive lane
- later fixed map/object loads will prefer that archive over the normal `FIXED.DAT`
In plain terms: the executable is telling you it found a replacement/patch copy of the world-map data archive and is going to load map content from that patched archive path.
## Evidence Summary
- `1048:039b Init_Everything` checks `File_Exists("static\\fixed.dat")` and prints `1478:09cc` only on success
- `10a8:163a ItemCache_InitAndLoadFixedDat` loads base `fixed.dat` plus optional `static\\fixed.dat`
- `10b8:0616 FixedDat_LoadData` resolves the standard `fixed.dat` path from `g_fixedDatFilenamePtr`
- recovered fixed-map load logic prefers `DAT_1478_1064` (`static\\fixed.dat`) over `DAT_1478_1060` (base `fixed.dat`) when the optional archive is present
## Ghidra Annotation Status
The live-session note to add in Ghidra is straightforward:
`Init_Everything startup status print: if static\\fixed.dat exists, print "Using map patch file.". Later ItemCache_InitAndLoadFixedDat loads that archive into DAT_1478_1064, and the fixed-map load path prefers DAT_1478_1064 over the base fixed.dat handle DAT_1478_1060.`
In this session the live comment could not be pushed automatically because the reachable MCP bridge endpoints for the open GUI session were unavailable and the local PyGhidra writer path is blocked by the project lock while Ghidra owns the project.

View file

@ -144,9 +144,9 @@ The live debugger-state model is now strong enough to split the old table blobs
| Offset | Current name | Confidence | Current meaning |
|---|---|---|---|
| `+0x00..+0x08` | `unit_name_inline[9]` | High | Inline unit-name buffer for the active frame. |
| `+0x09` | `source_stream_target_farptr` | Medium | Far pointer derived from the interpreter source-stream lane plus one fetched word in the only verified caller. |
| `+0x0d` | `current_frame_payload_farptr` | Medium | Far pointer to the current frame payload at `frame_base + 0x04` in the only verified caller. |
| `+0x11` | `aux_farptr` | Low | Trailing auxiliary far pointer slot; still zero in the only verified caller. |
| `+0x09` | `source_stream_cursor_farptr` | High | Far pointer to the current debugger/source descriptor stream cursor. `Debugump_13a0_0291` passes this lane into `13a0:045c`, which reads descriptor bytes and inline strings directly from it. This field name is now promoted into the live datatype in `CRUSADER.EXE`, not just the note set. |
| `+0x0d` | `current_frame_payload_farptr` | High | Far pointer to the current frame payload/evaluation context at `frame_base + 0x04`. `13a0:045c` dereferences this lane while formatting debugger dump/watch text. |
| `+0x11` | `aux_farptr` | Low | Trailing auxiliary far pointer slot; still zero in the only verified `CallstackPushFrame` caller and still lacks a confirmed current-entry consumer. |
## Current Working Layout
@ -176,6 +176,7 @@ Verified first live class batch landed on 2026-04-06.
- `1408:0053` -> `MaybeBreakOnCurrentLine`
- `1408:00dd` -> `BreakpointInsertSorted`
- `1408:01a5` -> `BreakpointRemove`
- `1408:0230` -> `BreakpointFindFirstForUnitAtOrAfterLine`
- `1408:029e` -> `HasBreakpoint`
- `1408:02f5` -> `CallstackPushFrame`
- `1408:03b0` -> `CallstackPushEntry`
@ -183,11 +184,14 @@ Verified first live class batch landed on 2026-04-06.
- `1408:0419` -> `EnableSingleStep`
- `1408:0432` -> `ClearStepState`
- `1408:0444` -> `CurrentEntryGetUnitName`
- Promoted the remaining bounded breakpoint-table helper `1408:0230` under the class owner as `BreakpointFindFirstForUnitAtOrAfterLine`; it now reads as the lower-bound search over `breakpoint_entries` for a given `(unit_name, line_number)` query instead of an anonymous seg1408 utility.
- Resolved the retail debugger vtable root at `1478:65ab` one step further: slot 0 points to `1408:046f` and slot 1 points to `1408:0474`, and both entries are now class-owned as `OnBreakTriggeredNoop` and `VtableSlot1ReturnZero` with explicit comments that the shipped retail implementations are inert stubs.
- Tightened the live method signatures to explicit object-style forms, including:
- `UsecodeDebuggerBreakState * __cdecl16far Create(UsecodeDebuggerBreakState * this, dword init_spec)`
- `void __cdecl16far MaybeBreakOnCurrentLine(UsecodeDebuggerBreakState * this, word current_line)`
- `byte __cdecl16far BreakpointInsertSorted(UsecodeDebuggerBreakState * this, dword unit_name_farptr, word line_number)`
- `void __cdecl16far BreakpointRemove(UsecodeDebuggerBreakState * this, dword unit_name_farptr, word line_number)`
- `int __cdecl16far BreakpointFindFirstForUnitAtOrAfterLine(UsecodeDebuggerBreakState * this, dword unit_name_farptr, word line_number)`
- `uint __cdecl16far HasBreakpoint(UsecodeDebuggerBreakState * this, dword unit_name_farptr, word line_number)`
- `void __cdecl16far CallstackPushFrame(UsecodeDebuggerBreakState * this, dword unit_name_farptr, dword source_stream_target_farptr, dword current_frame_payload_farptr, dword aux_farptr)`
- `byte __cdecl16far CallstackPushEntry(UsecodeDebuggerBreakState * this, dword unit_name_farptr)`
@ -195,6 +199,11 @@ Verified first live class batch landed on 2026-04-06.
- `void __cdecl16far EnableSingleStep(UsecodeDebuggerBreakState * this)`
- `void __cdecl16far ClearStepState(UsecodeDebuggerBreakState * this)`
- `dword __cdecl16far CurrentEntryGetUnitName(UsecodeDebuggerBreakState * this)`
- The only verified `CallstackPushFrame` caller at `1418:051d` is now constrained a little more tightly in-session too: the trailing payload is still best kept as `source_stream_target_farptr`, `current_frame_payload_farptr`, and `aux_farptr`, but the raw stack setup now confirms that the last slot is literal zero in retail, the middle slot is pushed from `frame_base + 0x04`, and the first slot is pushed from the interpreter `+0xd6/+0xd8` source-stream lane plus a local offset.
- The next seg109 consumer pass closed the main follow-up from that batch. `13a0:0291 Debugump_13a0_0291` now has a decompiler comment showing that it resolves the current callstack entry as `this + 0x67 + depth * 0x15`, passes entry `+0x09` as the descriptor/source-stream cursor, and passes entry `+0x0d` as the frame payload context into `13a0:045c`.
- `13a0:045c FUN_13a0_045c` now also carries a decompiler comment recording the same split from the consumer side: it reads descriptor bytes and inline strings directly from the `+0x09` lane and dereferences the `+0x0d` lane as the evaluation context while formatting debugger dump/watch text into the shared output buffer at `0x45a6`.
- The `CallstackPushFrame` comment in seg1408 was updated to reflect that narrower live read: `+0x09` is no longer just a generic source-derived far pointer, but a real source-stream cursor used by the seg109 formatter path.
- That naming decision is now applied live too. `/Remorse/UsecodeDebuggerCallstackEntry` now exposes `source_stream_cursor_farptr` at offset `+0x09` with a matching field comment in the datatype manager, and `CallstackPushFrame` now uses `source_stream_cursor_farptr` in its parameter list instead of the older `source_stream_target_farptr` wording.
- Added decompiler comments on the breakpoint and callstack helpers so the recovered inline-record layout is visible in-session even before every field is formally typed.
- Added decompiler comments on the only verified `Interpreter_NextUsecodeOp` caller of `CallstackPushFrame`, which confirms the current live read of the three trailing callstack dwords:
- `source_stream_target_farptr` is source-stream-derived from the interpreter `+0xd6/+0xd8` lane plus one fetched word
@ -204,15 +213,16 @@ Verified first live class batch landed on 2026-04-06.
## Current Cautions
- The retail instantiation path still appears absent; no normal caller currently reaches `Create` in the unpatched retail binary.
- The record boundaries inside both tables are now landed in the live datatype, and two of the three trailing callstack dwords now have caller-backed structural names. The exact gameplay role behind those two far pointers is still only partly recovered.
- The record boundaries inside both tables are now landed in the live datatype, and the first two trailing callstack dwords now have both producer-side and seg109 consumer-side structural evidence plus live datatype names/comments. The remaining uncertainty is no longer whether those lanes are meaningful, but whether they should ever become more subsystem-specific than `source_stream_cursor_farptr` and `current_frame_payload_farptr`.
- The retail vtable root is no longer an open slot-map question for the first two entries. What remains open is whether any non-retail or UI-side build ever installed non-stub behavior behind those same callback positions.
- `init_spec` on `Create` and `unit_name_farptr` on the breakpoint/callstack helpers are intentionally neutral names; the live signatures are object-correct, but the payload semantics should stay conservative.
## Best Next Moves
1. Identify the real gameplay semantics of `source_stream_target_farptr` and `current_frame_payload_farptr` from the interpreter-side caller lanes before promoting subsystem-specific names.
2. Identify the vtable callback slots used by `MaybeBreakOnCurrentLine` and decide whether one or two additional methods belong on the class owner.
3. Cross-check the seg1408 class note against the interpreter callback site at `1418:04b5` so the dormant-orphan lifecycle remains explicit in the live notes.
4. Decide whether `aux_farptr` should remain neutral or can be promoted after one more caller or consumer pass.
1. Cross-check the seg1408 class note against the interpreter callback site at `1418:04b5` so the dormant-orphan lifecycle remains explicit in the live notes now that slot 0 is confirmed to land on an inert retail stub.
2. Decide whether `aux_farptr` should remain neutral or can be promoted after one more caller or consumer pass.
3. Decide whether `13a0:0291` and `13a0:045c` are ready for stable debugger-UI names or should remain comment-backed until another direct caller or string anchor lands.
4. If the debugger family stalls there, switch to the next planned class-lift family instead of overworking this orphaned subsystem.
## Bottom Line