Crusader_Decomp/docs/usecode-startup-override.md
2026-04-05 09:55:21 +02:00

22 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

What is now materially tighter from the live Filespec_GetFullPath and File_Exists implementations:

  • the path is interpreted with DOS file APIs, not Windows host-path APIs
  • the builder uses the literal separator string "\\", not "/"
  • the parser-side s_usecode buffer is only 0x1e bytes long, so the raw -u token is truncated to at most 29 visible characters plus the terminator

That makes the safest current syntax guidance:

  • prefer DOS-style backslashes, not forward slashes
  • prefer short path tokens
  • assume the path must exist inside the game's DOS-visible filesystem view

If the game is running under DOSBox or a similar DOS environment, C:\MADDOCODE refers to the guest DOS C: drive, not automatically to the Windows host C: drive.

One more practical constraint is now strong enough to call out explicitly:

  • the -u existence probe uses an old DOS find-first style path, so 8.3-safe directory names are the safest override roots

For the current GOG install used in this investigation, Windows reports the short alias:

  • MADDOCODE -> MADDOC~1

So the highest-probability next token for this specific setup is:

  • -u MADDOC~1

or the explicit relative form:

  • -u .\MADDOC~1

Why -u FLICTEST.FLX Fails

The live helper shape is now tight enough to explain the failed FLICTEST attempts directly.

What retail CRUSADER.EXE does at 1420:0cf0..0d33 is:

  • read the copied argv token from 1478:065a
  • load the fixed filename template through 1478:06d6/06d8 -> 1478:07a0
  • use that template as eusecode.flx
  • call Filespec_GetFullPath(0, <copied token>, "eusecode.flx", 0)

The raw data bytes at 1478:079a confirm the fixed template directly:

  • 65 75 73 65 63 6f 64 65 2e 66 6c 78 00
  • ASCII = eusecode.flx\0

So these example invocations do not mean load this exact archive file:

  • -u USECODE/FLICTEST.FLX
  • -u FLICTEST.FLX

They instead mean, in the current safest static interpretation:

  • use USECODE/FLICTEST.FLX as the path component and still append the fixed filename eusecode.flx
  • or use FLICTEST.FLX as the path component and still append the fixed filename eusecode.flx

That makes both attempts wrong for a direct single-file override. The game is not being told to open your FLICTEST.FLX archive as the final filename at all.

Safest Current Experiment Shape

The current safest static-model experiment is therefore:

  1. create a directory that will act as the override root
  2. place a structurally valid replacement archive in that directory
  3. name that archive EUSECODE.FLX
  4. pass the directory path to -u, not the archive filename

Examples that now fit the recovered helper better are:

  • -u USECODE
  • -u .\\USECODE
  • -u \USECODE
  • -u C:\USECODE
  • -u MOD_UC

with the important condition that the target directory must contain EUSECODE.FLX, not FLICTEST.FLX.

The safest current path interpretations for those examples are:

  • -u MADDOCODE -> tries MADDOCODE\EUSECODE.FLX relative to the game's current DOS working directory
  • -u .\MADDOCODE -> tries .\MADDOCODE\EUSECODE.FLX
  • -u \MADDOCODE -> tries \MADDOCODE\EUSECODE.FLX on the current DOS drive
  • -u C:\MADDOCODE -> tries C:\MADDOCODE\EUSECODE.FLX on DOS drive C:

The current safest negative guidance is:

  • do not use forward slashes like USECODE/FLICTEST.FLX
  • do not pass the archive filename itself as the -u token
  • do not assume a long absolute host path will survive the 0x1e-byte parser buffer intact
  • do not assume a directory name longer than classic 8.3 DOS spelling will be accepted exactly as typed

Expected Visible Output

There is currently no evidence that retail -u prints a success or failure banner before the game loads.

What the live code shows:

  • the -u parser case at 1048:0a46 only copies the token into 1478:065a
  • unlike -debug, -warp, -skill, -mapoff, -egg, and -demo, that parser case does not call ConsolePrintf
  • startup_apply_u_override_if_present also does not call ConsolePrintf
  • if File_Exists says the constructed path is missing, the helper just returns and startup continues

So the safest current user-facing expectation is:

  • no dedicated -u console line on success
  • no dedicated -u console line on file-not-found failure
  • if the override path is wrong, the game most likely falls back silently to stock usecode

That means no message appeared before the game loaded is currently normal and does not prove the override worked.

Strongest Current Diagnosis For The MADDOCODE Test Case

The current install-side evidence makes this case much tighter than a generic -u experiment.

What is now verified in the GOG install:

  • MADDOCODE contains EUSECODE.FLX, OVERLOAD.DAT, UNKCOFF.DAT, and UNKDS.DAT
  • OVERLOAD.DAT, UNKCOFF.DAT, and UNKDS.DAT in MADDOCODE are byte-identical to the working copies in USECODE
  • only EUSECODE.FLX differs between the stock USECODE folder and MADDOCODE

That makes the A/B interpretation unusually clean.

If:

  • replacing USECODE\EUSECODE.FLX with the hacked file produces the expected obvious gameplay breakage
  • but launching with -u MADDOCODE produces stock gameplay instead

the strongest current read is not the alternate root loaded but behaved differently. The stronger current read is:

  • the alternate root was probably never selected
  • startup most likely fell back silently to stock usecode after the override path probe failed

Given the recovered code and the current install layout, the leading practical failure mode is now:

  • the DOS-side file probe did not accept MADDOCODE exactly as typed
  • the short alias MADDOC~1 is the next highest-probability working token

This is still a static-analysis conclusion rather than a runtime-tested acceptance matrix, so the exact relative-path and alias forms remain open. But the filename side is no longer the main uncertainty: the recovered helper is strongly telling us directory/root override for EUSECODE.FLX, not arbitrary filename override.

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

That also means a standalone archive like FLICTEST.FLX is currently a poor first experiment even if its internals are valid. The live retail loader path is much more likely to accept a complete replacement EUSECODE.FLX rooted under a directory passed to -u.

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.