Crusader_Decomp/docs/usecode-equipment-system.md
2026-03-25 23:32:36 +01:00

10 KiB

Crusader USECODE Equipment System

Purpose

This note records the current evidence-backed read of Crusader's surviving equip / unequip system.

The short answer is: yes, a real inherited equipment-style event system survived into Crusader, but by this point it has been generalized far beyond RPG inventory handling. In Crusader, equip and unequip are standard item usecode events that many classes reuse for actor setup, turret arming, trap activation, alarm-state changes, and environmental control.

Short version

The funny interpretation is half true and half misleading.

What is true:

  • Crusader still has first-class equip and unequip item events.
  • The live binary has dedicated intrinsics for them.
  • The extracted USECODE corpus has a large number of slot 0x0A equip and slot 0x0B unequip bodies.
  • Actor-like classes such as NPC and MONSTER really do implement equip bodies.

What is misleading:

  • this is not yet proof of a fully intact Ultima-style paper-doll equipment subsystem
  • many equip bodies are plainly being used as generalized state-change hooks rather than literal "put item on actor" logic
  • alarms, sentries, guns, floors, conveyors, cameras, and hazard objects all reuse the same event vocabulary

Current best read:

Crusader inherited the Ultima 8 event names and event slots, then repurposed them into a broad object-activation vocabulary. The old RPG-flavored name equip survived, but its semantics widened into apply mode / arm / initialize / attach behavior / activate state.

Compiled-side proof

The live CRUSADER.EXE session already has named intrinsic handlers for these events:

  • 10a0:2a35 = Item_Equip
  • 10a0:2a68 = Item_Unequip
  • nearby sibling 10a0:2b30 = Item_EnterFastArea

The decompiler output for Item_Equip is the key proof:

word __cdecl16far Item_Equip(int *pitemno,uint dest)
{
  res = Usecode_ItemCallEvent(pitemno,0x400,UC_Equip,...);
  if (res != 0) {
    return in_stack_0000fffc;
  }
  return 0;
}

Item_Unequip is parallel:

word __cdecl16far Item_Unequip(int *pitemno,int val)
{
  res = Usecode_ItemCallEvent(pitemno,0x800,UC_Unequip,...);
  if (res != 0) {
    return in_stack_0000fffc;
  }
  return 0;
}

That proves several things immediately:

  1. equip and unequip are real compiled-side usecode events, not parser inventions.
  2. They are gated by dedicated owner-row capability masks: 0x400 for equip and 0x800 for unequip.
  3. The intrinsic does not directly manipulate an inventory table itself. It forwards into a generic item-event dispatcher.

The central dispatcher decompiles as Usecode_ItemCallEvent. Its current Ghidra decompile explicitly comments it as a generic owner-row mask gate:

  • compute item usecode class id
  • consult the owner-row table behind the runtime at 0x6611
  • test the supplied capability mask
  • if the row supports the event, create a usecode process for the requested event

That is the architectural core of the surviving system: equip and unequip are class capabilities, not hardwired game-engine special cases.

Cross-check against the engine source lineage

ScummVM's Ultima8/Crusader code preserves the same event interpretation.

In engines/ultima/ultima8/world/item.cpp:

  • callUsecodeEvent_equipWithParam(param) is explicitly documented as event A
  • callUsecodeEvent_unequipWithParam(param) is explicitly documented as event B

That source also shows the event neighborhood:

  • event 9 = release
  • event A = equip
  • event B = unequip
  • event C = combine
  • event E = called from anim
  • event F = enter fast area
  • event 10 = leave fast area
  • event 11 = cast

This strongly supports the idea that Crusader inherited an Ultima-style item event vocabulary wholesale and kept using it even after the gameplay moved away from a classic RPG.

How widespread it is in the extracted corpus

The current exported pseudocode corpus contains:

  • 77 slot 0x0A equip entries
  • 50 slot 0x0B unequip entries

That is far too widespread to be accidental or restricted to a tiny one-off subsystem.

The distribution is also telling:

  • actor/NPC classes: NPC, MONSTER
  • turret/weapon classes: BASEGUN, SENTRY, GATGUN*, WALGUN*, GOVGUN*
  • alarm/hazard/environment classes: ALARMBOX, ALARMHAT, FFFLOOR, FLAMEBOX, STEAMBOX
  • cameras and movers: CAM_*, EYECAM*, CONVEY_*, ROLL_*, HOVER*

So the names survived as a general event interface across many object families.

What equip means in practice

1. General activation or state application

Many non-actor classes use equip exactly the way a modern engine might use activate, arm, enable, or set mode.

Representative example: BASEGUN::equip

set_info(0x0211, *(arg_06));
process_exclude();
spawn class_0A1A_slot_24(pid, arg_0A, ..., arg_06);
suspend;

