Crusader_Decomp/docs/remorse-class-lift-index.md
2026-04-12 14:45:08 +02:00

20 KiB

That aux_farptr lane also survived one more direct caller pass without widening. CallstackPushFrame still has only the retail 1418:051d Interpreter_NextUsecodeOp caller, that caller still pushes literal zero for the trailing field, and the current seg109 consumers still read only +0x09 and +0x0d, so the field remains intentionally neutral rather than weakly renamed.

That first SpriteNode batch now includes datatype authoring too. /Remorse/SpriteNodeBase and /Remorse/SpriteNodeVtable both exist live with the current safest base offsets and event-slot anchors, so the family is no longer just a method-owner shell.

The constructor side has now started too. 1360:036a lives as Remorse::SpriteNode::Create with an explicit comment that preserves the remaining caveat: this is the current safest constructor-style anchor because it allocates 0x34 bytes, stamps the 0x501a vtable, and initializes the core node lanes, but it may still prove to be a higher derived/UI wrapper once more subtype evidence lands. The main remaining SpriteNode gap is therefore the live GetOrTraverse anchor, not whether the family has any constructor surface at all.

That traversal gap is now closed too. 1360:0955 is live as Remorse::SpriteNode::GetOrTraverse, with a comment recording that it recurses through the child-linked subtree, adjusts query coordinates by the local offsets, and returns either the matched child node or the default sentinel through the out pointer. The remaining SpriteNode questions are now the constructor-wrapper split and the deeper slot/layout story rather than missing core method anchors.

The next bounded family after SpriteNode is started too. Remorse::CacheBackendObject now exists live with 1250:0000 promoted as Create, backed by the decompiler's explicit old 0009:5600 segment metadata and a comment recording the 0x20-byte object allocation plus file-handle/method-table initialization path. That family is still only at its constructor shell, but it is no longer inventory-only.

That broader Tier 1 sweep is now complete too. EntityVmOwnerResource is no longer just a create/destroy shell: the outer wrapper model is corrected to a 0x14-byte file-backed object with helper vtable at +0x08 and materialized owner-row table at +0x0c, and the two adjacent 1430: wrappers now live under the class as QueryMaterializationSize and MaterializeChecked. CacheBackendObject also moved beyond its constructor shell: 1250:026c is now LoadEntryTableFromManifest, 1250:0749 is now InitFixedEntryTable, and the live decompiler now supports a tighter 0x20-byte layout read around the +0x10/+0x14/+0x16/+0x18/+0x1c entry-table lanes. SpriteNode slot work tightened as well: DispatchEvent now maps event codes 1/2/4/8/0x10/0x20/0x40/0x100 onto concrete vtable offsets instead of the earlier generic A/B/C/D placeholders.

The next broader shortlist is now partially started as well. PresentationCallbackBroker has its first live foothold in CRUSADER.EXE: 12d0:0513 and 12d0:0656 now live under Remorse::PresentationCallbackBroker::{InitOnce,TeardownOnce} with comments tied to the 0x4588 lifecycle globals. The same pass also clarified what is not ready yet: first-pass live searches for WatchEntityController (0x2c2b, 0x2be4, 0x39ca, 0x0219) and DialogMenuObject (0x28b5, 0x27ca, 0x2843) hit camera/process and controller-save false positives rather than safe reanchors, so those two families remain documented candidates but not yet live-authored classes.

