Crusader_Decomp/docs/usecode-startup-override.md

16 KiB

Retail -u USECODE Startup Override

Question

If retail non-Japanese CRUSADER.EXE is started with -u <arg>, does that add extra usecode on top of the stock runtime, or does it replace the stock usecode root? And if it really is a replacement path, what parts of the game does that replacement actually control?

Active analysis target for the binary side was live CRUSADER.EXE in the Ghidra session.

Short Answer

Current best answer:

  • -u is a real retail startup override, not dead parser residue.
  • In retail CRUSADER.EXE, it behaves like a replacement of the live usecode runtime root, not an additive overlay.
  • The replacement root is then used by the normal usecode event/process/interpreter machinery.

So the practical meaning is:

-u <arg> points startup at an alternate usecode/EUSECODE source, and subsequent scripted behavior runs against that replacement runtime instead of the stock one.

The strongest remaining open questions are now the precise Filespec path rules for <arg> and the still-unclosed live-NE stock bootstrap path that seeds the same runtime when -u is absent.

What <arg> Currently Looks Like

The token-shape question is no longer completely open.

Current best retail read from the live 1420:0cdf helper is:

  • the parser still copies the raw argv token into 1478:065a
  • the helper does not pass that token straight to the loader as the final filename
  • instead it loads a far pointer from 1478:06d6/06d8 to the mutable string at 1478:07a0, which is eusecode.flx
  • it forces the first byte of that filename template to 'e'
  • it then calls Filespec_GetFullPath(0, s_usecode, "eusecode.flx", 0)
  • it probes that constructed full path with File_Exists
  • if the path exists, it rebuilds the same full path again and passes that result into the runtime loader/constructor path

That makes the safest current interpretation:

  • <arg> is a Filespec path-like argument that supplies the directory/resource-root component
  • the filename component is still the fixed retail archive name eusecode.flx

So the current evidence argues against these stronger interpretations:

  • arbitrary full filename override
  • free-form alternate archive basename
  • direct one-off script/body filename

If the user passes a literal filename as <arg>, the recovered helper shape suggests the game would still try to append the fixed filename template rather than use the supplied token as the final filename.

The remaining uncertainty is narrower now:

  • whether the path component can be only a filesystem directory
  • whether it can also be a loader alias/resource root that Filespec_GetFullPath understands
  • whether relative, absolute, and CD-backed forms are all accepted equally

External engine code reinforces the fixed-filename part of this read. Both Pentagram and ScummVM build the default Crusader main usecode path as usecode/ + language letter + usecode.flx, and for Remorse/Regret the language-usecode letter is 'e', which matches the retail helper's forced eusecode.flx template.

Why This Looks Like Replacement, Not Addition

1. There is one live runtime root, not a list of roots

The key global pair is:

  • 1478:6611/6613

That far pointer is read directly by all of the main recovered usecode-side consumers, including:

  • Usecode_ItemCallEvent at 1420:0e3a
  • UsecodeProcess_CreateProcess at 1420:0f20
  • Interpreter_NextUsecodeOp at 1418:332c
  • Item_GetDamaged at 10a0:277b

No parallel second root pointer, linked list, or merge walk was recovered in this pass.

That by itself already makes additive-overlay behavior unlikely.

2. The -u startup helper overwrites that single root pointer

The retail parser case at 1048:0a46 copies the following argv token into 1478:065a.

Startup later calls startup_apply_u_override_if_present at 1420:0cdf from 1048:05d3.

Inside startup_apply_u_override_if_present:

  • if 1478:065a is empty, it returns without changing anything
  • if 1478:065a is non-empty, it resolves/loads an alternate source and then writes the resulting far pointer to 1478:6611/6613
  • it sets 1478:6615 = 1
  • it rebuilds the cumulative slot-base words at 1478:8c7c..8c82

The important behavior is the direct assignment:

  • 1420:0d49 writes DX -> 1478:6613
  • 1420:0d4d writes AX -> 1478:6611

That is a swap of the live root pointer, not registration of a side table.

3. The helper rebuilds the slot-base state for the new root

After the root overwrite, startup_apply_u_override_if_present immediately recomputes the cumulative slot-base words:

  • 1478:8c7c
  • 1478:8c7e
  • 1478:8c80
  • 1478:8c82

Those are the same runtime bases later used by the slot-index/category mapping and owner-row lookups.

That makes the replacement behavior even stronger:

  • the code is not merely keeping an alternate archive pointer around
  • it is re-deriving the runtime indexing state that downstream usecode dispatch relies on

4. No merge step was recovered

This pass did not recover any logic that:

  • appends records from the -u source onto the stock root
  • patches individual classes/events into an existing live root
  • or checks two roots in order during normal dispatch