Parallel example: SENTRY::equip

Its exported pseudocode is the same structural pattern as BASEGUN::equip, and SENTRY::unequip mirrors the same shutdown logic as BASEGUN::unequip.

That is not wearable-item behavior. It is a clean arm/disarm lifecycle.

2. Alarm-state or environmental mode switching

ALARMBOX::equip and ALARMHAT::equip are good examples of the generalized meaning.

They both begin with:

set_info(0x0211, *(arg_06));
process_exclude();

Then they branch by frame/state and spawn or equip helper objects in specific response modes.

ALARMHAT is especially illustrative because it calls Item::I_equip(pid, 0x17, item) and Item::I_equip(pid, 0x15, item) on nearby shape 0x04D0 helper objects. That is exactly where the older RPG-flavored naming sounds absurdly literal, but the behavior is really push nearby helper into alarm response mode.

3. NPC and monster setup

Actor-facing classes really do implement equip, but the current evidence still points to scripted setup and state application rather than visible inventory slot management.

Representative example: NPC::equip

The exported body does not look like a paper-doll or inventory routine. It looks like a setup dispatcher keyed by arg_0A:

  • arg_0A == 10: reset, teleport to egg, gather several values, create or legalize the NPC/item context, then spawn class_0A11_slot_29(...)
  • arg_0A == 30: read several values with Intrinsic00DF(...)
  • arg_0A == 31: suspend
  • arg_0A == 1/2/3: explicit but empty branches

That reads much more like apply initialization mode or run NPC setup variant than equip helmet.

Representative example: MONSTER::equip

MONSTER::equip checks frame/state, stamps 0x0211, then spawns class_0A1E_slot_2D(pid, var, monster1, arg_06) for a range of mode values. Again, that looks like a dispatcher for monster behavior setup or activation, not a visible inventory pane.

What unequip means in practice

unequip is the paired shutdown or detachment event.

Representative example: BASEGUN::unequip

set_info(0x0212, *(arg_06));
process_exclude();
if (Item.getStatus(arg_06) & 4) {
  spawn class_0A1A_slot_27(arg_06);
}

SENTRY::unequip is the same pattern.

That strongly suggests unequip has become the general reverse transition for classes that use equip as arm or enable.

Environmental example: FFFLOOR::unequip

This body does not resemble inventory logic at all. It scans nearby family-6 objects, filters by overlap, and then spawns an actor-facing helper class_0A11_slot_2D(retval, *(arg_06), item) before suspending.

So by the time we reach Crusader, unequip has clearly broadened into deactivate / detach / reverse state / cleanup side effects.

What survived from the RPG ancestor

The surviving part is not just the word choice. The system architecture survived too:

  • item classes advertise event capabilities through owner-row mask bits
  • the engine exposes generic intrinsics that dispatch into class-specific usecode handlers
  • the event numbers remain organized in an Ultima-style item-event vocabulary
  • actor and item classes both participate in the same event model

That is exactly the kind of subsystem that makes sense as an inherited RPG engine layer which later got repurposed for an action game.

What we do not have yet

We do not yet have proof of a fully intact classic RPG equipment layout such as:

  • fixed visible paper-doll slots
  • a clean actor inventory table indexed by helmet/armor/weapon slots
  • universal actor gear attachment semantics across all NPCs

The current evidence is stronger for:

  • an inherited event vocabulary with equip / unequip
  • class-specific scripted setup and mode application
  • actor and environmental reuse of the same event names

So the safe statement is:

Crusader definitely has a hidden equipment event system.

Crusader may also retain deeper actor-equipment semantics under that layer, but that is not yet proven from the current pass.

Current best model

The most defensible current model is:

  1. Ultima-style item events A = equip and B = unequip survived into Crusader.
  2. The compiled engine still dispatches them through generic item-event intrinsics.
  3. Owner-row capability bits decide which classes support those events.
  4. USECODE then interprets equip and unequip per class.
  5. In Crusader, many classes reinterpret those events as activate/deactivate, arm/disarm, initialize/teardown, or switch mode, while actor-facing classes also use them for setup-like behavior.

Good follow-up targets

The best next places to deepen this are:

  1. compare more actor-facing equip bodies (NPC, MONSTER, ANDROID, AVATAR) against each other
  2. find compiled-side consumers of the value passed as the equip parameter to see whether it maps to actor slots, modes, or stance/state enums
  3. recover more names around the helper slots repeatedly spawned from equip bodies, especially the 0A11, 0A1A, and 0A1E families
  4. check whether any actor-only classes expose both equip and unequip in ways that look like real gear attach/detach rather than generic initialization
  5. decode more loop-selector idioms so actor/environment equip bodies become less VM-shaped
  6. cross-check ScummVM Crusader actor/inventory code for surviving slot semantics that may still map to the retail binary