That next-pass follow-up is now tighter too. The raw WatchEntityController lane no longer needs to stay completely abstract: its 0007:ba00/ba45 create pair maps onto the live Camera_Init / Camera_CreateProcess cluster at 1180:0000/0045, and those functions now carry provenance comments instead of forcing a weaker duplicate rename over the clearer camera-process naming. DialogMenuObject remains the one compact family in this batch that still lacks a safe live re-anchor: second-pass searches on the obvious 0x28b5/0x27ca/0x2843 leads still landed on unrelated process/save helpers, so that family stays note-backed but not yet live-authored. PresentationCallbackBroker also has its first non-method live adjunct now: 1288:0fc3 is renamed allocator_phase_finalize_pass with a comment recording the two broker slot +0x08 calls before allocator-head sweep, and two slot +0x0c caller sites (1278:0616, 1320:1588) now carry caller-evidence comments in-session. CacheBackendObject advanced another bounded step as well: 1250:0910 now lives under the class as SetEntryNameAndTag, and the latest SpriteNode caller pass supports treating Create as the compact shared node constructor used by the larger GumpCreate_* wrapper family.

Remorse Class-Lift Work Index

Purpose

This note is the easy-to-find landing page for the current Remorse C++ and class-lifting preparation work.

Use it as the starting point when the project returns to:

  • class and namespace authoring inside Ghidra
  • vtable and instance-layout promotion
  • hand-maintained C++ skeleton emission
  • ABI-safe source reconstruction planning

This index does not replace the detailed notes. It groups them into one work order so later implementation can resume quickly.

Read This First

  1. docs/remorse-cpp-decompilation-plan.md
  2. docs/remorse-class-candidate-inventory.md
  3. docs/remorse-rebuild-abi-notes.md
  4. docs/ghidra-mcp-class-lifting-endpoint-spec.md

That set gives the high-level target, the current candidate families, the rebuild constraints, and the future MCP authoring surface.

Current Note Groups

1. Overall Direction

2. Candidate Inventory And Tooling Surface

3. Family-Specific Layout Notes

4. Execution Checklists

Stage 1: Keep The Evidence Model Honest

  1. Re-read the plan, ABI note, and candidate inventory.
  2. Pick one family with bounded ambiguity.
  3. Confirm ctor, dtor, vtable root, and stable field groups before any class ownership changes in Ghidra.

Best current pilot families:

  1. EntityDispatchEntry
  2. SpriteNode
  3. EntityVmOwnerResource

Entity remains a top-priority family, but it should be split deliberately rather than promoted as one giant class too early.

Stage 2: Ghidra Authoring Pass

  1. Create class or namespace owners.
  2. Move only strongly owned methods first.
  3. Create provisional instance structs and vtable structs.
  4. Preserve slot order and unresolved fields instead of trying to beautify them.

The future MCP endpoint sequence should follow the spec note rather than ad hoc scripting.

Stage 3: First C++ Skeleton Slice

  1. Emit one header/source pair for the pilot family.
  2. Build it against the compatibility layer rather than raw host C++ alone.
  3. Keep unresolved offsets as named placeholders instead of collapsing them into speculative semantics.
  4. Record which parts are Track A safe versus Track B convenience-only.

Immediate Follow-Ups Still Open

  1. Convert the first family note into a hand-maintained C++ skeleton once the compatibility header is accepted.
  2. Implement the MCP class/vtable authoring endpoints only after the workflow and note set above are stable enough to drive them.
  3. Add one more dedicated note for the callback/object lane around 0x4588 only if later caller evidence supports a stronger subsystem name than PresentationCallbackBroker.
  4. Turn the first-class-authoring checklist into a completed execution log once the first real MCP batch lands.

Broader Sweep Shortlist

The broader class-identification pass is now narrow enough to be explicit.

Current best near-term shortlist after the recent live authoring work:

  1. WatchEntityController
  2. DialogMenuObject
  3. PresentationCallbackBroker
  4. deeper CacheBackendObject follow-up around 1250:0910
  5. SpriteNode subtype/layout follow-up beyond the recovered event-slot map
  6. broader Entity family partitioning

That ordering is deliberate:

  • the old Tier 1 set is now complete, and the next pass already resolved part of the follow-up list
  • PresentationCallbackBroker still belongs on the map, but its subsystem label remains weaker than its mechanics even after the first live reanchor
  • the last two items stay in view because they now have live footholds and cleaner remaining follow-up than they did before

