10 KiB
Usecode Debugger Break-State Layout
Purpose
This note captures the current class-lift-relevant evidence for the dormant seg1408 debugger-state object.
The retail binary still appears to leave this family orphaned at runtime, but the object model itself is strong enough to justify explicit class authoring in Ghidra.
Current working family name:
UsecodeDebuggerBreakState
Current Best Class-Level Read
UsecodeDebuggerBreakState is a retained debugger object that owns:
- a small breakpoint table
- current interpreted-line state
- single-step / break-armed flags
- a callstack entry stack
- a small vtable-backed break/notify surface used by the interpreter callback lane
The compiled interpreter still calls into this object when the global debugger-state pointer is non-null, even though the retail binary no longer seems to instantiate it during normal play.
Strongest Evidence Anchors
Constructor
1408:0000 Create
Current verified behavior:
- allocates
0x2f2bytes whenthis == null - writes retail vtable offset
0x65abat object+0x00 - fills the breakpoint-entry region starting at
+0x04with0xffff - clears
+0x02,+0x75, and+0x7a - returns the object far pointer in
DX:AX
This is a real constructor-style path, not just a helper wrapper.
Break gate
1408:0053 MaybeBreakOnCurrentLine
Current verified behavior:
- stores the incoming interpreted line minus one at
+0x72 - resolves the current unit-name pointer through
CurrentEntryGetUnitName - checks file+line breakpoints through
HasBreakpoint - dispatches through the object vtable when a break condition is met
This is the strongest proof that the hidden debugger lane is object-based and that the interpreter-side callback still expects a live debugger object.
Breakpoint table helpers
1408:00dd BreakpointInsertSorted
Current verified behavior:
- enforces a maximum of ten breakpoint entries
- scans the
0x0b-byte breakpoint-entry table rooted at+0x04 - compares unit-name strings via the common string helper
- inserts a new
(unit_name, line_number)pair into the sorted table
1408:01a5 BreakpointRemove
Current verified behavior:
- scans the same
0x0b-byte breakpoint-entry table for an exact(unit_name, line_number)match - compares the stored inline name bytes first, then the stored line word at entry
+0x09 - compacts the remaining entries downward when a match is found
- decrements
breakpoint_count
1408:029e HasBreakpoint
Current verified behavior:
- scans the same breakpoint-entry table
- compares the requested line number against entry
+0x09 - compares the requested unit-name pair against the stored name bytes
- returns a boolean-style
uintinAX
Callstack helpers
1408:02f5 CallstackPushFrame
Current verified behavior:
- computes the current callstack-entry base as
this + 0x7c + callstack_depth * 0x15 - copies an inline unit-name buffer into entry
+0x00 - enforces a maximum visible unit-name length of eight characters plus terminator
- stores three trailing far-pointer/state dwords at
+0x09,+0x0d, and+0x11 - increments
callstack_depth
1408:03b0 CallstackPushEntry
Current verified behavior:
- uses
+0x7aas the current callstack depth - acts as a thinner wrapper over
CallstackPushFramewhen only the inline unit-name payload matters - increments the depth and asserts when it reaches
0x1e
1408:03f7 CallstackPopEntry
Current verified behavior:
- decrements
+0x7a - asserts if the depth underflows below zero
Step-state helpers
1408:0419 EnableSingleStep
- clears
+0x76/+0x78 - sets
+0x75 = 1
1408:0432 ClearStepState
- clears
+0x74 - clears
+0x75
Current-entry name accessor
1408:0444 CurrentEntryGetUnitName
Current verified behavior:
- returns null when
callstack_depth <= 0 - otherwise returns a far pointer to the current callstack entry's inline unit-name buffer
Recovered Entry Schemas
The live debugger-state model is now strong enough to split the old table blobs into concrete fixed-size entry records.
UsecodeDebuggerBreakpointEntry (0x0b bytes)
| Offset | Current name | Confidence | Current meaning |
|---|---|---|---|
+0x00..+0x08 |
unit_name_inline[9] |
High | Inline unit-name buffer, consistent with eight visible characters plus terminator. |
+0x09 |
line_number |
High | Breakpoint line number compared by BreakpointInsertSorted, BreakpointRemove, and HasBreakpoint. |
UsecodeDebuggerCallstackEntry (0x15 bytes)
| 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. |
Current Working Layout
The live datatype /Remorse/UsecodeDebuggerBreakState now exists in-session with the currently safest anchors:
| Offset | Current name | Confidence | Current meaning |
|---|---|---|---|
+0x00 |
vtable_offset |
High | Retail debugger-state vtable offset 0x65ab. |
+0x02 |
breakpoint_count |
High | Count of 0x0b-byte breakpoint entries. |
+0x04..+0x71 |
breakpoint_entries[10] |
High | Ten inline UsecodeDebuggerBreakpointEntry records. |
+0x72 |
current_line |
High | Current interpreted line minus one. |
+0x74 |
break_armed |
Medium | Break/armed flag cleared by ClearStepState. |
+0x75 |
single_step_enabled |
High | Single-step flag set by EnableSingleStep. |
+0x76/+0x78 |
step_state_lo/hi |
Medium | Step-state pair cleared by EnableSingleStep. |
+0x7a |
callstack_depth |
High | Current callstack depth. |
+0x7c..+0x2f1 |
callstack_entries[30] |
High | Thirty inline UsecodeDebuggerCallstackEntry records. |
Live Ghidra Authoring Status
Verified first live class batch landed on 2026-04-06.
- Created class owner
Remorse::UsecodeDebuggerBreakState. - Created
/Remorse/UsecodeDebuggerBreakpointEntry(0x0b) and/Remorse/UsecodeDebuggerCallstackEntry(0x15) in the live data-type manager. - Rewrote
/Remorse/UsecodeDebuggerBreakStatein-session so the old blob regions are now explicitUsecodeDebuggerBreakpointEntry[10]andUsecodeDebuggerCallstackEntry[30]arrays at the recovered offsets. - Moved the main seg1408 helpers under the class owner:
1408:0000->Create1408:0053->MaybeBreakOnCurrentLine1408:00dd->BreakpointInsertSorted1408:01a5->BreakpointRemove1408:029e->HasBreakpoint1408:02f5->CallstackPushFrame1408:03b0->CallstackPushEntry1408:03f7->CallstackPopEntry1408:0419->EnableSingleStep1408:0432->ClearStepState1408:0444->CurrentEntryGetUnitName
- 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)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)void __cdecl16far CallstackPopEntry(UsecodeDebuggerBreakState * this)void __cdecl16far EnableSingleStep(UsecodeDebuggerBreakState * this)void __cdecl16far ClearStepState(UsecodeDebuggerBreakState * this)dword __cdecl16far CurrentEntryGetUnitName(UsecodeDebuggerBreakState * this)
- 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_NextUsecodeOpcaller ofCallstackPushFrame, which confirms the current live read of the three trailing callstack dwords:source_stream_target_farptris source-stream-derived from the interpreter+0xd6/+0xd8lane plus one fetched wordcurrent_frame_payload_farptris current-frame-derived from theframe_base + 0x04laneaux_farptris still zero in the only verified caller
Current Cautions
- The retail instantiation path still appears absent; no normal caller currently reaches
Createin 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.
init_speconCreateandunit_name_farptron the breakpoint/callstack helpers are intentionally neutral names; the live signatures are object-correct, but the payload semantics should stay conservative.
Best Next Moves
- Identify the real gameplay semantics of
source_stream_target_farptrandcurrent_frame_payload_farptrfrom the interpreter-side caller lanes before promoting subsystem-specific names. - Identify the vtable callback slots used by
MaybeBreakOnCurrentLineand decide whether one or two additional methods belong on the class owner. - Cross-check the seg1408 class note against the interpreter callback site at
1418:04b5so the dormant-orphan lifecycle remains explicit in the live notes. - Decide whether
aux_farptrshould remain neutral or can be promoted after one more caller or consumer pass.
Bottom Line
UsecodeDebuggerBreakState is now past the “interesting orphan subsystem” stage and into real class territory.
The live database now has a bounded debugger-state class with a constructor, breakpoint gate, breakpoint table helpers, callstack helpers, explicit recovered 0x0b / 0x15 entry schemas, and step-state helpers, even though the retail game still appears to leave that object dormant.