Instead, the recovered shape is:

  1. default startup path establishes the ordinary runtime state
  2. startup_apply_u_override_if_present runs once during startup
  3. if -u was supplied, it replaces the root pointer and rebuilds the slot bases
  4. later gameplay/usecode consumers all read the same replacement root

The stock runtime may still remain allocated in memory if it was created earlier, but the current evidence says it is no longer the one normal dispatch uses. That is replacement semantics, not additive semantics.

Best Current Read Of The Stock Path

The default non--u bootstrap is only partially closed in the live NE database.

What is directly closed in live CRUSADER.EXE:

  • Init_Everything calls startup_apply_u_override_if_present at 1048:05d3
  • 1478:6611/6613 starts as zero in the loaded image
  • the only currently recovered explicit write to 1478:6611/6613 in the live NE session is the -u override helper itself

That means the stock bootstrap still has an unresolved step in the live NE target.

The best current cross-reference evidence for that missing default path comes from the verified raw-side VM note in docs/raw-000a-000d.md:

  • entity_vm_runtime_init_from_path_if_configured (000d:44df) builds the external path and creates the runtime
  • entity_vm_runtime_create (000d:4c99) creates the runtime object
  • entity_vm_runtime_owner_resource_create (000d:7000) allocates and fills the owner/resource table used later by slot/context creation
  • that same raw-side init path also seeds the cumulative category bases at 0x8c7c/0x8c7e/0x8c80/0x8c82 from the four counts at 0x6608..0x660e

That raw-side bootstrap is important because it matches the same state that the live retail -u helper rebuilds after replacement:

  • same four category counts at 1478:6608..660e
  • same cumulative bases at 1478:8c7c..8c82
  • same downstream owner-row and slot-index consumers already recovered in the live NE session

So the safest current combined read is:

  • the normal game also has a stock runtime bootstrap in the same VM family
  • the raw-side path is strongly evidenced as that bootstrap
  • but the exact live-NE caller that seeds 1478:6611/6613 before gameplay without -u is still not directly closed

That distinction matters for documentation quality. We can now say the stock path is strongly cross-referenced, but we should not yet claim a fully recovered live-NE writer when the current explicit xrefs do not show it.

Replacement Root Layout So Far

The direct constructor/loader pair behind the retail -u path is now materially clearer in the live NE session.

1. entity_vm_runtime_create at 1420:1499

This function is the direct constructor called by startup_apply_u_override_if_present after the alternate eusecode.flx path is resolved.

Current best read:

  • allocates 0x1319 bytes when no existing object is supplied
  • clears the first 0x1300 bytes through 1420:1536
  • stores several tail defaults in the 0x1300..0x1314 range
  • calls entity_vm_runtime_owner_resource_create with the resolved path
  • stores the returned far pointer at +0x1315/+0x1317

The zeroed first 0x1300 bytes are especially suggestive:

  • 1420:1575 later walks that area as 0x80 entries of stride 0x26
  • for each entry it frees two far-pointer pairs at offsets +0x1e/+0x20 and +0x22/+0x24

So the safest current shape is:

  • runtime object header/tail metadata at 0x1300..0x1318
  • leading table of 0x80 slot/runtime records occupying 0x0000..0x12ff

2. entity_vm_runtime_owner_resource_create at 1430:0000

This helper is now named in the live NE session because its behavior matches the already-verified raw-side owner/resource loader closely.

Current best read:

  • allocates a compact 0x14-byte helper object
  • initializes it as a file-backed loader object
  • opens the resolved external eusecode.flx path through its vtable-backed file path
  • queries an entry count through vtable +0x04
  • allocates a backing buffer and stores its far pointer at +0x10/+0x12
  • materializes indexed owner/resource records through vtable +0x0c

That aligns well with the raw-side note that the owner/resource loader manages indexed file-set materialization rather than directly exposing simple class-name lookups.

3. Why This Matters For -u

This constructor/loader pair gives the override note a more concrete partial object model:

  • -u is not just swapping in a raw file handle or archive pointer
  • it is constructing a full VM runtime object
  • that object owns both a slot/runtime table region and an attached owner/resource helper
  • the attached helper is what materializes the indexed owner-loaded data later consumed by event/process/interpreter paths

That makes the replacement semantics stronger in practice. The override is swapping the live VM runtime object graph, not merely redirecting one filename global.

What The Replacement Root Actually Controls

1. Item/event dispatch

Usecode_ItemCallEvent at 1420:0e3a reads 1478:6611, follows the live runtime tables, checks the owner-row capability bits, and creates the event/process context from that data.

That means the -u override reaches normal item-event lanes such as the surviving Crusader event families already documented elsewhere:

  • use
  • look
  • hit
  • gotHit
  • equip
  • unequip
  • schedule
  • anim
  • and the wider event-slot family behind class-specific behavior