Current Live Authoring Snapshot

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
  • EntityVmSlotEntry
  • NPCActionProcess
  • StandProcess
  • PaceProcess
  • SurrenderProcess
  • GuardProcess
  • LoiterProcess

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 new bounded NPC-family batch is intentionally lighter on datatypes than the VM and dispatch-entry work, but it is still real class lifting rather than mere renaming. The live database now has owner-first class shells for the seg033 AI-process family with NPCActionProcess as the shared base owner and StandProcess, PaceProcess, SurrenderProcess, GuardProcess, and LoiterProcess as derived behavior owners. The safest current stop point is still owner-first only: shared create/destroy/no-op virtual entries and the direct per-family create/run/destroy methods are lifted, while datatype and slot-order work remain open until the process-state layout and vtable roots are tighter.

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.

That pause point has moved again. The timed/periodic derived branch is now partially lifted in-session too: ConstructVtable3AD2, ConstructVtable3AA6, SetUpdatePeriodAndReschedule, TickPeriodic, EnableActiveCounters, and DisableActiveCounters are now owned by Remorse::EntityDispatchEntry in the live 11e0: substrate segment with short 0008: provenance comments preserved.

The earlier word-list blocker on that same family is now closed by a re-anchor correction rather than by local boundary repair. The old 0008:da00..dfa1 word-list lane does not live in the dead 11e0:2000..25a1 window after all; it reappears as the live 11e8: MList_* cluster, and that full batch is now also owned under Remorse::EntityDispatchEntry: SetWordList0408Terminated, FreeWordList, Destroy, EnsureWordListContains, AppendUniqueWord, RemoveWordValue, GetWordAt, SetWordAt, and FindUnflaggedWordById10. The main remaining EntityDispatchEntry question is therefore no longer "where did the word-list methods go," but whether that 11e8: subtype should eventually become its own explicit derived/overlay class in the datatype model.

The next family switch has now started for real too. Remorse::SpriteNode exists live in CRUSADER.EXE, and its first bounded 1360: batch is no longer just a note: Destroy, IsDirty, MarkDirty, DispatchEvent, and UpdateAndDispatch are now class-owned with short 000b: provenance comments. That shifts the SpriteNode family from note-only planning into the same live-authoring lane as the earlier Remorse pilots, while still keeping the constructor side and GetOrTraverse mapping deliberately open.

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.
  • ApplyToMatchingOwnerRows(runtime_farptr, slot_index_filter, chunk_index_filter) now reads as a real iterator/filter helper instead of a split-word scratch signature.

The next live batch pushed that further still: most of the EntityVmRuntime method cluster now carries an explicit 4-byte EntityVmRuntime * this in-session, including Create. The main remaining type gap inside that class is no longer the runtime object itself, but the exact far slot-entry pointer positions on AcquireSlotForEntity and InitSlotOwnerBuffers.

That VM-side gap is now closed too: AcquireSlotForEntity returns EntityVmSlotEntry *32 in DX:AX, InitSlotOwnerBuffers now accepts EntityVmSlotEntry *32, EntityVmOwnerResource::{Create,Destroy} now carry explicit 4-byte this, and the simple EntityVmContext lifecycle methods now do the same.

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_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.

That formatter consumer lane is no longer just comment-backed either. The two seg109 helpers now have stable live names in CRUSADER.EXE: 13a0:0291 is usecode_debugger_format_expression_to_shared_buffer, and 13a0:045c is usecode_debugger_format_descriptor_expression. That keeps the debugger-family residue focused where it belongs now: mainly the still-neutral aux_farptr lane and any later evidence for non-retail callback behavior, not anonymous formatter plumbing.

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.

Bottom Line

The current prep work is now large enough that it should be treated as one coordinated lane rather than scattered notes.

Use this file as the resume point for future class-lift and C++ reconstruction work.