1981 lines
No EOL
101 KiB
Markdown
1981 lines
No EOL
101 KiB
Markdown
# No Regret Hidden Debugger Investigation
|
|
|
|
## Scope
|
|
|
|
This note records a deeper live-Ghidra pass on `REGRET.EXE` for the hidden usecode debugger, with emphasis on three questions:
|
|
|
|
- did No Regret keep the hidden debugger as a real subsystem,
|
|
- did it keep a real bootstrap path that retail No Remorse appears to have lost,
|
|
- and did it keep any launch/open path that is materially stronger than retail No Remorse?
|
|
|
|
## Short Answer
|
|
|
|
Current best read: No Regret still contains the hidden usecode debugger as a real relocated subsystem, and unlike retail No Remorse it also preserves both a recovered writer/bootstrap for the debugger-state global and a live debugger-facing vtable override that can open the debugger when break/step conditions hit.
|
|
|
|
This is the most important cross-build finding so far for the retail unlock problem. In retail `CRUSADER.EXE`, the current blocker is that fresh live recovery still finds reads but no recovered writer for `1478:659c/659e`, and the retail break-state object still points at inert vtable slots. In `REGRET.EXE`, the analogous global at `1480:712c/712e` has both many debugger-side reads and one direct write from a compact constructor/store stub at `1398:0000`, and that same bootstrap rewires the freshly created object away from the inert base vtable and onto a live frontend vtable whose slot `0` is `usecode_debugger_open_for_current_unit`.
|
|
|
|
## Verified Regret Findings
|
|
|
|
### 1. The broad hidden cheat/debug framework still exists
|
|
|
|
Live decompile of `1148:34d2 Key_CheckSecretCodeSequences` confirms that No Regret still ships a broad hidden cheat/debug family rather than only a few leftover strings.
|
|
|
|
Recovered sequence behavior in this pass:
|
|
|
|
- `jassica16` now routes to the visible taunt `Of course we changed the cheats...` rather than acting as the real cheat-enable sequence.
|
|
- `loosecannon` toggles the main cheat-active latches at `1480:0ac0` and `1480:009b`.
|
|
- Additional hidden sequences still drive `Pix`, `Memory profiler`, and `Video test` style branches.
|
|
|
|
This does not prove the usecode debugger by itself, but it does show that No Regret retained a wider debug/testing framework than a stripped retail-only reading would suggest.
|
|
|
|
### 2. The hidden usecode debugger UI wrappers still exist, relocated
|
|
|
|
The debugger-specific names were not already present in the live `REGRET.EXE` session, but the structural equivalents were recovered by pivoting from `Dispatch_ModalGump` callers.
|
|
|
|
These functions are now renamed live in the active `REGRET.EXE` database:
|
|
|
|
- `1398:0000` = `usecode_debugger_bootstrap_init`
|
|
- `1398:0086` = `usecode_debugger_open_for_current_unit`
|
|
- `1398:020d` = `usecode_debugger_open_modal`
|
|
- `1398:0291` = `usecode_debugger_format_expression_to_shared_buffer`
|
|
- `1398:088f` = `usecode_debugger_source_pane_create`
|
|
- `1398:0ba7` = `usecode_debugger_source_pane_handle_command`
|
|
- `1398:0f16` = `usecode_debugger_source_pane_handle_pointer_event`
|
|
- `1398:1088` = `usecode_debugger_source_line_copy_for_display`
|
|
- `1398:1118` = `usecode_debugger_source_pane_draw_visible_lines`
|
|
- `1398:1413` = `usecode_debugger_source_pane_clamp_viewport`
|
|
- `1398:15ac` = `usecode_debugger_source_pane_load_file`
|
|
- `1398:1791` = `usecode_debugger_source_pane_draw`
|
|
- `1398:193f` = `usecode_debugger_source_pane_handle_click`
|
|
- `1398:19b1` = `usecode_debugger_gump_create`
|
|
- `1398:1c2c` = `usecode_debugger_translate_registered_event`
|
|
- `1398:1dc6` = `usecode_debugger_forward_child_event`
|
|
- `1398:1df3` = `usecode_debugger_handle_event`
|
|
- `13e0:0000` = `usecode_debugger_break_state_create`
|
|
- `13e0:0053` = `usecode_debugger_break_state_update_line_and_maybe_break`
|
|
- `13e0:00dd` = `usecode_debugger_break_state_add_breakpoint`
|
|
- `13e0:01a5` = `usecode_debugger_break_state_remove_breakpoint`
|
|
- `13e0:0230` = `usecode_debugger_break_state_find_breakpoint_or_next_index`
|
|
- `13e0:029e` = `usecode_debugger_break_state_has_breakpoint`
|
|
- `13e0:02f5` = `usecode_debugger_break_state_push_current_entry`
|
|
- `13e0:03b0` = `usecode_debugger_break_state_push_current_entry_copy`
|
|
- `13e0:03f7` = `usecode_debugger_break_state_pop_current_entry`
|
|
- `13e0:0419` = `usecode_debugger_break_state_enable_single_step`
|
|
- `13e0:0432` = `usecode_debugger_break_state_clear_runtime_break_flags`
|
|
- `13e0:0444` = `usecode_debugger_break_state_get_current_entry`
|
|
- `13e0:046f` = `usecode_debugger_break_state_vtable_slot0_noop`
|
|
- `13e0:0474` = `usecode_debugger_break_state_vtable_slot1_return_zero`
|
|
- `13f0:0000` = `interpreter_push_saved_farptr`
|
|
- `13f0:003c` = `interpreter_pop_saved_farptr`
|
|
- `13f0:00e8` = `usecode_interpreter_context_init`
|
|
- `13f0:0244` = `usecode_interpreter_context_create`
|
|
- `13f0:035f` = `usecode_interpreter_context_load_source_cursor_from_global_unit`
|
|
- `13f0:038b` = `usecode_debugger_interpreter_hook`
|
|
- `13f8:10da` = `usecode_interpreter_run_context_with_debugger_hook`
|
|
- `13f8:1d72` = `entity_vm_runtime_get_slot_chunk_ptr_at_offset`
|
|
- `1398:2c2e` = `usecode_debugger_source_buffer_create_from_path`
|
|
- `1398:2ca0` = `usecode_debugger_source_buffer_destroy`
|
|
- `1398:2d14` = `usecode_debugger_source_buffer_open_from_path`
|
|
- `1398:2e0a` = `usecode_debugger_source_buffer_load_text`
|
|
- `1398:2f4f` = `usecode_debugger_source_buffer_split_lines_in_place`
|
|
- `1398:301d` = `usecode_debugger_source_buffer_get_line_ptr`
|
|
|
|
Why `1398:0086` matches the current-unit wrapper:
|
|
|
|
- It builds a debugger gump via `1398:19b1`.
|
|
- It reads the current debugger object from `1480:712c/712e`.
|
|
- It calls `13e0:0444` to fetch the current entry/unit context from that object.
|
|
- It resolves a usecode/unit file path under the shared `s_usecode` root.
|
|
- It conditionally loads the corresponding unit file and then dispatches the gump modally.
|
|
|
|
Why `1398:020d` matches the modal-open wrapper:
|
|
|
|
- It is the smaller sibling that creates the same gump and immediately sends it through `Dispatch_ModalGump` without the extra current-unit file-load path.
|
|
|
|
Newly closed helper roles in this pass:
|
|
|
|
- `usecode_debugger_source_pane_create` is the source-view child-gump constructor used by `usecode_debugger_gump_create`.
|
|
- `usecode_debugger_source_pane_handle_command` owns source-view commands such as line scroll, page navigation, search navigation, and line breakpoint toggles.
|
|
- `usecode_debugger_source_pane_handle_pointer_event` converts pointer coordinates into source line/column space and updates viewport or selection state.
|
|
- `usecode_debugger_source_line_copy_for_display` expands tabs into fixed-width spaces before a source line is rendered.
|
|
- `usecode_debugger_source_pane_draw_visible_lines` is the stronger local draw body for visible source rows, including current-line highlight and breakpoint marks.
|
|
- `usecode_debugger_source_pane_clamp_viewport` constrains the source viewport and syncs the pane against child scrollbars.
|
|
- `usecode_debugger_source_pane_load_file` is the common file-load path used both by the current-unit opener and by the file-open event lane.
|
|
- `usecode_debugger_source_pane_draw` renders the visible source pane from the loaded `.unk` buffer and overlays debugger-side markers.
|
|
- `usecode_debugger_source_pane_handle_click` maps mouse position back to a visible line index.
|
|
- `usecode_debugger_break_state_add_breakpoint`, `remove_breakpoint`, `find_breakpoint_or_next_index`, and `has_breakpoint` close the breakpoint-table maintenance cluster.
|
|
- `usecode_debugger_break_state_push_current_entry`, `push_current_entry_copy`, and `pop_current_entry` close the current-entry stack helper cluster enough to stop treating it as anonymous storage logic.
|
|
- `usecode_debugger_source_buffer_create_from_path`, `destroy`, `open_from_path`, `load_text`, `split_lines_in_place`, and `get_line_ptr` now close the line-indexed source-buffer ownership chain instead of leaving the file-loader side anonymous.
|
|
|
|
### 3. The event dispatcher is the real debugger dispatcher, not a generic text UI
|
|
|
|
Live recovery of `1398:1df3` shows the same usecode-debugger-style command/state machine seen in retail No Remorse, just relocated.
|
|
|
|
Recovered dispatcher lanes in this pass include:
|
|
|
|
- file open with `FILE NOT FOUND` and `Unable to open this file`
|
|
- run / break-next / single-step state changes
|
|
- `Goto Line`
|
|
- `Watch what?`
|
|
- `Inspect what?`
|
|
- `Global name`
|
|
- symbol-not-found / range-check / `Done` flows
|
|
- search flows with `Search for`, `Nothing to find`, and `Not found`
|
|
|
|
This is much stronger than a generic console/editor interpretation. It is the same hidden debugger family in functional form.
|
|
|
|
Wider interaction map from the disassembly cleanup:
|
|
|
|
- the `RUN` lane clears `+0x74/+0x75` and then resumes through the gump callback rather than interpreting source text directly
|
|
- the single-step lane routes through `usecode_debugger_break_state_enable_single_step`
|
|
- one branch clears the current line's breakpoint bit in the loaded source-line table
|
|
- another clears the full 10-line visible breakpoint bitmap band in the current source view
|
|
- one branch prompts for a line number and routes the result through the source-pane selection helper
|
|
- another prompts for a search string, then scans forward through `usecode_debugger_source_buffer_get_line_ptr` results until a match is found
|
|
|
|
That broader event map makes the loaded source buffer and the breakpoint table look like first-class debugger subsystems, not mere UI decoration.
|
|
|
|
### 4. No Regret preserves the missing bootstrap writer
|
|
|
|
This was the key result of the pass.
|
|
|
|
`get_data_uses(1480:712c)` returned many reads from the recovered debugger wrappers/dispatcher and one direct write at `1398:0064`, followed immediately by a read at `1398:0067`.
|
|
|
|
Live decompile of `usecode_debugger_bootstrap_init` now gives the fuller shape. The function:
|
|
|
|
- allocates two debugger-side support buffers (`0x19a` and `0x32`)
|
|
- allocates `0x2f2`
|
|
- calls `13e0:0000`
|
|
- overwrites the newly created object's vtable pointer
|
|
- stores the returned far pointer into `1480:712c/712e`
|
|
- immediately touches object byte `+0x74`
|
|
|
|
Relevant instruction sequence:
|
|
|
|
- `1398:0029` pushes `0x2f2` to the allocator
|
|
- `1398:0042` calls `13e0:0000`
|
|
- `1398:004d` stores `0x6972` into the object vtable slot
|
|
- `1398:0060` writes `DX` to `1480:712e`
|
|
- `1398:0064` writes `AX` to `1480:712c`
|
|
- `1398:0067` reloads the far pointer
|
|
- `1398:006b` sets byte `ES:[BX + 0x74] = 1`
|
|
|
|
There are still no normal direct callers to `usecode_debugger_bootstrap_init`, which makes it look more like a segment/bootstrap initializer than a normal gameplay-invoked helper. But regardless of how it is reached, this is exactly the kind of recovered write-side path that is still missing in current retail No Remorse analysis.
|
|
|
|
### 5. The seg13e0 helper cluster is the Regret break-state family
|
|
|
|
Live decompile of `13e0:0000` shows a real constructor-sized helper for the debugger object:
|
|
|
|
- null-check plus optional internal allocation
|
|
- vtable/root pointer store
|
|
- bulk `0xffff` initialization across the breakpoint/callstack state area
|
|
- explicit zeroing of selected control fields
|
|
- total object size matching the `0x2f2` bootstrap allocation
|
|
|
|
Other recovered matches in the same cluster:
|
|
|
|
- `13e0:0444` returns the current line/entry-derived record pointer from the debugger object, matching the role needed by `1398:0086`
|
|
- `13e0:0419` arms the single-step state by zeroing `+0x76/+0x78` and setting byte `+0x75 = 1`
|
|
- `13e0:0432` clears the two runtime break/step bytes at `+0x74/+0x75` and is called by the interpreter-side hook after it sees the debugger object
|
|
- `13e0:0053` updates the current line, resolves the current entry, checks breakpoint/step conditions, and calls through vtable slot `0` when a break should fire
|
|
|
|
The current safest read is that seg13e0 is the No Regret counterpart of the retail seg1408 break-state helper family.
|
|
|
|
### 6. The bootstrap does more than create the object: it upgrades the base vtable to a live frontend vtable
|
|
|
|
This is the most important deeper result of the second pass.
|
|
|
|
`usecode_debugger_break_state_create` seeds the object with base vtable root `1480:713b`. Live vtable analysis shows that this base table still has the retail-style inert slot pair:
|
|
|
|
- slot `0` -> `13e0:046f usecode_debugger_break_state_vtable_slot0_noop`
|
|
- slot `1` -> `13e0:0474 usecode_debugger_break_state_vtable_slot1_return_zero`
|
|
|
|
But `usecode_debugger_bootstrap_init` immediately overwrites the object's vtable pointer with root `1480:6972`.
|
|
|
|
That replacement vtable is not inert. Its first recovered slots are:
|
|
|
|
- slot `0` -> `1398:0086 usecode_debugger_open_for_current_unit`
|
|
- slot `1` -> `1398:0291 usecode_debugger_format_expression_to_shared_buffer`
|
|
|
|
This is the clearest structural reason No Regret is stronger than retail for debugger launch behavior.
|
|
|
|
Retail No Remorse still has the base break-state object and the inert callback slots. No Regret keeps those same inert base methods in the constructor, but then the bootstrap upgrades the object onto a live frontend-aware vtable.
|
|
|
|
### 7. The interpreter-side hook also still appears to be wired
|
|
|
|
`13f0:038b` is called from `13f8:10fa` inside the newly named `usecode_interpreter_run_context_with_debugger_hook`, and reads the debugger global at `1480:712c/712e` before calling `13e0:0432`.
|
|
|
|
Recovered disassembly around the key handoff:
|
|
|
|
- `13f0:03dd` loads `1480:712c`
|
|
- `13f0:03e0` tests `1480:712e`
|
|
- `13f0:03e6` / `03ea` push the debugger far pointer
|
|
- `13f0:03ee` calls `13e0:0432`
|
|
|
|
That is the current strongest evidence that No Regret preserves not just the UI/event layer and not just the constructor/store path, but also the interpreter-side consumer/hook path that can actually consult the debugger object during VM execution.
|
|
|
|
The wrapper at `13f8:10da` now makes that path clearer:
|
|
|
|
- it marks the live interpreter context active via bytes near `+0x122/+0x123`
|
|
- it passes two runtime context pointers into `usecode_debugger_interpreter_hook` from the same parent object (`+0x121` and `+0x36`)
|
|
- if the hook returns the continue code, the wrapper falls back into the normal virtual dispatch lane
|
|
|
|
That is not the shape of a manual debugger launcher. It is the shape of ordinary VM execution with an optional debugger sidecar.
|
|
|
|
The final direct-xref closure in Regret is now tighter than the earlier pass:
|
|
|
|
- exhaustive `1480:712c/712e` data-use recovery now shows only the already-named bootstrap/open/format/draw/event functions plus `usecode_debugger_interpreter_hook`; no extra hidden debugger-object consumers surfaced outside that cluster
|
|
- direct address searches for the helper entry points also stayed narrow: `usecode_debugger_break_state_clear_runtime_break_flags` is only called from the hook prologue, `usecode_debugger_break_state_pop_current_entry` is only called from the hook unwind, `usecode_debugger_interpreter_hook` is only called from `usecode_interpreter_run_context_with_debugger_hook`, and `usecode_interpreter_context_create` is only called from the upstream usecode-process/context factory path at `13f8:0eec`
|
|
- that means the remaining uncertainty is no longer broad subsystem discovery; it is the exact location of the missing current-entry push inside an already-identified interpreter/runtime path
|
|
|
|
### 8. No Regret appears to keep an auto-open-on-break path that retail No Remorse lacks
|
|
|
|
This is the practical launch-path conclusion.
|
|
|
|
`usecode_debugger_break_state_update_line_and_maybe_break` sets the current line, resolves the current entry, evaluates breakpoint/step conditions, and when those conditions hit it calls through vtable slot `0`.
|
|
|
|
Because the Regret bootstrap overwrites the object onto vtable root `1480:6972`, that slot `0` call is no longer inert. It now lands at `usecode_debugger_open_for_current_unit`.
|
|
|
|
That means Regret still appears to preserve this path:
|
|
|
|
- debugger object exists
|
|
- interpreter-side hook sees that object
|
|
- break/step state reaches `usecode_debugger_break_state_update_line_and_maybe_break`
|
|
- slot `0` dispatch opens the current-unit debugger UI
|
|
|
|
This is exactly the kind of end-to-end `break leads to debugger UI` path that retail No Remorse does not currently show, because retail still routes the equivalent slot `0` call into a no-op stub.
|
|
|
|
### 9. But no direct user-facing launch path has been recovered yet
|
|
|
|
The stronger Regret result still has an important limit.
|
|
|
|
Current direct-caller state in the live database:
|
|
|
|
- no normal direct callers to `usecode_debugger_bootstrap_init`
|
|
- no normal direct callers to `usecode_debugger_open_for_current_unit`
|
|
- no normal direct callers to `usecode_debugger_open_modal`
|
|
- `usecode_debugger_handle_event` is reached only through debugger callback wrappers (`usecode_debugger_translate_registered_event` and `usecode_debugger_forward_child_event`)
|
|
- `usecode_debugger_interpreter_hook` is called from `usecode_interpreter_run_context_with_debugger_hook`, which looks like a real runtime/interpreter-side consumer
|
|
- no normal direct callers are yet recovered for `usecode_debugger_break_state_push_current_entry` or `usecode_debugger_break_state_push_current_entry_copy`
|
|
|
|
So the current best distinction is:
|
|
|
|
- Regret does preserve a real debugger-opening path once break/step conditions are active
|
|
- but this pass still did not recover a clean player-facing hotkey/menu/command-line entry that intentionally opens the debugger from normal gameplay
|
|
|
|
In other words, No Regret looks better wired than retail No Remorse, but still not yet proven to expose a deliberate user-facing launcher.
|
|
|
|
### 10. The required seeding currently looks engine-side, not like a compiled-usecode feature
|
|
|
|
This pass materially tightens the `who seeds the records?` question.
|
|
|
|
What the new names and caller map now support:
|
|
|
|
- `usecode_debugger_break_state_pop_current_entry` has a direct caller in `usecode_debugger_interpreter_hook`
|
|
- `usecode_interpreter_run_context_with_debugger_hook` wraps an ordinary interpreter execution path, not a debugger-only path
|
|
- the hook clears runtime break flags on entry, runs the interpreter loop, and unwinds debugger current-entry depth on exit
|
|
- the current-entry push helpers still have no recovered script-visible or UI-visible callers
|
|
- the new source-buffer/file-load chain is only consumed by debugger UI and current-unit open paths, not by the interpreter-side seeding path
|
|
|
|
Caller-recovery status after the wider disassembly pass:
|
|
|
|
- the direct caller of `usecode_debugger_break_state_push_current_entry` is still not surfaced as a normal xref in the current Regret database
|
|
- the strongest current structural candidate remains an unrecovered or still-overlapped interpreter-side producer analogous to retail `Interpreter_NextUsecodeOp`
|
|
- the deeper recovered Regret chain is now explicit: `Usecode_ItemCallEvent -> 13f8:0eec -> usecode_interpreter_context_create -> usecode_interpreter_context_init / interpreter_push_saved_farptr -> usecode_debugger_interpreter_hook`
|
|
- that outer factory path also explains where the debugger-side source cursor comes from: `13f8:0eec` and `usecode_interpreter_context_load_source_cursor_from_global_unit` both use `entity_vm_runtime_get_slot_chunk_ptr_at_offset` against the current live usecode root at `1480:71a1/71a3`
|
|
- the nearby `13f0` context/setup helpers seed the live interpreter object's source-stream and frame-base lanes (`+0xd6/+0xd8` and `+0xda/+0xdc`), but the visible direct call path there still reaches stack/context helpers rather than the debugger push helper itself
|
|
- a seemingly promising `1458:` lane that writes fields at `+0x72/+0x74/+0x76/+0x78/+0x7a/+0x7c` turned out to be the RIFF/animation parser family, not the debugger, so it should be treated as a false structural cousin rather than as debugger seeding evidence
|
|
|
|
What this now rules out with fairly high confidence:
|
|
|
|
- there is no additional large unnamed Regret-side debugger subsystem still hiding outside the already recovered `1398` / `13e0` / `13f0` / `13f8` lanes
|
|
- there is no second recovered writer or alternate global-owner path for the debugger object beyond `usecode_debugger_bootstrap_init`
|
|
- there is no recovered direct UI/source-buffer-side path that seeds the current-entry stack before `RUN`
|
|
- there is no ordinary standalone caller of `usecode_debugger_break_state_push_current_entry` waiting to be found by one more basic xref sweep
|
|
|
|
Current best read from that combination:
|
|
|
|
- current-entry seeding is part of engine-side interpreter bookkeeping around live usecode execution
|
|
- it is not currently evidenced as a direct feature toggled by `.unk` contents
|
|
- it is not currently evidenced as a distinct compiled-usecode opcode that scripts can invoke to manufacture debugger state
|
|
|
|
Compiled usecode still matters, but in a narrower way:
|
|
|
|
- it can carry `LINE_NUMBER` metadata in builds/corpora that preserve it
|
|
- it provides the runtime activity that naturally flows through the interpreter wrapper/hook path
|
|
- but the actual debugger-object and current-entry stack ownership still appears to live in executable-side VM code
|
|
|
|
So the current answer to `is there an automated mechanism?` is `yes, probably`, but the mechanism currently looks like ordinary interpreter-side debug bookkeeping rather than a dedicated source-file or script-level launcher feature.
|
|
|
|
### 11. What the seeding record actually is, and what that implies for simulation
|
|
|
|
The current-entry push helpers are now explicit enough to answer the practical `what is being seeded?` question.
|
|
|
|
`usecode_debugger_break_state_push_current_entry` writes one `0x15`-byte inline record into the debugger break-state object:
|
|
|
|
- `+0x00..+0x08` = inline unit-name string, asserted to fit in `8` bytes plus terminator
|
|
- `+0x09..+0x0c` = first runtime far pointer
|
|
- `+0x0d..+0x10` = second runtime far pointer
|
|
- `+0x11..+0x14` = third runtime far pointer
|
|
|
|
The important part is not the string. It is what the debugger later does with the payload dwords.
|
|
|
|
Known current consumers:
|
|
|
|
- `usecode_debugger_open_for_current_unit` uses the inline unit name to build the `.unk` path under `s_usecode`
|
|
- `usecode_debugger_format_expression_to_shared_buffer` resolves the top current-entry record and passes entry `+0x09` and `+0x0d` into `FUN_1398_045c`
|
|
- the formatter helper then dereferences those payload dwords as live source/descriptor and frame/evaluation context, not as inert metadata
|
|
|
|
That means the debugger seeding issue is **not** `how do we invent filenames or line numbers`. It is `how do we capture a valid live VM snapshot and serialize it into the break-state stack`.
|
|
|
|
The recovered interpreter-side precursor path now explains where that snapshot data naturally comes from:
|
|
|
|
- `usecode_interpreter_context_create` seeds the live interpreter context with source-stream cursor at `+0xd6/+0xd8`
|
|
- the same helper seeds a second live lane at `+0xda/+0xdc` that the hook immediately dereferences as a frame/stream-adjacent context root
|
|
- `+0xe1/+0xe3` is the strongest current candidate for the third current-entry dword, though that trailing entry field still lacks a live consumer in Regret
|
|
- `usecode_interpreter_context_load_source_cursor_from_global_unit` and the upstream `13f8:0eec` factory both derive the source cursor from the current usecode root at `1480:71a1/71a3` through `entity_vm_runtime_get_slot_chunk_ptr_at_offset`
|
|
|
|
So the seeding data is already present in ordinary in-process runtime state. The missing piece is the serializer/call site that pushes it into the debugger break-state object.
|
|
|
|
### 12. Existing data versus live memory: what has to be true for the debugger to work
|
|
|
|
Current best answer:
|
|
|
|
- **existing data is necessary but not sufficient**
|
|
- the live interpreter context already contains the source cursor and frame payload the debugger needs
|
|
- but those values still have to be copied into the debugger break-state stack at the right moment
|
|
|
|
In other words, this is not a pure offline data-format problem.
|
|
|
|
What can be prepared offline:
|
|
|
|
- a loadable `.unk` for the current unit
|
|
- line-number metadata, if a future Regret-side injector is built
|
|
- breakpoint/source text that matches the compiled usecode unit name
|
|
|
|
What cannot be prepared offline with the current evidence:
|
|
|
|
- the live source-stream cursor far pointer the formatter dereferences
|
|
- the live frame/evaluation payload far pointer the formatter dereferences
|
|
- the surrounding timing of when the hook expects the current-entry depth to exist and when it later unwinds it
|
|
|
|
That is why `.unk` export alone can open files, but does not give stable `RUN` / step behavior.
|
|
|
|
### 13. External-process hypothesis: possible in principle, but weak in the current evidence
|
|
|
|
The idea that original developers might have used an external helper is worth testing against the recovered structure.
|
|
|
|
What the current evidence supports poorly:
|
|
|
|
- no recovered IPC, serial, network, or file-polling path tied specifically to the hidden debugger
|
|
- no recovered external-loader or sidecar command path that would import a prebuilt current-entry record
|
|
- no evidence that the `.unk` file or compiled-usecode stream carries the three live payload dwords directly
|
|
|
|
What an external helper **would** have to do if it existed:
|
|
|
|
- locate the debugger object in live DOS memory
|
|
- know the exact break-state layout
|
|
- locate the active interpreter context for the running unit
|
|
- copy the current unit name plus at least the source cursor and frame payload dwords into the debugger stack record
|
|
- keep pace with interpreter execution because the hook currently pops one current-entry record on unwind/exit
|
|
|
|
That is possible in principle for a development-only DOS memory poking tool, but it would still be a **live memory injector**, not an external source-file preprocessor.
|
|
|
|
The stronger current explanation is simpler:
|
|
|
|
- the game already has all the required data in-process
|
|
- Regret still has the debugger object, hook, open path, and stack helpers
|
|
- the missing visible push is most likely inside a table-driven, overlapped, or otherwise not-cleanly-recovered interpreter path rather than in a separate external system
|
|
|
|
So the external-process hypothesis is not impossible, but it is not needed to explain the current evidence and currently looks less likely than an internal interpreter-side producer.
|
|
|
|
### 14. How we can simulate the seeding today
|
|
|
|
There are three realistic simulation strategies, and they are not equally good.
|
|
|
|
#### A. Reuse the game's own runtime data with a small in-process patch
|
|
|
|
This is the most promising route.
|
|
|
|
The recovered Regret path already creates the live context data we need:
|
|
|
|
- `Usecode_ItemCallEvent -> 13f8:0eec -> usecode_interpreter_context_create -> usecode_debugger_interpreter_hook`
|
|
|
|
So the smallest high-confidence simulation is to add or re-enable a push at an existing in-process point where all ingredients are already live:
|
|
|
|
- current unit name
|
|
- source-stream cursor
|
|
- frame payload pointer
|
|
- debugger object pointer
|
|
|
|
Conceptually, this means restoring the missing `push_current_entry` behavior near the interpreter-context creation / hook handoff instead of inventing new data.
|
|
|
|
Why this is strongest:
|
|
|
|
- it uses the game's own live VM pointers
|
|
- it avoids guessing structure contents from outside the process
|
|
- it naturally matches the hook's later unwind behavior
|
|
|
|
#### B. Inject one current-entry record into live memory from outside the running game
|
|
|
|
This is possible, but it is a second-choice route.
|
|
|
|
For a minimal one-shot debugger open, the injected state would need at least:
|
|
|
|
- debugger object at `1480:712c/712e` already valid
|
|
- depth `+0x7a >= 1`
|
|
- inline unit-name record at `+0x7c`
|
|
- entry `+0x09` = valid live source cursor far pointer
|
|
- entry `+0x0d` = valid live frame payload far pointer
|
|
- entry `+0x11` = probably safe as zero or borrowed from the third interpreter lane unless a new consumer proves otherwise
|
|
- current line `+0x72` set coherently enough for the source pane and breakpoint checks
|
|
|
|
This could probably make `open_for_current_unit` and some expression/inspect flows work if the payload pointers are borrowed from a real live interpreter context.
|
|
|
|
Why this is weaker for stable `RUN` / step:
|
|
|
|
- the hook explicitly pops one current-entry record on exit
|
|
- if the normal push path is still absent, an external injector would need to re-seed repeatedly or patch around the unwind behavior
|
|
- blind injection without borrowing real live pointers is likely to crash when the formatter or hook dereferences the payload lanes
|
|
|
|
So yes, **injecting live data into a running DOSBox game could work**, but only if the injector is aware of the live interpreter context and not just writing guessed constants.
|
|
|
|
#### C. Offline-only tricks such as `.unk` rewriting or line-number injection
|
|
|
|
This is useful, but it does not solve the seeding problem by itself.
|
|
|
|
- better `.unk` files improve source loading
|
|
- line-number injection would improve current-line fidelity in Regret
|
|
- neither one creates the live source cursor / frame payload snapshot the debugger actually consumes
|
|
|
|
So offline-only work is supportive, not sufficient.
|
|
|
|
### 15. What an in-process patch would actually do
|
|
|
|
The safest current model is **not** `hex-edit the EXE until it behaves`. It is `restore one missing debugger-side serialization step at a point where the game already has all required live data`.
|
|
|
|
The candidate in-process patch is small in *behavioral* scope even if it still has to be handled carefully at the byte level:
|
|
|
|
- leave the existing debugger object/bootstrap path alone
|
|
- leave the existing UI/event/gump code alone
|
|
- leave the existing interpreter context creation path alone
|
|
- add or re-enable one call into `usecode_debugger_break_state_push_current_entry` or `...push_current_entry_copy` at a point where the current unit name and the three payload dwords already exist live
|
|
|
|
In concrete terms, the best current patch window is the already-recovered handoff around:
|
|
|
|
- `Usecode_ItemCallEvent`
|
|
- `13f8:0eec`
|
|
- `usecode_interpreter_context_create`
|
|
- `usecode_debugger_interpreter_hook`
|
|
|
|
That is the narrowest place where all of these are already true at once:
|
|
|
|
- a live interpreter/process object exists
|
|
- the source-stream cursor is already computed
|
|
- the frame/evaluation payload lane already exists
|
|
- the debugger object global can already be tested
|
|
|
|
So an in-process patch would most likely mean one of these two designs:
|
|
|
|
1. restore a missing direct push call near context creation or immediately before the debugger hook runs
|
|
2. synthesize the equivalent `0x15`-byte record in-place and then increment debugger depth exactly once per entered debugged frame
|
|
|
|
The first design is strongly preferred because it reuses the shipped helper and keeps the later pop/unwind behavior matched.
|
|
|
|
### 16. Why this should be done in Ghidra on a writable patch target, not by blind hex editing
|
|
|
|
The user's earlier failures with larger EXE edits fit the current structural risks.
|
|
|
|
What makes blind hex editing risky here:
|
|
|
|
- 16-bit NE code is tightly laid out and often table-driven
|
|
- some candidate lanes already show overlap/thunk-like behavior
|
|
- changing instruction length casually can invalidate nearby control flow or jump tables
|
|
- editing the main reference executable directly makes it too easy to lose track of which behavior is original versus experimental
|
|
|
|
What makes a Ghidra-side patch workflow safer:
|
|
|
|
- patch bytes can be applied at a studied address with nearby disassembly context preserved
|
|
- reanalysis can show whether the patched basic block still disassembles coherently
|
|
- comments can record intent directly at the patch site
|
|
- the experiment can be kept on a dedicated writable copy instead of the reference executable
|
|
|
|
So yes, Ghidra can be used for this kind of patching, and the MCP layer also exposes byte patching, but the current project rules still matter:
|
|
|
|
- **do not patch `REGRET.EXE` directly as the reference binary**
|
|
- only patch an explicitly writable copy, normally under a dedicated writable target folder
|
|
- study the exact callsite and byte budget first
|
|
|
|
The right mental model is `surgical patch on a writable clone`, not `edit the shipped EXE in place`.
|
|
|
|
### 17. Which debugger features depend on seeding, and which do not
|
|
|
|
The recovered event dispatcher at `1398:1df3` now makes the dependency split much clearer.
|
|
|
|
Features that are mostly just break-state/UI flags:
|
|
|
|
- `RUN` clears `+0x74/+0x75` and resumes through the existing callback path
|
|
- `break next` sets the break-next latch by writing `+0x74 = 1` with `+0x75 = 0`
|
|
- `single step` calls `usecode_debugger_break_state_enable_single_step`
|
|
- source-file open, goto-line, search, and breakpoint-table editing are mainly source-buffer/UI features once the debugger is open
|
|
|
|
### 18. Retail Ghidra follow-up implied by the Regret mapping
|
|
|
|
The Regret pass is now strong enough to drive a specific retail `CRUSADER.EXE` naming batch rather than a generic `more seg109 cleanup` request.
|
|
|
|
The most important retail equivalents to promote explicitly in live Ghidra next are:
|
|
|
|
- `13a0:2882` = `usecode_debugger_build_menubar`
|
|
- `13a0:088f` = `usecode_debugger_source_pane_create`
|
|
- `13a0:0ae8` = `usecode_debugger_source_pane_init_view_from_break_state`
|
|
- `13a0:0ba7` = `usecode_debugger_source_pane_handle_command`
|
|
- `13a0:0f16` = `usecode_debugger_source_pane_handle_pointer_event`
|
|
- `13a0:1088` = `usecode_debugger_source_line_copy_for_display`
|
|
- `13a0:1118` = `usecode_debugger_source_pane_draw_visible_lines`
|
|
- `13a0:1413` = `usecode_debugger_source_pane_clamp_viewport`
|
|
- `13a0:15ac` = `usecode_debugger_source_pane_load_file`
|
|
- `13a0:16ee` = `usecode_debugger_watch_pane_create`
|
|
- `13a0:1791` = `usecode_debugger_watch_pane_draw`
|
|
- `13a0:193f` = `usecode_debugger_watch_pane_handle_click`
|
|
- `13a0:1c2c` = `usecode_debugger_translate_registered_event`
|
|
- `13a0:1dc6` = `usecode_debugger_forward_child_event`
|
|
- `13a0:2c2e` = `usecode_debugger_source_buffer_create_from_path`
|
|
- `13a0:2ca0` = `usecode_debugger_source_buffer_destroy`
|
|
- `13a0:2d14` = `usecode_debugger_source_buffer_open_from_path`
|
|
- `13a0:2e0a` = `usecode_debugger_source_buffer_load_text`
|
|
- `13a0:2f4f` = `usecode_debugger_source_buffer_split_lines_in_place`
|
|
- `13a0:301d` = `usecode_debugger_source_buffer_get_line_ptr`
|
|
|
|
Why this follows from the Regret result rather than from guesswork:
|
|
|
|
- the retail `000b:* -> 13a0:*` table already closes the same UI layer at the raw/reference level
|
|
- the Regret `1398:*` cleanup shows the same function ordering and subsystem boundaries in a build where the debugger bootstrap survived
|
|
- that makes the remaining retail seg109 backlog primarily a promotion/documentation task, not a fresh discovery task
|
|
|
|
The live retail rename pass also closed one useful correction: the original retail-first mirror list over-assigned `13a0:1791` and `13a0:193f` to the source pane. Current decompile evidence now makes the split cleaner: `13a0:16ee/1791/193f` form a watch-pane lane, while the source-pane lane retains the file-load, pointer-event, line-copy, and viewport helpers.
|
|
|
|
This is the right pre-patch documentation step because it turns the surviving retail debugger lane into named UI, event, and source-buffer surfaces instead of leaving the future patch window hidden among anonymous helpers.
|
|
|
|
### 19. Delivery implication after the patching stall
|
|
|
|
The Regret result changes what `practical next step` should mean.
|
|
|
|
The next useful proof is no longer `try another manual hex patch`. It is one of these two:
|
|
|
|
1. a runtime-only memory experiment that proves the create/store/open model on a clean executable
|
|
2. a reproducible scripted patch against a writable clone, driven by a verified byte plan rather than by hand edits
|
|
|
|
Why the Regret comparison pushes in that direction:
|
|
|
|
- it already tells us the missing retail behavior is a small bootstrap-and-vtable problem, not a whole missing subsystem
|
|
- it shows that the real stability question is whether live interpreter state is seeded correctly, not whether the source pane exists
|
|
- it therefore favors runtime proof and scripted reproducibility over one-off static patching attempts
|
|
|
|
Features that clearly depend on seeded current-entry runtime payload:
|
|
|
|
- `Inspect what?`
|
|
- `Watch what?`
|
|
- current-unit open centered on the correct unit/line
|
|
- any resume/step flow that expects the debugger to know the active frame context
|
|
|
|
Why watch/inspect depend on seeding:
|
|
|
|
- the dispatcher routes both commands through the debugger object's formatter callback
|
|
- `usecode_debugger_format_expression_to_shared_buffer` resolves the top current-entry record from the break-state stack
|
|
- it passes entry `+0x09` and `+0x0d` into `FUN_1398_045c`
|
|
- that helper dereferences those values as live descriptor/source and frame/evaluation context
|
|
|
|
So watch and inspect are not a workaround for missing seeding. They are actually one of the strongest proofs that seeding must be correct.
|
|
|
|
The `Global name` command is different.
|
|
|
|
What case `0x0f` in the dispatcher currently appears to do:
|
|
|
|
- prompt for a global symbol name
|
|
- resolve that symbol through `FUN_13e8_039f`
|
|
- copy the current bytes of the resolved global into a temporary buffer
|
|
- prompt for replacement bytes
|
|
- validate each entered byte is within `0x00..0xff`
|
|
- write the replacement bytes back into the live backing buffer
|
|
|
|
That means `change globals` looks comparatively independent of callstack seeding once the debugger UI itself is alive. It edits resolved global data by symbol name rather than by current frame context.
|
|
|
|
Practical implication:
|
|
|
|
- if the debugger can be opened at all, `change globals` may be one of the first useful commands to test
|
|
- `watch` and `inspect` are later-stage validation targets because they are stricter consumers of the seeded runtime payload
|
|
|
|
### 18. Do the other debugger features help us get debug functionality working?
|
|
|
|
Yes, but mostly as **validation targets**, not as bootstrap mechanisms.
|
|
|
|
Most useful feature signals after a bring-up patch:
|
|
|
|
1. `open current unit` works and loads the right `.unk`
|
|
2. source navigation, search, goto-line, and breakpoint toggles work without crashing
|
|
3. `change globals` can resolve and write a known symbol safely
|
|
4. `watch` and `inspect` return coherent values instead of crashing or producing garbage
|
|
5. `RUN`, `break next`, and `single step` survive at least one execution/unwind cycle
|
|
|
|
Those checks tell us progressively more:
|
|
|
|
- stages 1-3 prove the UI, file loading, symbol lookup, and non-frame-sensitive debugger features are alive
|
|
- stages 4-5 prove the current-entry seeding is actually coherent enough for real runtime debugging
|
|
|
|
So the extra debugger commands help a lot in narrowing validation order, but they do not remove the need to solve seeding first.
|
|
|
|
### 19. Most likely path to getting working debug functionality in No Regret
|
|
|
|
Current best path, in order of likelihood and engineering sanity:
|
|
|
|
1. Create a writable Regret patch target and keep the reference `REGRET.EXE` untouched.
|
|
2. Identify the smallest concrete callsite near `13f8:0eec -> usecode_interpreter_context_create -> usecode_debugger_interpreter_hook` where a debugger push can be added with minimal byte disruption.
|
|
3. Prefer a patch that calls the shipped `usecode_debugger_break_state_push_current_entry` helper rather than reimplementing the record layout by hand.
|
|
4. If a direct call will not fit cleanly, use a tiny detour/trampoline into spare writable code space on the patch target rather than forcing a larger inline rewrite.
|
|
5. Validate the patch first by opening the debugger and exercising file open, goto-line, search, and `change globals`.
|
|
6. Only then test `watch` and `inspect`, because they are the first strong consumers of the seeded runtime payload.
|
|
7. Only after those work, test `RUN`, then `break next`, then `single step`, because the pop-on-unwind behavior means resume features are stricter than one-shot UI success.
|
|
|
|
What is *less* likely to be the winning path:
|
|
|
|
- offline `.unk` or line-number work alone
|
|
- a large multi-site executable rewrite
|
|
- blind direct hex editing of the reference binary
|
|
- an external helper unless it is effectively a smart live-memory injector that reads the real interpreter context
|
|
|
|
So the most likely route to working debug functionality is:
|
|
|
|
- **small in-process patch on a writable Regret copy**
|
|
- **reuse shipped helper(s) and existing VM context data**
|
|
- **validate in stages, with `change globals` before `watch/inspect`, and `watch/inspect` before full resume/step**
|
|
|
|
### 20. First writable-patch prototype applied to `REGRET-PATCHED.EXE`
|
|
|
|
As of this pass, the writable target `/Writable/REGRET-PATCHED.EXE` has a first proof-of-concept patch in the live Ghidra database.
|
|
|
|
Applied patch shape:
|
|
|
|
- patched call site at `13f8:10fa`
|
|
- original `CALLF 0x13f0:038b`
|
|
- now redirected to local trampoline `13f8:2040`
|
|
|
|
Local trampoline behavior at `13f8:2040`:
|
|
|
|
- check debugger object global `1480:712c/712e`
|
|
- derive the current process base from the hook context pointer
|
|
- resolve the unit-name far pointer from `g_processNames`
|
|
- read the live interpreter context lanes at `+0xd6/+0xd8`, `+0xda/+0xdc`, and `+0xe1/+0xe3`
|
|
- call `usecode_debugger_break_state_push_current_entry`
|
|
- tail into the original `usecode_debugger_interpreter_hook`
|
|
|
|
This is intentionally narrow. It does **not** try to rewrite the debugger UI, breakpoint tables, source loader, or watch/inspect formatter. It only restores one likely missing seeding step before the original hook runs.
|
|
|
|
Known remaining risk in this prototype:
|
|
|
|
- it assumes one current-entry push per wrapped hook invocation is the right balance for the later pop-on-unwind path
|
|
- it assumes the `g_processNames` entry is the correct unit-name source in this lane
|
|
- it assumes the third current-entry dword can be borrowed from `+0xe1/+0xe3` without harming current consumers
|
|
|
|
So this should be treated as a tightly scoped runtime experiment, not as a final debugger restoration.
|
|
|
|
### 21. What to test on the patched writable target
|
|
|
|
Recommended test order:
|
|
|
|
1. Verify the patched executable still starts at all.
|
|
2. Reproduce the same path that previously opened the hidden debugger with a loadable `.unk`.
|
|
3. Confirm the debugger window opens without an immediate crash.
|
|
4. Confirm the current-unit source file loads and the source pane is populated.
|
|
5. Test `Goto Line` and `Search for` first, because they exercise source-buffer/UI logic without depending on deep expression evaluation.
|
|
6. Test adding and removing a breakpoint marker in the source pane.
|
|
7. Test `Global name` on a harmless known symbol first, because this is the least callstack-sensitive mutation feature.
|
|
8. Test `Inspect what?` on a simple symbol/expression.
|
|
9. Test `Watch what?` with one watch entry.
|
|
10. Only after the above succeed, test `RUN`.
|
|
11. If `RUN` survives once, test `break next`.
|
|
12. Last, test `single step`.
|
|
|
|
What to note during testing:
|
|
|
|
- whether the debugger opens but still crashes only on `RUN`
|
|
- whether `Inspect` / `Watch` now produce values instead of crashing or returning obvious garbage
|
|
- whether the source view follows the current unit/line more coherently than before
|
|
- whether `Global name` is usable before the resume/step features are stable
|
|
- whether any crash now happens on unwind/return instead of on initial open, which would point at push/pop imbalance rather than missing seeding
|
|
|
|
### 22. The line-number injector idea is viable, but the real target is Regret, not retail Remorse
|
|
|
|
The new evidence makes that split much clearer.
|
|
|
|
What the current parser/extractor already shows:
|
|
|
|
- retail Remorse compiled usecode already carries line markers in the present parser pass: `977 / 977` events currently recover `LINE_NUMBER` ops
|
|
- the map-viewer decompiler already recognizes the opcode directly as `0x5b = LINE_NUMBER`
|
|
- current retail Regret compiled usecode is the stripped side in the present parser pass: `1152 / 1152` events currently recover `0` `LINE_NUMBER` ops
|
|
|
|
So the immediate correction is:
|
|
|
|
- a line-number injector is **not** mainly a retail Remorse need
|
|
- it is mainly a Regret recovery idea if the goal is to improve debugger/source correlation when loading usecode through `-u`
|
|
|
|
The idea itself is structurally plausible, but only if it is treated as a bytecode rewriter rather than as a text-side export trick.
|
|
|
|
Strongest viable shape:
|
|
|
|
1. decode existing compiled usecode into an IR that preserves exact opcode offsets and branch targets
|
|
2. attach desired line numbers to existing instruction boundaries from the pseudocode/IR mapping
|
|
3. inject `LINE_NUMBER` opcodes into the existing event streams
|
|
4. rewrite event sizes and any offset-based control-flow operands that shift because of the inserted bytes
|
|
5. rebuild a patched `EUSECODE.FLX` for `-u`
|
|
|
|
Why this is more than `just add debug markers`:
|
|
|
|
- even though the target opcode is simple, inserting bytes changes event layout
|
|
- any relative jump, switch, or offset-based control operand inside the event stream has to be rebased correctly
|
|
- the hard part is not inventing pseudocode again; it is preserving existing compiled behavior while changing stream length safely
|
|
|
|
Current best use for that tool if it is ever built:
|
|
|
|
- improve Regret line fidelity inside the debugger for `-u`-loaded compiled usecode
|
|
- potentially give breakpoints/current-line display a better match than the current synthetic floor
|
|
|
|
Current limit of that idea:
|
|
|
|
- even a perfect line-number injector would still not replace the missing interpreter-seeded current-entry stack
|
|
- it can improve source correlation, but it does not solve the runtime seeding problem by itself
|
|
|
|
### 23. Failed raw helper-host experiments to avoid repeating
|
|
|
|
These are the patch shapes that were tried and should be treated as dead ends for now:
|
|
|
|
- `13f8:2040..20b9` as a startup-path runtime helper: caused startup aborts and GPFs when booted through the interpreter hook path, including `Load program failed -- Error code 201`, `Abort: IRET: Illegal descriptor type 0x0`, and `General protection fault detected`.
|
|
- `13f8:20dd..2157` as a second helper host: allowed the game to reach startup, but then it immediately exited to the `No Pity. No Mercy. No Regret.` line, so it is also not a safe host.
|
|
- any variant that detours `13f8:10fa` away from retail at startup: the safe read is now that the startup wrapper should stay retail, because the runtime helper experiments are what destabilized boot.
|
|
- any variant that tries to keep the runtime helper logic alive inside segment `13f8` without first proving the target region is truly unused: those edits are too close to live startup code in this build.
|
|
|
|
Current safe patch shape:
|
|
|
|
- keep the startup wrapper retail
|
|
- keep the `loosecannon` trigger retargets only
|
|
- use the debugger bootstrap/open-modal path from the hidden cheat lane only
|
|
- do not re-enable the runtime-helper branch until a genuinely unused function-sized host region is identified
|
|
|
|
## Current Cross-Build Implications
|
|
|
|
### What this changes
|
|
|
|
The earlier retail conclusion was: the hidden debugger survives as an orphaned subsystem, but the missing writer/bootstrap makes minimum-modification entry hard to justify.
|
|
|
|
No Regret materially changes that framing.
|
|
|
|
The best current cross-build model is now:
|
|
|
|
- retail No Remorse still has the debugger UI, event dispatcher, and break-state helper family, but not yet a recovered writer for the debugger-state global and still appears to route slot `0` through inert stubs
|
|
- No Regret keeps the same broad subsystem shape, keeps a compact bootstrap stub at `1398:0000` that writes the debugger-state global, and upgrades the object from the inert base vtable at `1480:713b` to the live frontend vtable at `1480:6972`
|
|
- therefore the key difference is no longer only `writer exists versus writer missing`; it is also `live debugger-opening vtable path exists versus inert retail stubs`
|
|
|
|
### Why this matters for the retail unlock problem
|
|
|
|
If retail lost only a small bootstrap/writer path while Regret preserved it, then the smallest defensible retail activation path may be much closer to:
|
|
|
|
- recovering a missing retail analogue of the Regret bootstrap,
|
|
- restoring the retail equivalent of the Regret live vtable override,
|
|
- or identifying one surviving retail callsite that used to seed the same state,
|
|
- or borrowing the exact structural delta from a sibling build,
|
|
|
|
instead of inventing another wider retail-only interpreter patch chain.
|
|
|
|
## Force Options In No Regret
|
|
|
|
The remaining practical question is not only `does Regret keep the debugger?`, but `can we force it up by hacking?`
|
|
|
|
Current best read: yes, Regret is now the best known build for a forced debugger bring-up, but the viable options split into three very different classes:
|
|
|
|
- direct executable patching
|
|
- live memory forcing / trainer-style intervention
|
|
- usecode-assisted hybrids
|
|
|
|
Pure usecode-only forcing is still not evidenced.
|
|
|
|
### 1. Direct executable patching is now the strongest practical route
|
|
|
|
This is the biggest Regret-side difference from retail No Remorse.
|
|
|
|
Retail still appears to be missing both the write-side bootstrap and the live vtable promotion. Regret already has both of those pieces. That means a Regret-specific patch does **not** need to recreate the entire subsystem. It only needs to route some existing hidden/debug input lane into already-existing debugger entry points.
|
|
|
|
#### Best direct-open patch target
|
|
|
|
The lowest-risk explicit open path is now:
|
|
|
|
- ensure `usecode_debugger_bootstrap_init` has run or call it if the debugger global is null
|
|
- then call `usecode_debugger_open_modal`
|
|
|
|
Why this is the safest direct-open target:
|
|
|
|
- `usecode_debugger_open_modal` does not require the current-unit preload path
|
|
- it still uses the real Regret debugger gump constructor and event dispatcher
|
|
- it avoids depending on the current-entry index being valid before the first open
|
|
|
|
This should produce the most reliable `force the menu up` result if the immediate goal is simply to display the debugger UI.
|
|
|
|
#### Best full-context patch target
|
|
|
|
If the goal is not just `open a shell`, but `open the debugger on the current unit with real context`, the stronger path is:
|
|
|
|
- ensure the debugger object exists
|
|
- force the break/step state
|
|
- let the next interpreter-side debugger path auto-open through slot `0`
|
|
|
|
In Regret this is now structurally plausible because:
|
|
|
|
- `usecode_debugger_break_state_update_line_and_maybe_break` dispatches through slot `0`
|
|
- the live Regret vtable promotion makes slot `0` equal `usecode_debugger_open_for_current_unit`
|
|
- `usecode_debugger_interpreter_hook` is still wired into a real runtime caller at `13f8:10da`
|
|
|
|
This is the best path if the goal is `show me the real source/current-unit debugger experience`, not merely `show me a debugger window`.
|
|
|
|
#### Best patch host families
|
|
|
|
The most practical patch hosts are not generic gameplay code. They are hidden/debug-adjacent control points that already sit near cheat or diagnostic behavior.
|
|
|
|
Best current candidates:
|
|
|
|
- `Key_HandleOptionKeys` at `1148:0a9a`
|
|
- `Key_CheckSecretCodeSequences` at `1148:34d2`
|
|
|
|
Why these are the strongest patch hosts:
|
|
|
|
- they already belong to hidden/debug functionality rather than normal gameplay
|
|
- they already execute in live gameplay input conditions
|
|
- they are much smaller intervention points than rebuilding the retail interpreter patch family
|
|
|
|
Current concrete patch shapes worth considering:
|
|
|
|
1. repurpose one hidden secret-code completion lane to call `usecode_debugger_open_modal`
|
|
2. repurpose one hidden secret-code completion lane to call `usecode_debugger_bootstrap_init` if needed and then set the break/step bytes for the next interpreter break
|
|
3. repurpose one hidden option-key branch inside `Key_HandleOptionKeys` to do the same thing behind an intentional key combo
|
|
|
|
#### Why this is smaller than retail patching
|
|
|
|
In retail No Remorse, the smallest viable patch still had to recreate missing state bootstrap and missing live callback behavior. In Regret, those pieces are already there.
|
|
|
|
So the Regret patch problem is no longer `invent a debugger from fragments`. It is `redirect an existing hidden/debug input lane into an already-live debugger subsystem`.
|
|
|
|
That is a much smaller and more defensible hack surface.
|
|
|
|
### 2. Live memory forcing is viable, but only if the debugger object is actually live
|
|
|
|
Memory-only forcing is now plausible in Regret in a way that was not cleanly plausible in retail.
|
|
|
|
But there is one major condition:
|
|
|
|
- the debugger object and support buffers must already be initialized
|
|
|
|
If `usecode_debugger_bootstrap_init` has not run, then a raw memory poke to `1480:712c/712e` is not enough by itself. The bootstrap allocates support buffers, constructs the base object, and performs the vtable override. A pure `write one pointer` memory hack would skip too much setup.
|
|
|
|
#### Best memory-only open path if the object is live
|
|
|
|
If the object already exists and `1480:712c/712e` is valid, then the simplest trainer/debugger intervention is:
|
|
|
|
- call `usecode_debugger_open_modal`
|
|
|
|
This is the safest menu-only force path because it avoids needing a valid current-entry index first.
|
|
|
|
#### Best memory-only real-debug path if the object is live
|
|
|
|
If the goal is a real current-unit break rather than just a menu shell, the stronger live-memory path is:
|
|
|
|
- set the break/step state bytes in the debugger object
|
|
- let the next interpreted usecode activity reach `usecode_debugger_interpreter_hook`
|
|
|
|
The strongest currently evidenced control bytes are:
|
|
|
|
- byte `+0x74`: likely break-next / force-break style latch
|
|
- byte `+0x75`: single-step style latch
|
|
- words `+0x76` and `+0x78`: cleared by `usecode_debugger_break_state_enable_single_step`
|
|
|
|
Evidence:
|
|
|
|
- bootstrap sets `+0x74 = 1`
|
|
- `usecode_debugger_break_state_clear_runtime_break_flags` clears `+0x74/+0x75`
|
|
- `usecode_debugger_break_state_enable_single_step` sets `+0x75 = 1` and zeros `+0x76/+0x78`
|
|
- `usecode_debugger_handle_event` contains paths that also manipulate `+0x74/+0x75`
|
|
|
|
Current safest read is therefore:
|
|
|
|
- setting single-step or break-next state in memory and then provoking the next live usecode opcode is a plausible force path in Regret
|
|
- but it depends on the debugger object already being real and on the current interpreter context being valid
|
|
|
|
#### Main limitation of memory-only forcing
|
|
|
|
Memory-only forcing is practical for a debugger/trainer workflow, but it is not the best first patch if the goal is a reusable mod or stable reproduction path. It depends on runtime timing and on the hidden object already existing.
|
|
|
|
### 3. Pure usecode-only forcing is still not evidenced
|
|
|
|
This remains the weakest route.
|
|
|
|
Current negative evidence:
|
|
|
|
- no recovered usecode-visible primitive constructs the debugger break-state object
|
|
- no recovered usecode-visible primitive writes `1480:712c/712e`
|
|
- no recovered usecode-visible primitive directly calls `usecode_debugger_open_modal` or `usecode_debugger_open_for_current_unit`
|
|
- the hidden secret-code and option-key lanes that are currently recovered are still compiled cheat/debug code, not usecode-side entry points
|
|
|
|
So the current safest conclusion is:
|
|
|
|
> pure usecode by itself is still not an evidence-backed way to bring the Regret debugger up.
|
|
|
|
### 4. A usecode-assisted hybrid is plausible and may be the cleanest experimental setup
|
|
|
|
Even though pure usecode does not currently look sufficient, usecode can still be useful as part of a hybrid forcing plan.
|
|
|
|
The strongest hybrid model is:
|
|
|
|
- use a tiny executable patch or runtime memory intervention to ensure the debugger object exists and the break/step latch is armed
|
|
- use a replacement `EUSECODE.FLX` or a known script trigger to force predictable nearby usecode execution
|
|
- let the next interpreter-side debugger break land in a controlled current-unit context
|
|
|
|
This hybrid is stronger than pure usecode because it uses usecode only for what the current evidence actually supports: providing predictable runtime context after the compiled debugger path has already been armed.
|
|
|
|
That means `-u` can still matter in Regret, but not as `open debugger from script`. Its best role is:
|
|
|
|
- generate deterministic usecode activity once the debugger break/step machinery has already been forced by code or memory.
|
|
|
|
### 5. Ranked Regret force options
|
|
|
|
If the goal is simply `make the debugger menu appear`, the current ranking is:
|
|
|
|
1. small executable patch that routes a hidden key/sequence lane into `usecode_debugger_open_modal`
|
|
2. live memory/trainer call into `usecode_debugger_open_modal` if the object is already initialized
|
|
3. small executable patch that arms break/step state and lets the next interpreter break auto-open `usecode_debugger_open_for_current_unit`
|
|
4. live memory forcing of break/step bytes plus a controlled next usecode opcode
|
|
5. usecode-assisted hybrid using `-u` only after one of the code/memory force paths is in place
|
|
6. pure usecode-only forcing
|
|
|
|
If the goal is specifically `open on the current unit with the real debugger context`, the order changes slightly:
|
|
|
|
1. executable patch that ensures bootstrap and then forces break/step state
|
|
2. live memory forcing of break/step state if the debugger object is already present
|
|
3. hybrid `code/memory force + -u` setup to land in a deterministic usecode context
|
|
4. direct `usecode_debugger_open_for_current_unit` call from a patch host if runtime current-entry state is known good
|
|
5. direct `usecode_debugger_open_modal`
|
|
|
|
### 6. Bottom line on forcing the Regret debugger
|
|
|
|
The main result of this pass is that No Regret is no longer just an evidence source for retail comparison. It is also the first build where a practical forced debugger bring-up looks realistically hackable without rebuilding half the subsystem.
|
|
|
|
Current safest forcing conclusion:
|
|
|
|
- executable patching is the strongest practical route
|
|
- live memory forcing is plausible if the object already exists
|
|
- usecode is useful only as a hybrid context-generator, not yet as a direct launcher
|
|
|
|
If the immediate goal is `bring the menu up by any reasonable hack`, the best current target is a small Regret-specific patch from a hidden cheat/input lane into `usecode_debugger_open_modal` or into the break/step auto-open path.
|
|
|
|
## Live DOSBox-X Runtime Check
|
|
|
|
After the static analysis, a first real DOSBox-X debugger pass was used to test the least-invasive runtime ideas directly.
|
|
|
|
These results materially narrow the practical options.
|
|
|
|
### 1. The debugger object is not live during ordinary gameplay
|
|
|
|
Using the DOSBox-X debugger's data view on the runtime data selector:
|
|
|
|
- `D 05FF 712C`
|
|
|
|
the first four bytes at `05FF:712C` were:
|
|
|
|
- `00 00 00 00`
|
|
|
|
That means the runtime debugger-object pointer is still null in ordinary gameplay at the tested point.
|
|
|
|
This is an important practical constraint:
|
|
|
|
- the poke-only single-step plan cannot be the first move in ordinary gameplay, because there is no live object to poke yet.
|
|
|
|
### 2. Known hidden input lanes did not initialize the object
|
|
|
|
Two runtime checks were performed after confirming the pointer was null.
|
|
|
|
#### `loosecannon`
|
|
|
|
After entering the real No Regret cheat-enable sequence and re-checking:
|
|
|
|
- `D 05FF 712C`
|
|
|
|
the pointer remained:
|
|
|
|
- `00 00 00 00`
|
|
|
|
#### F10 option-key lane
|
|
|
|
After triggering the Regret F10 cheat lane and re-checking:
|
|
|
|
- `D 05FF 712C`
|
|
|
|
the pointer still remained:
|
|
|
|
- `00 00 00 00`
|
|
|
|
Current safest conclusion from those live tests:
|
|
|
|
- neither the main hidden cheat-enable sequence nor the known F10 option-key cheat lane initializes the debugger object in ordinary gameplay.
|
|
|
|
That materially weakens the earlier hope that a purely existing hidden input path might already seed the debugger for us.
|
|
|
|
### 3. The runtime vtable selectors were still valuable to recover
|
|
|
|
Even though the first live forcing attempts did not succeed, the DOSBox-X reads still recovered the runtime code selectors for the debugger family.
|
|
|
|
Reading the vtable roots directly from the live data segment:
|
|
|
|
- `D 05FF 6972` -> `86 00 17 05 91 02 17 05`
|
|
- `D 05FF 713B` -> `6F 04 5F 05 74 04 5F 05`
|
|
|
|
This gives the runtime selector mapping:
|
|
|
|
- live frontend debugger code selector = `0517`
|
|
- live base break-state helper selector = `055F`
|
|
|
|
and confirms the runtime slot mapping:
|
|
|
|
- `0517:0086` = `usecode_debugger_open_for_current_unit`
|
|
- `0517:0291` = `usecode_debugger_format_expression_to_shared_buffer`
|
|
- `055F:046F` = inert base slot `0`
|
|
- `055F:0474` = inert base slot `1`
|
|
|
|
So the DOSBox-X pass still strengthened the runtime model even though the forcing attempt failed.
|
|
|
|
### 4. Raw DOSBox-X far-call injection is currently not robust
|
|
|
|
Two manual far-call experiments were attempted through the DOSBox-X debugger.
|
|
|
|
#### Manual bootstrap call attempt
|
|
|
|
A manual far-call frame was built on the stack and execution was redirected to:
|
|
|
|
- `0517:0000` (`usecode_debugger_bootstrap_init`)
|
|
|
|
Result:
|
|
|
|
- execution entered the function body successfully
|
|
- but after return/break handling, the debugger pointer at `05FF:712C` still read as `00 00 00 00`
|
|
|
|
Current safest read:
|
|
|
|
- the raw DOSBox-X call injection method is not yet a trustworthy way to initialize the debugger object from an arbitrary paused gameplay state.
|
|
|
|
#### Manual `open_modal` call attempt
|
|
|
|
A second manual far-call frame was built and execution was redirected to:
|
|
|
|
- `0517:020D` (`usecode_debugger_open_modal`)
|
|
|
|
Result:
|
|
|
|
- DOSBox-X reported `DYNX86: Can't run code in this page!`
|
|
- the game effectively crashed / failed to continue cleanly
|
|
|
|
Current safest read:
|
|
|
|
- raw DOSBox-X debugger call injection into the Regret debugger frontend is currently too fragile to recommend as the primary least-invasive workflow.
|
|
|
|
This does **not** prove the underlying debugger functions are broken. It only shows that this particular external debugger-based call-injection method is unstable from the tested paused context.
|
|
|
|
## Concrete Manual Hex Retarget
|
|
|
|
The best first on-disk experiment is no longer a generic "patch some hidden key path" idea. Regret now has one specific low-risk family that stays inside existing hidden-sequence control flow and preserves the original caller stack discipline.
|
|
|
|
### Recommended patch family
|
|
|
|
Use the `loosecannon` secret-sequence completion lane in `1148:34d2 Key_CheckSecretCodeSequences`.
|
|
|
|
Important NE format note:
|
|
|
|
- the code sites themselves are on-disk `CALLF` placeholders: `9A FF FF 00 00`
|
|
- the real internal far-call target lives in the segment relocation record, not in the opcode immediate bytes
|
|
- so the manual hex patch should edit the fixup records, not the `9A FF FF 00 00` code bytes
|
|
|
|
Why this is the current best manual hex target:
|
|
|
|
- it is already a hidden debug-adjacent input path reached from ordinary gameplay
|
|
- it already plays a short side-effect call and then opens a modal message path
|
|
- the call shapes line up well enough to retarget without adding a trampoline
|
|
- the patch can stay at `10` bytes for the normal case, or `15` bytes if the Christmas-only alternate message lane is also covered
|
|
|
|
### Patch level 1: 5-byte smoke test
|
|
|
|
If the goal is only `prove that a tiny retarget can bring the debugger menu up`, the smallest first test is a single call-target replacement in the normal `loosecannon` active-message lane.
|
|
|
|
Code site for reference:
|
|
|
|
- live address `1148:3702`
|
|
- raw file offset `0x7A502`
|
|
- on-disk bytes `9A FF FF 00 00 83 C4 14`
|
|
|
|
Fixup record to edit:
|
|
|
|
- file offset `0x7BB05`
|
|
- old record `03 00 03 37 6B 00 46 00`
|
|
- new record `03 00 03 37 74 00 0D 02`
|
|
|
|
That changes the internal target from:
|
|
|
|
- segment index `0x006B`, offset `0x0046` = `1350:0046`
|
|
|
|
to:
|
|
|
|
- segment index `0x0074`, offset `0x020D` = `1398:020d` (`usecode_debugger_open_modal`)
|
|
|
|
Why this site is attractive:
|
|
|
|
- the call is reached only after the hidden sequence has completed and the cheat-active lane has been selected
|
|
- immediately before the call, the original code pushes two zero words
|
|
- `usecode_debugger_open_modal` is the safest known menu-only debugger entry because it does not require the current-unit preload path
|
|
|
|
Why the stack is plausibly safe here:
|
|
|
|
- the original message builder takes many arguments, but the final two pushed words at this site are both `0`
|
|
- retargeting to `usecode_debugger_open_modal` means the debugger wrapper should see `(0, 0)` as its first two arguments
|
|
- the caller already does `ADD SP,0x14` after return, so the extra message-builder arguments are still discarded cleanly
|
|
|
|
Main limitation of the 5-byte smoke test:
|
|
|
|
- ordinary gameplay still showed `1480:712c/712e = 0`, so `open_modal` may still depend on state that is not always ready
|
|
- this is the best tiny proof-of-life patch, but not the strongest first patch if the goal is `make it work reliably`
|
|
|
|
### Patch level 2: recommended 10-byte bring-up patch
|
|
|
|
If the goal is the smallest practical patch with a real chance to work from a clean ordinary gameplay state, use a two-call retarget in the same `loosecannon` path:
|
|
|
|
1. replace the ambient-sound call with `usecode_debugger_bootstrap_init`
|
|
2. replace the modal message call with `usecode_debugger_open_modal`
|
|
|
|
#### Site A: bootstrap retarget
|
|
|
|
Code site for reference:
|
|
|
|
- live address `1148:3678`
|
|
- raw file offset `0x7A478`
|
|
- on-disk bytes `9A FF FF 00 00 83 C4 02`
|
|
|
|
Fixup record to edit:
|
|
|
|
- file offset `0x7BB25`
|
|
- old record `03 00 79 36 5C 00 D0 04`
|
|
- new record `03 00 79 36 74 00 00 00`
|
|
|
|
That changes the internal target from:
|
|
|
|
- segment index `0x005C`, offset `0x04D0` = `12d8:04d0`
|
|
|
|
to:
|
|
|
|
- segment index `0x0074`, offset `0x0000` = `1398:0000` (`usecode_debugger_bootstrap_init`)
|
|
|
|
Why this site works well:
|
|
|
|
- it executes before the `loosecannon` lane branches into the active/inactive message paths
|
|
- the original call already has exactly one pushed word argument (`PUSH 1901`), and the caller already removes it with `ADD SP,0x2`
|
|
- `usecode_debugger_bootstrap_init` is currently treated as a no-argument helper, so the extra stack word should be ignored and then cleaned by the existing caller logic
|
|
|
|
What this changes functionally:
|
|
|
|
- the patch trades the one-shot confirmation sound for a chance to seed the debugger object and support buffers before the modal-open retarget runs
|
|
|
|
#### Site B: open-modal retarget
|
|
|
|
At `0x7BB05`, replace the fixup record:
|
|
|
|
- old: `03 00 03 37 6B 00 46 00`
|
|
- new: `03 00 03 37 74 00 0D 02`
|
|
|
|
This is the same site as the 5-byte smoke test, but paired with the bootstrap retarget it becomes the best current low-byte bring-up candidate.
|
|
|
|
### Patch level 2b: 15-byte Christmas-safe variant
|
|
|
|
The `loosecannon` active-message lane has a seasonal split:
|
|
|
|
- `1148:36c0` = Christmas-only message call
|
|
- `1148:3702` = normal active-message call
|
|
|
|
If the patch should work regardless of the `Date_IsAlmostChristmas` branch, also retarget `1148:36c0` the same way:
|
|
|
|
- code site for reference: `1148:36c0` = raw file offset `0x7A4C0` with on-disk bytes `9A FF FF 00 00 83 C4 14`
|
|
- fixup record at `0x7BB15`
|
|
- old record `03 00 C1 36 6B 00 46 00`
|
|
- new record `03 00 C1 36 74 00 0D 02`
|
|
|
|
That makes the full robust manual patch set:
|
|
|
|
- `1148:3678` -> bootstrap retarget
|
|
- `1148:3702` -> normal active-message open-modal retarget
|
|
- optional `1148:36c0` -> Christmas active-message open-modal retarget
|
|
|
|
Byte-budget summary:
|
|
|
|
- `5` bytes: menu-only smoke test
|
|
- `10` bytes: recommended normal-case bring-up patch
|
|
- `15` bytes: recommended bring-up patch plus Christmas coverage
|
|
|
|
### Why this family is better than the earlier raw DOSBox-X call injection
|
|
|
|
The DOSBox-X experiments showed that arbitrary external far-call injection was too fragile from a paused gameplay context. This retarget stays inside an already-valid in-engine call path instead:
|
|
|
|
- real gameplay input reaches `Key_CheckSecretCodeSequences`
|
|
- the original function prologue, data selectors, and stack frame are already valid
|
|
- the retargeted calls return into the original control flow rather than into a manually fabricated debugger stack frame
|
|
|
|
So even though the patch is still speculative until tested, it is materially better grounded than another raw debugger-console call injection attempt.
|
|
|
|
### Expected trigger flow
|
|
|
|
With the recommended `10`-byte patch in place, the expected test flow is:
|
|
|
|
1. boot the patched `REGRET.EXE`
|
|
2. enter gameplay normally
|
|
3. type `loosecannon`
|
|
4. the original sound call is replaced by `usecode_debugger_bootstrap_init`
|
|
5. the original active cheat-message call is replaced by `usecode_debugger_open_modal`
|
|
6. if the stack and UI assumptions hold, the hidden debugger menu should appear instead of the normal `Cheats are now active.` message
|
|
|
|
### Side effects and risk profile
|
|
|
|
This patch family is attractive because it avoids trampolines, new code, and interpreter edits, but it still has visible tradeoffs:
|
|
|
|
- entering `loosecannon` still flips the normal cheat-active latches first
|
|
- the original confirmation sound is lost at the bootstrap-retarget site
|
|
- the original cheat-active dialog is replaced by the debugger modal
|
|
- if `usecode_debugger_bootstrap_init` has hidden caller assumptions that were not recovered statically, the `1148:3678` retarget could still fail or destabilize the lane
|
|
|
|
Even so, this remains the best current manual-hex candidate because every other known route either needs more bytes, needs an injected trampoline, or depends on a runtime object that is still null in ordinary gameplay.
|
|
|
|
### If the 10-byte patch fails
|
|
|
|
The next fallback should still stay in the same family rather than jumping straight to a wider rewrite.
|
|
|
|
Recommended fallback order:
|
|
|
|
1. add the Christmas-safe `1148:36c0` retarget if only the normal lane was patched
|
|
2. keep the bootstrap retarget and move the open retarget to a different hidden-sequence message site with the same `CALLF 1350:0046` argument shape
|
|
3. only after that, consider a low-teens-byte local trampoline that does `bootstrap_init` and `open_modal` back-to-back explicitly
|
|
|
|
The important point is that the first practical Regret patch should still be a call-target retarget, not an interpreter rewrite.
|
|
|
|
## Least-Invasive Short List
|
|
|
|
Given the practical constraint that larger patches tend to break startup, the current Regret options compress to a much shorter ranked list.
|
|
|
|
### Best overall: live memory call hack
|
|
|
|
If the goal is `make the debugger appear with the fewest moving parts`, the current best candidate is a live memory call sequence rather than an on-disk EXE patch.
|
|
|
|
#### Menu-only sequence
|
|
|
|
Current best minimal sequence:
|
|
|
|
1. if `1480:712c/712e` is null, call `usecode_debugger_bootstrap_init`
|
|
2. call `usecode_debugger_open_modal(0, 0)`
|
|
|
|
Why this is currently the cleanest candidate:
|
|
|
|
- it is only one or two far calls
|
|
- it does not need a persistent EXE modification
|
|
- it avoids relying on current-unit state being valid first
|
|
- it reuses the already-working Regret gump/event machinery directly
|
|
|
|
Current practical status after DOSBox-X testing:
|
|
|
|
- in theory this is still the smallest call sequence
|
|
- in practice, ordinary gameplay had a null debugger pointer, and the raw DOSBox-X manual-call method did not initialize it reliably
|
|
|
|
So this remains the smallest **theoretical** path, but not the smallest **validated DOSBox-X workflow**.
|
|
|
|
#### Current-unit sequence
|
|
|
|
If the goal is `open the real current-unit debugger rather than just show the menu shell`, the current best live-memory sequence is:
|
|
|
|
1. if `1480:712c/712e` is null, call `usecode_debugger_bootstrap_init`
|
|
2. call `usecode_debugger_break_state_enable_single_step(DAT_1480_712c)`
|
|
3. provoke the next usecode/interpreter activity
|
|
|
|
Why this is stronger than a direct call to `usecode_debugger_open_for_current_unit`:
|
|
|
|
- it uses the already-recovered Regret runtime path instead of cold-entering the wrapper
|
|
- it should land through the real interpreter-side hook and slot-`0` auto-open path
|
|
- it gives the best chance of valid current-unit/current-line state on entry
|
|
|
|
Current practical status after DOSBox-X testing:
|
|
|
|
- this is still structurally the cleanest `real debugger context` path
|
|
- but it is blocked in ordinary gameplay until some method really seeds the debugger object first
|
|
|
|
### Best poke-only candidate: 3 to 4 writes plus one usecode trigger
|
|
|
|
If the tooling can poke memory but not call functions, the current least invasive poke recipe is the single-step path.
|
|
|
|
Current best candidate writes inside the object pointed to by `1480:712c/712e`:
|
|
|
|
- byte `+0x75 = 1`
|
|
- word `+0x76 = 0`
|
|
- word `+0x78 = 0`
|
|
|
|
Optional fourth write if needed for stability testing:
|
|
|
|
- byte `+0x74 = 0`
|
|
|
|
Then provoke one nearby live usecode action.
|
|
|
|
Why this is the best poke-only candidate:
|
|
|
|
- it mirrors the verified helper `usecode_debugger_break_state_enable_single_step`
|
|
- it is only 3 required writes, 4 at most
|
|
- it tries to reuse the real Regret interpreter path rather than bypassing it
|
|
|
|
Main limitation:
|
|
|
|
- this still assumes the debugger object already exists
|
|
- if `1480:712c/712e` is null, this poke-only plan is not enough by itself
|
|
|
|
Current live status:
|
|
|
|
- ordinary gameplay checks in DOSBox-X found `1480:712c/712e` still null even after `loosecannon` and the F10 cheat lane
|
|
- so this poke-only recipe is currently a second-stage trick, not a first-stage bring-up
|
|
|
|
### Best tiny EXE patch family: retarget one hidden cheat/input lane
|
|
|
|
If an on-disk patch is acceptable only when it is very small, the current best family is still a key/sequence-lane retarget rather than any interpreter rewrite.
|
|
|
|
#### Current smallest credible patch shape
|
|
|
|
Current best concept:
|
|
|
|
- hijack one hidden secret-sequence completion lane in `Key_CheckSecretCodeSequences`
|
|
- or hijack one hidden option-key lane in `Key_HandleOptionKeys`
|
|
- route it into `usecode_debugger_open_modal`
|
|
- optionally call `usecode_debugger_bootstrap_init` first if runtime testing shows the object is not always live
|
|
|
|
Why this remains the smallest credible EXE family:
|
|
|
|
- these hosts already sit in hidden/debug control paths
|
|
- they are already reachable from normal gameplay input
|
|
- they avoid touching the interpreter core, startup path, or loader path
|
|
|
|
#### Byte-budget estimate
|
|
|
|
Current realistic byte budgets are:
|
|
|
|
- about `5` bytes if a single existing `CALLF` target can be retargeted cleanly
|
|
- about `10-16` bytes if one additional local cleanup/skip patch is needed after the redirected call
|
|
- larger than that only if runtime testing proves `usecode_debugger_bootstrap_init` must also be injected into the same path
|
|
|
|
Current safest reading of those budgets:
|
|
|
|
- a true one-call retarget is the ideal target
|
|
- a two-site patch in the low-teens of bytes is still likely acceptable under the current `few bytes only` constraint
|
|
- anything that grows into a mini-trampoline or interpreter rewrite is no longer in the preferred class
|
|
|
|
#### Best first executable candidates
|
|
|
|
Current best on-disk patch hosts remain:
|
|
|
|
- one hidden secret-code completion lane in `Key_CheckSecretCodeSequences`
|
|
- one hidden option-key lane in `Key_HandleOptionKeys`
|
|
|
|
Current best behavior targets remain:
|
|
|
|
- `usecode_debugger_open_modal` for the smallest reliable menu bring-up
|
|
- `usecode_debugger_break_state_enable_single_step` if the goal is to reuse the existing auto-open path and keep the patch logic tiny
|
|
|
|
Current practical note after DOSBox-X runtime testing:
|
|
|
|
- because the object stayed null through normal gameplay and the raw DOSBox-X far-call method was unstable, a very small executable retarget now looks *more* attractive than a complicated external live-call recipe for the first reproducible bring-up attempt.
|
|
|
|
### Weakest option under the current constraints: pure usecode soft-mod
|
|
|
|
Soft-modding through usecode is still the safest class operationally, but it is not the strongest class evidentially.
|
|
|
|
Current best read remains:
|
|
|
|
- pure usecode cannot currently bootstrap the debugger object
|
|
- pure usecode cannot currently write `1480:712c/712e`
|
|
- pure usecode cannot currently call the debugger wrappers directly
|
|
|
|
So under the current `least invasive` constraint, usecode only becomes competitive if it is paired with one tiny compiled or live-memory intervention first.
|
|
|
|
### Final least-invasive ranking
|
|
|
|
For `just make the menu appear`:
|
|
|
|
1. tiny EXE patch that routes one hidden key/sequence lane into `open_modal`
|
|
2. live memory call hack through a smarter in-process tool, not raw DOSBox-X far-call injection: `bootstrap if needed -> open_modal`
|
|
3. tiny EXE patch that routes one hidden key/sequence lane into `enable_single_step`
|
|
4. live memory poke hack: single-step bytes then trigger one usecode action, but only after some other method creates the object
|
|
5. usecode-assisted hybrid after one tiny compiled or live-memory intervention
|
|
6. pure usecode-only forcing
|
|
|
|
For `open the real current-unit debugger with context`:
|
|
|
|
1. tiny EXE patch into the single-step auto-open path
|
|
2. live memory call hack through a smarter in-process tool: `bootstrap if needed -> enable_single_step -> trigger usecode`
|
|
3. poke-only single-step recipe on an already-live object
|
|
4. direct `open_for_current_unit` call if runtime context is already known good
|
|
5. pure menu-open via `open_modal`
|
|
|
|
Under the user's current stability constraint, the strongest recommendation is therefore:
|
|
|
|
> prefer the smallest possible hidden-input EXE retarget first if it can truly stay in the single-digit or low-teens byte range. Raw DOSBox-X far-call injection is currently too fragile to be the primary workflow, and poke-only memory forcing still cannot start from a null object.
|
|
|
|
## What The `.unk` File Actually Looks Like
|
|
|
|
Now that the patched Regret build can open the hidden debugger menu, the next blocker is the file-open prompt. The immediate practical question is whether that prompt wants one of the stock `UNK*.DAT` files or some other artifact.
|
|
|
|
Current best answer: the debugger does **not** appear to want `UNKDS.DAT` directly. It wants a per-unit `.unk` file under the `USECODE` root, and that file is expected to be a plain text source-style listing, not a small binary metadata blob.
|
|
|
|
### Short answer
|
|
|
|
- the debugger builds `<current-unit-name>.unk` under `s_usecode`
|
|
- the debugger-side source loader opens that path as a normal file, reads the entire file into memory, splits it into lines, and rejects files that do not look like text
|
|
- `UNKCOFF.DAT` is a separate fixed archive-like table of 4-byte code pointers used by the older `unkcoffs` tooling
|
|
- `UNKDS.DAT` is a second tiny fixed sidecar loaded by a different code path; it is probably related to the same broader usecode/debug data family, but it is **not** the same thing as the `.unk` unit file the menu is asking for
|
|
|
|
So the current safest read is:
|
|
|
|
> `.unk` is a human-readable per-unit debugger/source listing, while `UNKCOFF.DAT` and `UNKDS.DAT` are shared support data files.
|
|
|
|
### 1. The debugger explicitly appends `.unk` to the current unit name
|
|
|
|
The retail exported source for `usecode_debugger_open_for_current_unit` still shows the clearest shape, and the Regret wrapper is the relocated counterpart.
|
|
|
|
Recovered flow:
|
|
|
|
- fetch current callstack entry unit name via `UsecodeDebuggerBreakState::CurrentEntryGetUnitName`
|
|
- call `Filespec_GetFullPath(0, s_usecode, unit_name, ".unk")`
|
|
- if that resolved path differs from the currently loaded file, call `usecode_debugger_load_unit_file(...)`
|
|
|
|
That is strong direct evidence that the debugger wants a filename of the form:
|
|
|
|
- `USECODE\<unitname>.unk`
|
|
|
|
not one of the fixed shared filenames like `UNKDS.DAT`.
|
|
|
|
### 2. The `.unk` loader expects a text file, not a tiny binary blob
|
|
|
|
The debugger-side file path goes through:
|
|
|
|
- `usecode_debugger_load_unit_file`
|
|
- `usecode_debugger_source_file_create_or_open`
|
|
- `usecode_debugger_source_file_load_path`
|
|
- `Debugump_13a0_2e0a`
|
|
|
|
What `Debugump_13a0_2e0a` does is the key format clue:
|
|
|
|
- seeks to the file end to get its size
|
|
- allocates a buffer for the whole file plus terminator space
|
|
- reads the whole file into memory
|
|
- runs a line-splitting pass that walks the buffer looking for `\n`
|
|
- replaces the byte before each newline with `0`, which is consistent with stripping CRLF text lines into C strings
|
|
- stores per-line pointers for later indexed access through `usecode_debugger_source_file_get_line_text`
|
|
- shows `This is probably not a text file` if the loaded content fails its text-like sanity check
|
|
|
|
This is not the behavior of a parser for a small binary metadata file. It is the behavior of a source/listing viewer for line-oriented text.
|
|
|
|
Current safest format read:
|
|
|
|
- `.unk` should be an ASCII or DOS-text file
|
|
- line-oriented
|
|
- newline-delimited, very likely CRLF-friendly
|
|
- small enough to fit the fixed debugger source buffer and line table
|
|
|
|
### 3. No `.unk` samples are currently present in the workspace
|
|
|
|
A workspace-wide search for `*.unk` returned no files.
|
|
|
|
That means the current evidence is asymmetric:
|
|
|
|
- the loader shape is clear
|
|
- the expected filename suffix is clear
|
|
- but no stock sample `.unk` file has been recovered yet from the current repo/game folders
|
|
|
|
So the format conclusion is based on the loader and parser behavior, not on a known-good sample file.
|
|
|
|
### 4. What `UNKCOFF.DAT` appears to be
|
|
|
|
The existing `tools/unkcoffs` utilities are the strongest clue here.
|
|
|
|
Those scripts:
|
|
|
|
- read `UNKCOFF.DAT` as raw 4-byte integers
|
|
- split each entry into `segment` and `offset`
|
|
- convert them into NE far addresses like `1000 + (seg - 1) * 8 : off`
|
|
- align the resulting entries with intrinsic name dumps such as `reg_functions.txt`, `rem_functions.txt`, `regret_ints.py`, and `remorse_ints.py`
|
|
|
|
So `UNKCOFF.DAT` is not a text source file. It is a compact offset table.
|
|
|
|
Verified current sizes:
|
|
|
|
- Remorse `USECODE\UNKCOFF.DAT` = `1244` bytes = `311` 4-byte entries
|
|
- Regret `USECODE\REGRET\UNKCOFF.DAT` = `1400` bytes = `350` 4-byte entries
|
|
|
|
Current safest interpretation:
|
|
|
|
- `UNKCOFF.DAT` is a shared intrinsic or function-offset table used to map usecode ordinals to compiled handlers
|
|
- it belongs to the broader usecode/debug symbol ecosystem
|
|
- but it is not the per-unit `.unk` text listing the debugger menu is trying to open
|
|
|
|
This also explains the long-standing `unkcoffs/` tooling and naming in the repo: that tooling is built around `UNKCOFF.DAT` as an address table.
|
|
|
|
### 5. What `UNKDS.DAT` appears to be
|
|
|
|
`UNKDS.DAT` is opened by a separate fixed-filename loader in the old retail export lane near `seg_1410`, not by the debugger's unit-file wrapper.
|
|
|
|
What is currently verified:
|
|
|
|
- the executable contains a literal `unkds.dat` string
|
|
- a dedicated startup/helper path opens that fixed file under `s_usecode`
|
|
- the loaded blob is tiny compared with `UNKCOFF.DAT`
|
|
|
|
Verified current sizes:
|
|
|
|
- Remorse `USECODE\UNKDS.DAT` = `66` bytes
|
|
- Regret `USECODE\REGRET\UNKDS.DAT` = `36` bytes
|
|
|
|
That size profile does **not** match the debugger's line-oriented unit-source loader.
|
|
|
|
Current safest interpretation:
|
|
|
|
- `UNKDS.DAT` is some small shared sidecar for the same broad usecode-debug/intrinsic ecosystem
|
|
- it may be a descriptor table, count/header block, or small symbol-side metadata blob
|
|
- but it is not a drop-in substitute for `<unit>.unk`
|
|
|
|
### 6. `UNKOFF.DAT` vs `UNKCOFF.DAT`
|
|
|
|
The files present in the repo and game folders are named:
|
|
|
|
- `UNKCOFF.DAT`
|
|
- `UNKDS.DAT`
|
|
|
|
The older tooling and docs also consistently use the `unkcoffs/` name.
|
|
|
|
So if `UNKOFF.DAT` was seen elsewhere, the safest current read is:
|
|
|
|
- either a typo
|
|
- or a shorthand reference to the same `UNKCOFF.DAT` family
|
|
|
|
There is no current workspace evidence for a separate stock file literally named `UNKOFF.DAT`.
|
|
|
|
### 7. Practical implication for the live Regret debugger
|
|
|
|
Now that the menu opens, the next practical consequence is straightforward:
|
|
|
|
- copying `UNKCOFF.DAT` or `UNKDS.DAT` into the open-file prompt is unlikely to satisfy the debugger's unit-source viewer
|
|
- the debugger most likely wants a text file named after the current usecode unit, for example something structurally like `<unitname>.unk`
|
|
|
|
Because no sample `.unk` corpus is currently present, the best next move is not to guess blindly at `UNKDS.DAT`. The better next move is:
|
|
|
|
1. capture the exact current-unit name the debugger is asking for
|
|
2. locate or reconstruct a matching text listing for that unit
|
|
3. only treat `UNKCOFF.DAT` / `UNKDS.DAT` as support data for reconstruction, not as direct replacements
|
|
|
|
### 7.5. First manufactured `.unk` corpus
|
|
|
|
That reconstruction step is now far enough along to try in practice.
|
|
|
|
The original Python generator at `tools/generate_usecode_unk.py` established the first manufactured corpus, but the JavaScript exporter in the map-viewer project is now the version that should be used for development and testing going forward.
|
|
|
|
Current active workflow:
|
|
|
|
- authoritative development path = `Crusader-Map-Viewer/map_renderer/src/lib/usecode-unk-exporter.js`
|
|
- cache pipeline owner = `Crusader-Map-Viewer/map_renderer/src/lib/usecode-decompiler.js`
|
|
- JS-built `.unk` outputs now live in the map-viewer cache under `.cache/usecode/<game>/<source>/.data/`, next to the decompiled pseudocode tree
|
|
- Python generator remains useful as historical reference and fallback, but ongoing `.unk` iteration should be validated against the JavaScript exporter output
|
|
|
|
Operational note:
|
|
|
|
- the map-viewer `build-usecode` command only refreshes `.cache/usecode/...` output
|
|
- the intended development/testing corpus is the cache-local `.data` output, not a copy written back into the Crusader repo
|
|
- this matters for debugger bring-up because the old cache layout mixed `.unk` files into the pseudocode tree and encouraged the wrong sync workflow
|
|
|
|
Current generated Regret corpus:
|
|
|
|
- output root: `.cache/usecode/regret/EUSECODE/.data`
|
|
- manifest: `.cache/usecode/regret/EUSECODE/.data/SYNTH_UNK_MANIFEST.tsv`
|
|
- synthesized files written: `477`
|
|
|
|
Current shape of each generated `.unk` file:
|
|
|
|
- filename = uppercased eight-character unit/class name, for example `ALARMHAT.unk` or `EVENT.unk`
|
|
- when recovered debug line markers exist, the top of the file is reserved for sparse line-mapped content keyed to those original line numbers
|
|
- after that, each file appends readable reconstructed pseudocode for every decoded slot in that class
|
|
|
|
Current practical limitation:
|
|
|
|
- in the present Regret extracted corpus, the parsed class bodies examined so far appear to have `0` surviving `line_number` op markers
|
|
- that means the generated Regret `.unk` files currently behave mostly as readable reconstructed source appendices, not as truly line-accurate original debug-source replicas
|
|
|
|
Current loader-side constraint that matters for the crash:
|
|
|
|
- live decompile of `1398:2f4f` shows the debugger line splitter writes `0` to the byte immediately before each `\n`
|
|
- that means a file must not begin with a bare newline, or the splitter underwrites one byte before the buffer start
|
|
- it also strongly indicates the debugger expects DOS-style `CRLF` line endings so the overwritten byte is the `\r`, not the last visible character of the line
|
|
- this makes the previous sparse-output shape unsafe for real debugger bring-up whenever the file began with blank padding or with an appendix-only body
|
|
|
|
Current executable-side `RUN` findings:
|
|
|
|
- `usecode_debugger_handle_event` case `3` is the real `RUN` path; it does **not** execute or parse source text, it clears the debugger object's runtime break latches and resumes through the gump callback
|
|
- the post-resume debugger path still depends on source-line identity: `usecode_debugger_open_for_current_unit` recenters the viewer on debugger-state line `+0x72 - 1`, and the source panel / breakpoint toggles compare against loaded file path plus 1-based line numbers
|
|
- shipped retail Remorse usecode is fully line-mapped in the current parser: `977 / 977` events contain `LINE_NUMBER` ops, with a highest recovered line number of `2991`
|
|
- current retail Regret `EUSECODE.FLX` does **not** yield any recovered `LINE_NUMBER` ops in the present parser pass: `1152 / 1152` events currently show `0` `LINE_NUMBER` ops total
|
|
- because the debugger still drives by runtime line numbers even when the extracted Regret source lacks recovered line markers, a cache-built Regret `.unk` needs a synthetic dense line table floor if it is going to participate in `RUN` / break-next / single-step flows at all
|
|
- but the file is only one requirement: `usecode_debugger_open_modal` merely creates the debugger gump, while `usecode_debugger_break_state_create` initializes the current-entry depth at `+0x7a = 0`; a truly runnable session still needs a real current-entry record and current line, not just a loadable `.unk`
|
|
- current live result after adding the synthetic line floor: the `.unk` now opens cleanly, but `RUN` still crashes, which is consistent with the remaining missing debugger-state requirement rather than with a remaining text-file parser failure
|
|
|
|
That limitation matters for breakpoint fidelity, but it does **not** block the basic experiment the user actually needs next:
|
|
|
|
- the debugger's file loader only needs a line-oriented text file to open and display
|
|
- these synthesized `.unk` files now provide that text payload in the exact filename family the debugger expects
|
|
- the rebuilt Regret cache corpus currently stays well under the loader's average-line-length rejection threshold, with the highest sampled file at about `50.74` bytes per parsed line versus the fail cutoff at `0x84`
|
|
- the JS exporter now pads Regret classes that have no recovered debug lines with a synthetic `2991`-line scaffold, capped so the total indexed line table stays within the debugger's `5999`-line source limit before the appendix is appended
|
|
|
|
So the current live experiment surface is no longer hypothetical: the patched Regret build can now be tested directly against manufactured per-unit `.unk` files copied from the map-viewer cache under `.cache/usecode/regret/EUSECODE/.data`.
|
|
|
|
### 8. Best current synthesis
|
|
|
|
The current evidence fits one coherent model:
|
|
|
|
- `UNKCOFF.DAT` = shared far-pointer table for usecode/intrinsic mapping
|
|
- `UNKDS.DAT` = small shared sidecar in the same broader ecosystem
|
|
- `<unit>.unk` = per-unit text listing that the hidden debugger actually displays
|
|
|
|
That model matches all of the currently recovered evidence:
|
|
|
|
- filename construction in the debugger wrapper
|
|
- text-file parsing and line indexing in `Debugump_13a0_2e0a`
|
|
- the older `unkcoffs` tooling
|
|
- the very small size of `UNKDS.DAT`
|
|
- and the complete absence of any stock `.unk` files in the current workspace
|
|
|
|
## What Proper Debugger State Requires
|
|
|
|
The file-format work answered only the loader side of the problem. The deeper runtime pass now shows that a loadable `.unk` file is necessary, but it is **not** sufficient for a stable debugger session.
|
|
|
|
Current safest model:
|
|
|
|
- the debugger has one global break-state object at `1480:712c/712e`
|
|
- the source viewer reads text from the loaded `.unk` file through `DAT_1480_64ec/64ee`
|
|
- the debugger runtime state that drives `RUN`, `break next`, `single step`, current-unit open, watches, and inspect is stored in the break-state object, not in the `.unk` file
|
|
- `usecode_debugger_open_modal` only creates and displays the debugger window
|
|
- `usecode_debugger_open_for_current_unit` is stronger because it expects a valid current-entry stack and current line, resolves the unit name from that stack, and then loads the matching `.unk`
|
|
|
|
So the current Regret result is now split cleanly into two separate requirements:
|
|
|
|
1. text/source requirement: loadable `.unk` file with a usable line table
|
|
2. runtime-context requirement: valid break-state current-entry records plus current line and break/step control state
|
|
|
|
The current JS exporter work now satisfies the first requirement well enough for file open and source display. The remaining blocker for `RUN` is the second requirement.
|
|
|
|
### 1. What lives in the break-state object
|
|
|
|
The break-state constructor at `13e0:0000` zeroes and initializes the object, including the debugger-current-entry depth at `+0x7a`.
|
|
|
|
Current verified fields that matter most:
|
|
|
|
- `+0x72` = current source line, stored zero-based by `usecode_debugger_break_state_update_line_and_maybe_break`
|
|
- `+0x74` = break-next style runtime latch
|
|
- `+0x75` = single-step latch
|
|
- `+0x76/+0x78` = step countdown / timing words used by the step logic
|
|
- `+0x7a` = current-entry depth/count
|
|
- `+0x7c ...` = current-entry stack, one entry per frame-like debugger context
|
|
|
|
Current-entry pointer rule:
|
|
|
|
- `usecode_debugger_break_state_get_current_entry` returns `break_state + depth * 0x15 + 0x67`
|
|
- with depth `1`, that lands at `break_state + 0x7c`, which is the first current-entry record
|
|
|
|
### 2. Current-entry record layout
|
|
|
|
The record push helper at `13e0:02f5` and the copy helper at `13e0:03b0` now make the current-entry shape visible enough to describe.
|
|
|
|
Current safest layout for one current-entry record (`0x15` bytes total):
|
|
|
|
- `+0x00 .. +0x08` = unit name string, max 8 chars plus terminating `0`
|
|
- `+0x09 .. +0x14` = three packed runtime dwords copied from live interpreter context
|
|
|
|
What is firmly verified about those packed fields:
|
|
|
|
- they are **not** just cosmetic line metadata
|
|
- `13e0:02f5` pushes them as three dwords after the unit string and increments `+0x7a`
|
|
- `1398:045c` consumes at least the first two packed dwords when evaluating watch / inspect expressions, which means those fields carry real runtime context needed for symbol/value resolution
|
|
- `13e0:03f7` decrements `+0x7a` on interpreter-side unwind, so the current-entry stack is meant to track live nested interpreter context rather than merely loaded source files
|
|
|
|
Current safest interpretation:
|
|
|
|
- the current-entry record is a live interpreter/debug frame descriptor: unit name plus several runtime context pointers/handles
|
|
- it is intended to be pushed on entry to a debugger-relevant usecode context and popped on exit
|
|
|
|
### 3. Who seeds the records
|
|
|
|
This answer is now partly verified and partly inferred.
|
|
|
|
What is directly verified:
|
|
|
|
- `usecode_debugger_bootstrap_init` seeds the **object itself** and stores it into `1480:712c/712e`
|
|
- `13e0:02f5` seeds a **new current-entry record** from `(unit name, three runtime dwords)`
|
|
- `13e0:03b0` seeds a **new current-entry record** by copying an already-built `0x15`-byte record wholesale
|
|
- `13e0:03f7` pops one current-entry record on interpreter-side unwind
|
|
- `usecode_debugger_interpreter_hook` is the live interpreter-side consumer that owns the pop on exit
|
|
|
|
What is not yet fully recovered as a normal xref in the current MCP session:
|
|
|
|
- the exact direct callsite that invokes `13e0:02f5` or `13e0:03b0`
|
|
|
|
Current best evidence-based conclusion anyway:
|
|
|
|
- the record seeding is almost certainly performed by interpreter-side instrumentation around real usecode entry, not by the source loader and not by the modal-open wrapper
|
|
- the push/pop pairing strongly points to a runtime-enter / runtime-leave model: enter usecode context -> push record -> run -> unwind -> pop record
|
|
|
|
So the current best answer to `who seeds the records?` is:
|
|
|
|
> the debugger bootstrap seeds the object, but a separate interpreter-side runtime path seeds the current-entry records. The exact static caller of `13e0:02f5` / `13e0:03b0` is still not recovered cleanly in the present database, but the runtime ownership is clearly on the interpreter side rather than on the file-loader side.
|
|
|
|
### 4. Why `open_modal` is not enough
|
|
|
|
This is now the key practical distinction.
|
|
|
|
`usecode_debugger_open_modal`:
|
|
|
|
- creates the debugger gump
|
|
- shows the debugger UI
|
|
- does **not** load a current unit automatically
|
|
- does **not** seed a current-entry record stack
|
|
|
|
`usecode_debugger_open_for_current_unit`:
|
|
|
|
- creates the debugger gump
|
|
- reads the current-entry record from the break-state object
|
|
- derives the unit name from that record
|
|
- loads the matching `.unk`
|
|
- recenters the view around break-state line `+0x72 - 1`
|
|
|
|
So the current crash after `RUN` is fully consistent with this model:
|
|
|
|
- the `.unk` now opens and displays because the file requirement is satisfied
|
|
- but the debugger session still lacks the same runtime current-entry state that a real interpreter-driven break would provide
|
|
- therefore `RUN` can still resume into an invalid or half-seeded debugger context and crash
|
|
|
|
### 5. What can be done manually right now
|
|
|
|
There are now three manual tiers, each with different reliability.
|
|
|
|
#### Tier A: menu-only bring-up
|
|
|
|
This is the already-validated state.
|
|
|
|
Minimum manual state:
|
|
|
|
1. ensure `1480:712c/712e` points at a valid debugger object
|
|
2. load a debugger-safe `.unk` file for the desired unit
|
|
3. open the debugger gump
|
|
|
|
This is enough for:
|
|
|
|
- opening the debugger window
|
|
- opening source text
|
|
- browsing text and using some viewer-side commands
|
|
|
|
It is **not** enough for:
|
|
|
|
- stable `RUN`
|
|
- stable single-step / break-next
|
|
- reliable watch / inspect value evaluation
|
|
|
|
#### Tier B: manually seeded synthetic current-entry session
|
|
|
|
This is the first plausible manual path toward a truly runnable session.
|
|
|
|
Required manual state:
|
|
|
|
1. bootstrap the debugger object if `1480:712c/712e` is null
|
|
2. set `break_state + 0x7a = 1`
|
|
3. write one current-entry record at `break_state + 0x7c`
|
|
4. set `break_state + 0x72` to the current source line minus one
|
|
5. optionally arm `+0x75` or `+0x74`
|
|
6. open via `usecode_debugger_open_for_current_unit`, not only `open_modal`
|
|
|
|
Minimum record contents for that experiment:
|
|
|
|
- unit name at record `+0x00`
|
|
- three packed runtime dwords at record `+0x09 .. +0x14`
|
|
|
|
Main limitation:
|
|
|
|
- we do not yet have a fully decoded semantic map for the three packed dwords
|
|
- at least some of them are consumed by expression evaluation and probably by other debugger-context code
|
|
- so zero-filling them is unlikely to give a fully stable session
|
|
|
|
Current safest read:
|
|
|
|
- manual synthetic seeding is plausible, but it needs **captured real runtime values** from a live interpreter context, not invented placeholders
|
|
|
|
#### Tier C: let the interpreter seed the record naturally, then open the debugger
|
|
|
|
This is now the strongest practical route.
|
|
|
|
Desired sequence:
|
|
|
|
1. bootstrap the debugger object
|
|
2. arm break-next or single-step in the object
|
|
3. allow one real usecode/interpreter pass to run
|
|
4. let the interpreter-side debugger path push a genuine current-entry record
|
|
5. open through the normal current-unit path or let the slot-`0` auto-open path fire
|
|
|
|
Why this is stronger than manual record fabrication:
|
|
|
|
- the runtime itself provides the three packed context dwords
|
|
- the current line is seeded by the real update path
|
|
- watch / inspect / resume have the best chance of seeing coherent context
|
|
|
|
### 6. Best current manual recipe
|
|
|
|
If the immediate goal is `make the debugger work properly`, the best current manual recipe is no longer `generate a better .unk` by itself. The better recipe is:
|
|
|
|
1. keep the current JS exporter and synthetic Regret line floor so source load stays stable
|
|
2. capture one real current-entry record from a live interpreter-side seed point
|
|
3. reuse those captured values either:
|
|
- by manual object seeding, or
|
|
- by patching the launcher so the interpreter seeds them naturally before open
|
|
|
|
Current candidate capture points:
|
|
|
|
- breakpoint on `13e0:02f5`
|
|
- breakpoint on `13e0:03b0`
|
|
- breakpoint on `13e0:03f7` to see the matching pop side
|
|
- breakpoint on `13e0:0053` if a real break path can be reached
|
|
|
|
What to record when one of those hits:
|
|
|
|
- debugger object pointer (`1480:712c/712e`)
|
|
- `+0x72`
|
|
- `+0x7a`
|
|
- the full `0x15` bytes at the current-entry record start
|
|
- the three packed dwords separately
|
|
- the unit name string
|
|
|
|
That would immediately answer the remaining unknowns that static decompilation has not resolved cleanly.
|
|
|
|
## Workable Plan
|
|
|
|
The path to a fully working debugger session now looks like this.
|
|
|
|
### Phase 1. Finish the record-seeding recovery
|
|
|
|
Goal:
|
|
|
|
- confirm the exact interpreter-side caller that reaches `13e0:02f5` / `13e0:03b0`
|
|
|
|
Concrete actions:
|
|
|
|
1. use runtime breakpoints on `13e0:02f5`, `13e0:03b0`, and `13e0:03f7`
|
|
2. trigger ordinary usecode activity while the debugger object is live
|
|
3. capture caller addresses and arguments
|
|
4. map the three packed dwords to concrete runtime structures
|
|
|
|
Deliverable:
|
|
|
|
- one verified record-seeding path with argument semantics
|
|
|
|
### Phase 2. Prove a coherent manual seed
|
|
|
|
Goal:
|
|
|
|
- show that a manually seeded break-state can survive `RUN`
|
|
|
|
Concrete actions:
|
|
|
|
1. bootstrap the debugger object
|
|
2. write one captured real current-entry record into `break_state + 0x7c`
|
|
3. set `+0x7a = 1`
|
|
4. set `+0x72` to a matching line
|
|
5. open with `usecode_debugger_open_for_current_unit`
|
|
6. test `RUN`, then `single step`, then `watch`
|
|
|
|
Deliverable:
|
|
|
|
- one working manual debugger session without relying on the exact hidden launcher path
|
|
|
|
### Phase 3. Replace manual seeding with a real launch path
|
|
|
|
Goal:
|
|
|
|
- stop fabricating debugger context and let the runtime produce it naturally
|
|
|
|
Concrete actions:
|
|
|
|
1. patch a hidden input lane only far enough to bootstrap the object and arm break-next / single-step
|
|
2. do **not** open only through `open_modal`
|
|
3. let the next real interpreter pass seed the current-entry record stack
|
|
4. open through `usecode_debugger_open_for_current_unit` or the slot-`0` auto-open path
|
|
|
|
Deliverable:
|
|
|
|
- the smallest reproducible launcher patch that yields a coherent debugger context
|
|
|
|
### Phase 4. Tighten the source side only after runtime seeding is solved
|
|
|
|
Goal:
|
|
|
|
- improve source fidelity once the runtime context path is stable
|
|
|
|
Concrete actions:
|
|
|
|
1. keep the current synthetic Regret line floor for stability
|
|
2. continue investigating whether Regret still contains recoverable original line markers through another parser pass or another source of metadata
|
|
3. only after runtime stability, revisit whether the `.unk` needs better line correlation for breakpoint fidelity
|
|
|
|
Deliverable:
|
|
|
|
- stable debugger first, better line fidelity second
|
|
|
|
## Bottom Line
|
|
|
|
The investigation has now advanced beyond `what file will the debugger open?`.
|
|
|
|
Current safest answer to `what does it take for the debugger to work properly?` is:
|
|
|
|
- a debugger-safe `.unk` file
|
|
- a live debugger object
|
|
- a valid current-entry record stack seeded from real interpreter context
|
|
- a valid current line in `+0x72`
|
|
- and only then `RUN` / step / inspect / watch have a defensible chance to behave properly
|
|
|
|
Current safest answer to `who seeds the records?` is:
|
|
|
|
- bootstrap seeds the object
|
|
- interpreter-side runtime logic seeds the current-entry records
|
|
- the exact push caller is still not fully recovered as a named xref, but the push/pop ownership is now clearly on the interpreter side, not on the source loader side
|
|
|
|
Current safest answer to `how can we do it manually?` is:
|
|
|
|
- not by `.unk` text alone
|
|
- by either capturing and writing one real current-entry record into the break-state object, or by patching a launcher path that lets the interpreter seed that record naturally before opening the debugger
|
|
|
|
## Open Questions After This Pass
|
|
|
|
1. Does retail No Remorse still contain a dormant analogue of the Regret vtable override, not just the writer/bootstrap?
|
|
2. Are there any Regret-side callsites outside the current xref model that still seed break/step state intentionally for the debugger?
|
|
3. Does any hidden Regret cheat/debug key or menu path manipulate the break-state bytes at `+0x74/+0x75/+0x76/+0x78` or otherwise force the slot-`0` auto-open path?
|
|
4. Is `usecode_debugger_bootstrap_init` a real segment-load initializer, and if so where exactly is that load boundary controlled?
|
|
|
|
## Recommended Next Step
|
|
|
|
Use No Regret, not JP, as the primary retail comparison anchor and move back to retail with the stronger Regret model in hand.
|
|
|
|
The concrete retail follow-up should compare:
|
|
|
|
- the missing writer of `1478:659c/659e`
|
|
- any retail analogue of `usecode_debugger_bootstrap_init`
|
|
- any retail analogue of the `1480:713b -> 1480:6972` vtable promotion
|
|
- the retail equivalent of the Regret slot-`0` auto-open path through `usecode_debugger_break_state_update_line_and_maybe_break`
|
|
|
|
That remains the highest-value cross-build step. But if the immediate practical goal is only `make the debugger appear somewhere`, Regret is now the better live hack target than retail itself. |