2. Usecode process creation

UsecodeProcess_CreateProcess at 1420:0f20 also reads 1478:6611 and builds process/context state from the owner tables hanging off the live root.

That means the override is not limited to one-shot event callbacks. It reaches process-backed scripted behavior too.

3. Interpreter bytecode stepping

Interpreter_NextUsecodeOp at 1418:332c pushes 1478:6611/6613 into its runtime helper path.

That is the strongest scope clue in the whole pass. The replacement is not just changing metadata or an event name table. It is feeding the same runtime used when the interpreter advances usecode bytecode.

4. Gameplay-side scripted capability checks

Item_GetDamaged at 10a0:277b also reads the same live root and tests owner-row bit 0x40 before falling through to later behavior.

So the replacement root can affect gameplay-side scripted capability checks as well, not only the hidden/debugger-facing VM side.

Practical Meaning For Custom USECODE

If we can construct a compatible replacement archive/source for the -u path, the current evidence says it should be able to influence the same kinds of scripted behavior the stock USECODE runtime already controls.

Current safest statement:

  • a custom -u source should be able to replace class/event bodies that the retail interpreter and process/event creators consume
  • that makes it relevant to experimentation with event handlers, scripted item behavior, process-backed behaviors, and other VM-driven logic already present in Crusader's usecode system

Current important limit:

  • this does not imply arbitrary native-code injection
  • the replacement still has to fit the existing Crusader usecode VM/runtime contracts
  • it is constrained by the interpreter, intrinsic tables, class layout, event table layout, and slot/body decoding the retail game already knows how to execute

So the safe model is:

-u gives us a path to run custom data for the existing usecode VM, not a generic plug-in system for arbitrary executable code.

What A Replacement Probably Needs To Contain

Because the recovered runtime uses a single root plus one rebuilt slot-base family, the safest current expectation is that the -u target is a complete replacement source that the loader can resolve into a full owner/class runtime.

This pass did not recover evidence for a sparse overlay format such as:

  • only one class
  • only one event body
  • patch this one slot onto the stock archive

That does not prove such a sparse mode is impossible, but it means we should not assume it.

For current tooling planning, the defensive assumption should be:

  • prepare a structurally complete replacement source that satisfies the loader's normal expectations
  • do not assume the game will merge one partial class file into the stock runtime for us

Best Current Usefulness For Decompilation Work

This path is interesting for the current usecode-decompilation lane for two reasons.

1. It is a real in-engine execution route

If we can identify the accepted -u source format and feed it a compatible replacement archive, we get a retail-supported way to make the original game execute alternate usecode data.

That is far more valuable than a purely offline parser/export loop.

2. It can become a runtime validation path for reconstructed bodies

The current parser/export work is building toward editable and eventually round-trippable usecode.

If -u accepts a full replacement source, then a future recompiler/exporter could use it to answer questions like:

  • does a reconstructed class/event body still execute in the retail engine?
  • which opcodes/intrinsic signatures are mandatory for a valid replacement archive?
  • which behavior changes are script-level and which still depend on native code?

That makes -u potentially useful as a runtime validation hook for the ongoing decompilation toolchain.

Current Open Questions

  1. Which exact Filespec forms does <arg> accept in practice: relative directory, absolute directory, resource alias, CD-rooted path, or more than one?
  2. Which live-NE startup or game-start function establishes the stock 1478:6611/6613 root when -u is not used?
  3. What exact source/container format does the -u loader expect on disk or in resources beyond the filename family eusecode.flx, and how does that map onto the 0x14-byte owner-resource helper plus the 0x1319-byte runtime object?
  4. Does the replacement need to be a full archive, or can the loader tolerate a narrower-but-still-valid subset?
  5. Are there any startup-visible sanity checks that reject malformed but syntactically valid usecode containers?
  6. What is the smallest safe experimental replacement that can prove the runtime path end-to-end without destabilizing unrelated startup systems?
  1. Close the remaining Filespec semantics for s_usecode: confirm which directory/alias forms Filespec_GetFullPath accepts in this call shape.
  2. Identify the live-NE stock-runtime initialization site that seeds 1478:6611/6613 when -u is absent.
  3. Continue walking entity_vm_runtime_create / entity_vm_runtime_owner_resource_create so more of the 0x26-stride slot records and the owner/helper object fields are named concretely.
  4. Compare the resolved runtime root against known EUSECODE/USECODE container structure to decide whether -u expects a full FLEX-style archive or another packaging layer.
  5. Design the smallest conservative replacement experiment, ideally one structurally complete archive with one intentionally changed low-risk class/event body.
  6. Tie that future experiment back to the current parser/export pipeline so reconstructed bodies can eventually be runtime-tested through -u.