A complete reference to Starlancer's mission trigger/event scripting language: the trigger-condition enum, the Executor command set, the AI-behaviour codes, and the script-stream opcodes. This fuses two independent reverse-engineering efforts:
- Static RE of the decrypted executable (this project; ImageBase
0x400000). Source of the per-command implementation addresses + parameter counts, the trigger enum, the bytecode VM, and struct layouts. - Black-box RE by Captain Foster / "Starlancer ME" — blog
https://starlancerme.blogspot.com/, YouTube @CaptainFoster.
Source of the numeric opcode/command indices, the
AI-code and ship/pilot ID tables, and the observed in-game semantics. Their
empirical numbering and our disassembly agree everywhere they overlap — see
dte-format.mdfor the side-by-side reconciliation.
The companion tool
tools/dte_parse.pyholds these tables as importable data and prints them (dte_parse.py ref exec|triggers|ai|stream). The Executor table below is generated from it verbatim, so the doc and the tool can never drift. Its script disassembler (dte_parse.py decode <m> --section script) follows control flow from every routine's entry with the opcode table below, anddte_parse.py sweep <dir> --scriptchecks it over a folder of missions — see § The disassembler.
A .DTE carries data sections (ships, triggers, globals, strings, …) plus a block of
script bytecode. Execution is event-driven:
- The engine raises an event (a ship is destroyed, a proximity sphere is entered, a timer
expires, …) on an object. It walks that object's slice of the trigger list (section 7,
the object table, gives the slice's first index and count) and takes each record that is armed
(
+0x14), has the event's condition (TT_*at+0x00) and qualifier (+0x15), links to a block (+0x02 ≠ 0xFFFF) and whose operands pass (FUN_0045CEA0). - On a match,
FUN_0045B8D0starts a script thread atscript + link × 2: the link at+0x02is a halfword offset, and the block it names begins with au16length that counts itself. The byte at+0x16chooses whether the thread runs at once or from the scheduler. - The thread is run by the interpreter
FUN_0045C980: a flat loop that fetches one opcode byte, indexes the 86-entry handler tableDAT_004F6350(0x00–0x55), advances the IP, and calls the handler — repeating until a handler returns 0 (yield/finish). A parallel per-byte flag array (section 10,DAT_005294D8, indexed byIP − DAT_00525F88) marks yield points so long scripts can suspend across frames. - The VM is a stack machine. Commands (
0x21 <index>) dispatch through the command catalogue at0x4F0F50(installed byFUN_0045CE30), which holds, per command, its implementation pointer, parameter count, name and per-parameter labels — and each command pops its arguments from the operand stack that the other opcodes build (pushes of constants, bytes, strings, globals and record pointers; compares; arithmetic; big-endian branches; part calls).
So a script line such as SetAI(ship, mode, …) is a run of pushes followed by one 21 0B.
Both halves are listed below.
The condition type stored at trigger-record +0x00 (+0x15 is the component qualifier —
see dte-format.md §4). 33 scriptable types, 0x00–0x20 —
verified from the string array in FUN_0045B330 and identical to Starlancer ME's Triggers
page. (The condition-descriptor table DAT_0052952C has 35 entries / _DAT_00525F8C=0x23;
the 2 beyond 0x20 are internal, non-scriptable conditions.)
00 TT_SHOTAT |
09 TT_JUMPED_IN |
12 TT_TARGETTED |
1B TT_TRACTOR_BEAM_BROKEN |
01 TT_DESTROYED |
0A TT_FG_JUMPED_IN |
13 TT_PLAYER_L1_DOUBLETAP |
1C TT_INSIDE_OBJECT |
02 TT_LAUNCHED |
0B TT_PLAYER_READY_TO_WARP |
14 TT_PLAYER_L2_DOUBLETAP |
1D TT_OUTSIDE_OBJECT |
03 TT_CAMERAREACHED |
0C TT_JUMPED_THROUGH_HOOP |
15 TT_PLAYER_R1_DOUBLETAP |
1E TT_DOCKED |
04 TT_SHIPREACHED |
0D TT_PLAYER_WANTS_BACKUP |
16 TT_PLAYER_R2_DOUBLETAP |
1F TT_UNDOCKED |
05 TT_PROXIMITY_CLOSE |
0E TT_RIPPER_GRABBED_OBJECT |
17 TT_PLAYER_L1_L2_R1_R2_PRESSED |
20 TT_BEING_CHASED |
06 TT_PROXIMITY_GENERAL |
0F TT_RIPPER_DROPPED_OBJECT |
18 TT_PLAYER_L1_R1_PRESSED |
|
07 TT_OBJECT_SCOOPED |
10 TT_CLOAKED |
19 TT_GAME_TIMER_EXPIRED |
|
08 TT_PLAYER_READY_TO_JUMP |
11 TT_DECLOAKED |
1A TT_TRACTOR_BEAM_LOCKED |
The high-level mission verbs. This table is authoritative: we walk the engine's live command
catalogue at VA 0x4F0F50 (installed by FUN_0045CE30; the 0x21 <i> handler FUN_0045BEA0
indexes entry i, stride 0x74). Name, params, impl — and the per-parameter labels in the
§ Command parameters appendix — are the developers' own strings, recovered
straight from the binary. There are 95 real commands (0x00–0x5E); 0x5F has no
implementation and ends the catalogue (FUN_00452A80, the counter FUN_0045CE30 calls, stops at the
first entry without one; the entry is all zeros). Test_AI_Function is not command 0x60 — this
doc said so until 2026-09-29. It is entry 0 of a second catalogue, the AI-function catalogue at
0x4F3AD0 (description "Test AI Function", no implementation, two parameters labelled "Testttt
Param 1" and "Testttt Param 2" — a developer's placeholder, typo and all), which
begins where this one ends (0x4F0F50 + 0x60 × 0x74 = 0x4F3AD0) and serves opcode 0x4F; see
§ The vestigial AI layer. Our wiki's DAT_004F3AD0 "command catalogue" was
that address.
Numbering vs Starlancer ME. The blog's empirical indices agree with the catalogue except
0x16–0x19and0x26–0x2A, where the black-box effort mistook parameter-label text for commands — itsGTextPilotDefine,RadiusOfSphereandShipPointToFlyToare literally the parameter strings ofDisplaySubTitle("GText pilot define"),SetActionCentre("Radius of sphere…") andFly("Point to fly to") — and folds away theCommsFromPilot/CommsFromPilotOncepilot-voice twins.WaitNSeconds/ShipToDock/EntityToCloakare paraphrases ofWait/Dock/Cloak. The catalogue numbering is authoritative; the divergent rows are tabulated after the main table.
| idx | name | params | impl | idx | name | params | impl |
|---|---|---|---|---|---|---|---|
00 |
PrintShipName | 2 | 0x00458AB0 |
30 |
SetNavPoint | 2 | 0x00459270 |
01 |
CreateTimer | 4 | 0x0045D210 |
31 |
SetEscortPoint | 2 | 0x004592F0 |
02 |
DestroyTimer | 1 | 0x0045D290 |
32 |
ResetAfterBurners | 0 | 0x004594C0 |
03 |
CreateFlightGroup | 1 | 0x00457C40 |
33 |
DisableMissiles | 2 | 0x00459370 |
04 |
DestroyFlightGroup | 1 | 0x00457FD0 |
34 |
DisableEngines | 2 | 0x004593E0 |
05 |
Wait | 1 | 0x0045D2E0 |
35 |
DisableEject | 2 | 0x00459450 |
06 |
PlaySpeech | 1 | 0x00458090 |
36 |
SetHostile | 2 | 0x004594F0 |
07 |
WaitForSpeech | 0 | 0x00458100 |
37 |
ResetToSpawnPositions | 0 | 0x004591B0 |
08 |
PlayCommsMovie | 3 | 0x00458120 |
38 |
UpdateEnvironmentFXState | 0 | 0x004591A0 |
09 |
WaitForMovie | 0 | 0x00458180 |
39 |
SetPrimaryTarget | 1 | 0x00459550 |
0A |
PrintDebugMessage | 1 | 0x004581A0 |
3A |
WaitForJumpOrLaunch | 1 | 0x004595A0 |
0B |
SetAI | 4 | 0x004581F0 |
3B |
DoNotDisturb | 2 | 0x00459640 |
0C |
ClearAI | 1 | 0x004588C0 |
3C |
SetEnvironmentFXNebula | 1 | 0x00459190 |
0D |
SetPatrolRoute | 2 | 0x00458860 |
3D |
StartShipAnimationReverse | 2 | 0x004587D0 |
0E |
SetPilot | 2 | 0x00458830 |
3E |
SnapToPoint | 2 | 0x004596A0 |
0F |
SetTriggerState | 3 | 0x0045D300 |
3F |
PlayFostersLastStand | 0 | 0x00459740 |
10 |
StartDirectorCam | 5 | 0x004582E0 |
40 |
OpenInstrument | 1 | 0x0045D9D0 |
11 |
StartShipAnimation | 2 | 0x00458720 |
41 |
CloseInstrument | 1 | 0x0045DA30 |
12 |
ShipFollowCurve | 3 | 0x004585D0 |
42 |
DestroySubObject | 2 | 0x00459750 |
13 |
SetupLaunch | 3 | 0x00458970 |
43 |
SetObjective | 2 | 0x00459870 |
14 |
StartLaunch | 1 | 0x00458A40 |
44 |
SetRescueProbabilities | 3 | 0x004598D0 |
15 |
DisplaySubTitle | 1 | 0x00458A80 |
45 |
IsShipThisPlayer | 1 | 0x004598F0 |
16 |
ResetCodePriority | 1 | 0x00458A90 |
46 |
SetFlybackMarker | 2 | 0x00459910 |
17 |
InterruptTriggerCode | 0 | 0x0045D450 |
47 |
ResetFlybackMarker | 0 | 0x004599E0 |
18 |
CommsFromShip | 3 | 0x00458AC0 |
48 |
StopShipAnimation | 2 | 0x00458770 |
19 |
CommsFromPilot | 3 | 0x00458B10 |
49 |
SetShipAvoidance | 2 | 0x00459A30 |
1A |
SetInvulnerability | 2 | 0x00458BC0 |
4A |
MatchSpeed | 2 | 0x00459A90 |
1B |
MovingShipFollowCurve | 4 | 0x004585A0 |
4B |
MovingShipBackupCurve | 4 | 0x00458670 |
1C |
DisableObject | 2 | 0x004583C0 |
4C |
WaitForKey | 1 | 0x00459AE0 |
1D |
PositionRelative | 2 | 0x004584D0 |
4D |
TerminateMission | 0 | 0x00459BB0 |
1E |
WhenPlayerLastJumped | 0 | 0x00458580 |
4E |
TurretSetTarget | 2 | 0x00459BD0 |
1F |
StartMissileCam | 1 | 0x00458B60 |
4F |
SetAnyTriggerState | 4 | 0x0045D3A0 |
20 |
StartChaseCam | 1 | 0x00458B80 |
50 |
WaitForDirectorCam | 0 | 0x00459C90 |
21 |
SetPlayerTarget | 2 | 0x00458C80 |
51 |
KillAllScriptExecutionExecptMe | 0 | 0x0045D990 |
22 |
SetTargetable | 2 | 0x00458D50 |
52 |
StackDirectorCam | 5 | 0x00458300 |
23 |
PlayMusic | 2 | 0x00458DF0 |
53 |
Scanner | 1 | 0x00459CB0 |
24 |
StopDirectorCam | 0 | 0x00458E30 |
54 |
ReplaceSubObject | 2 | 0x00459CF0 |
25 |
SetActionCentre | 2 | 0x00458E60 |
55 |
Fire | 2 | 0x00459DD0 |
26 |
Dock | 3 | 0x00458EB0 |
56 |
MultiplayerScriptSync | 1 | 0x00459DF0 |
27 |
DisableTaunts | 1 | 0x00458F40 |
57 |
FriendlyFire | 0 | 0x00459F30 |
28 |
Fly | 3 | 0x00458F50 |
58 |
Cloak | 2 | 0x00459F40 |
29 |
CommsFromShipOnce | 3 | 0x00458FD0 |
59 |
ReplenishWeapons | 1 | 0x00459FA0 |
2A |
CommsFromPilotOnce | 3 | 0x00459020 |
5A |
WillsBlag | 1 | 0x0045A1C0 |
2B |
DisableLights | 2 | 0x00459070 |
5B |
ShowHudIcon | 2 | 0x0045A1F0 |
2C |
SetEnvironmentFX | 2 | 0x00459170 |
5C |
DisableListing | 2 | 0x0045A210 |
2D |
MultiPlayerSync | 0 | 0x004591E0 |
5D |
DisableObjectAtNextJump | 2 | 0x0045A250 |
2E |
DisableGenericComms | 1 | 0x004591F0 |
5E |
DarrensNaughtyBlag | 2 | 0x0045A290 |
2F |
DisableGuns | 2 | 0x00459200 |
5F |
(empty) | — | (empty) |
Divergences from the blog numbering (catalogue authoritative; blog index ≡ catalogue index everywhere else):
| idx | catalogue (authoritative) | blog name | what the blog name actually is |
|---|---|---|---|
05 |
Wait | WaitNSeconds | paraphrase of Wait |
16 |
ResetCodePriority | GTextPilotDefine | DisplaySubTitle's parameter label |
17 |
InterruptTriggerCode | ResetCodePriority | (blog shifted +1) |
18 |
CommsFromShip | InterruptTriggerCode | (blog shifted +1) |
19 |
CommsFromPilot | CommsFromShip | blog folds the pilot twin away |
26 |
Dock | RadiusOfSphere | SetActionCentre's parameter label |
27 |
DisableTaunts | ShipToDock | paraphrase of Dock |
28 |
Fly | DisableTaunts | (blog shifted) |
29 |
CommsFromShipOnce | ShipPointToFlyTo | Fly's parameter label |
2A |
CommsFromPilotOnce | CommsFromShipOnce | blog folds the pilot twin away |
51 |
KillAllScriptExecutionExecptMe | …ExceptMe | dev typo preserved in the binary |
58 |
Cloak | EntityToCloak | paraphrase of Cloak |
5F |
(empty) | Test_AI_Function | the disabled stub is at 0x60, not 0x5F |
The catalogue carries CommsFromPilot (0x19) and CommsFromPilotOnce (0x2A) as first-class
pilot-voice twins of CommsFromShip/…Once; the blog folds each pair into one, which — together with
the three parameter-label phantoms above — is what shifts its indices across the two ranges. Dev-original
names like PlayFostersLastStand, WillsBlag, DarrensNaughtyBlag — and copy-paste leftovers
(ReplenishWeapons and WillsBlag both inherit Cloak's "Entity to cloak" label) — confirm these are
recovered symbols, not guesses. Every command's parameter labels are listed in
§ Command parameters.
The value a script passes as SetAI's second parameter ("AI Mode", see § Command
parameters). In the bytecode it is almost always pushed by push_byte
(0x32 <n>) immediately before 21 0B, which is how Starlancer ME came to read 0x32 as "set AI";
0x32 itself is only a push (see the opcode table below). From Starlancer ME's AI Codes page,
0x00–0x44; not re-derived from the binary here.
00 Do Nothing |
12 Slow Rotate |
24 Boridin breakaway |
36 Eject Spin |
01 Fly Aimlessly |
13 Jump In |
25 FAIL |
37 Dock |
02 Launch Missile |
14 Jump Out |
26 Rotate Boridin warp projector |
38 Dark Reign shoot |
03 Launch Missile |
15 Find Scoop Up |
27 Ripper drop carried object |
39 Ripper end drop object |
04 Warp In |
16 Random Spin Slow |
28 Jump In |
3A Ripper attach pod to Mammoth |
05 Warp Out |
17 Random Spin Med |
29 Jump Out |
3B Eject fighter attack |
06 Fly |
18 Random Spin Fast |
2A (unknown) |
3C Disrupted |
07 Run Away |
19 Fixed Gate Jump In |
2B Huge explosion |
3D Capship list left |
08 Land |
1A Fixed Gate Jump Out |
2C Zero velocity + rotation |
3E Capship list right |
09 Escort |
1B Formation |
2D Fly ship backwards |
3F Friendly Fire |
0A Find New Target |
1C Fixed Gate Open |
2E Player Control |
40 Eject Player |
0B Explode |
1D Fixed Gate Close |
2F Multiplayer Control |
41 Ship Follow Curve Backwards |
0C Ripper grabs object |
1E Eject |
30 Avoid Target |
42 Mill |
0D Object Attach |
1F Fixed Gate Collapse |
31 Torpedo |
43 Deathmatch Respawn Effect |
0E Formation Regroup |
20 Match Speed |
32 Launch |
44 Deathmatch Dark Reign target |
0F Patrol Route |
21 Dark Reign shoot |
33 Fight |
|
10 Toggle Cloak |
22 Move to spawn pos |
34 Destroy itself |
|
11 Ship Follow Curve |
23 Turn object lights on |
35 Scoop Up |
The VM is a stack machine. Every opcode either pushes a value, pops values and pushes a result,
moves the instruction pointer, or calls something with the values on the stack. There is no separate
"expression" and "action" register: a command (0x21 <i>) simply pops its arguments. The handlers
are the 86 pointers at DAT_004F6350 (0x00–0x55; 0x00, 0x01, 0x08–0x13 and 0x50 are
null, leaving 71 opcodes; five pairs share a handler and are the same operation). Names follow
openreliant's docs/formats/dte.md, which read the handlers and corrected this table's earlier
pattern-level readings. Every row is now re-verified here against the handler bytes (capstone
over the exe read as data): the rows marked ✓ on 2026-09-22, the rest on 2026-09-29, when each
handler's use of the IP cell gave its operand width. All 71 widths agree with openreliant's, which
they reached independently by symbolic execution, and the handler addresses below match the table at
0x4F6350 entry for entry. a is the value below the top of the stack, b the top. Operand bytes
follow the opcode; d16 displacements are big-endian and are counted from the displacement's own
address.
| opcode | handler | name | effect |
|---|---|---|---|
0x02 / 0x03 |
0x45BAD0 / 0x45BB00 |
equal / not_equal |
pop b, a; push a == b / a != b (sete / setne) ✓ |
0x04 / 0x05 |
0x45BB30 / 0x45BB60 |
greater / greater_equal |
unsigned compare of a with b ✓ |
0x06 / 0x07 |
0x45BB90 / 0x45BBC0 |
less / less_equal |
unsigned a < b (cmp a, b; sbb; neg) / a <= b (cmp b, a; sbb; inc) ✓ |
0x14 / 0x15 |
0x45BBF0 / 0x45BC30 |
in_flight_group / not_in_flight_group |
pop b (flight group), a (ship); push whether FUN_00452AA0(a) == b ✓ |
0x16–0x1A |
0x45BC70–0x45BD30 |
assign, add_assign, sub_assign, mul_assign, div_assign |
store b through the select target [0x537408] (=, +=, -=, *=, unsigned /=); pops two slots — the value and the old value select_* pushed ✓ |
0x1B–0x1E |
0x45BD60–0x45BDF0 |
add, sub, mul, div |
pop b, a; push a op b (div unsigned) ✓ |
0x1F / 0x20 |
0x45BE20 / 0x45BE60 |
logical_and / logical_or |
pop b, a; push 1 or 0 ✓ |
0x21 <i> |
0x45BEA0 |
command |
call Executor command i (catalogue 0x4F0F50, stride 0x74): drops params values from the stack, calls the implementation, stores its result; section-24 flag bit 0 (inverted) goes to DAT_00537584 first ✓ |
0x22 <p> |
0x45BFA0 |
call_part |
call part p (runtime part table [0x538C94], stride 0x74): pushes the argument count, return IP, frame base and block end, then enters the block ✓ |
0x23 / 0x24 <d16> |
0x45C270 |
branch_if_zero |
pop; if zero, IP = displacement address + d16, else skip the two bytes ✓ |
0x25 / 0x43 |
0x45C6E0 |
return |
pop the call frame, or end the thread when the call depth is zero ✓ |
0x26 <n> |
0x45C2D0 |
push_array |
push [0x52A3F0 + 4n] ✓ |
0x27 <n> |
0x45C300 |
push_global |
push globals[n].value (DAT_005294F8 + 12n + 4) ✓ |
0x28 <n> |
0x45C340 |
push_constant |
push dword n of the running block's constant table ([0x5373F0] + 4n, the table that follows the block) ✓ |
0x29 <n16> |
0x45C370 |
push_constant_wide |
as 0x28 with a big-endian two-byte index; unused in the 44 shipped missions ✓ |
0x2A / 0x2B <len> text NUL |
0x45C3B0 |
push_string |
push a pointer to the text after the length byte; IP += len — the length counts itself (2A 0F "new_sim02.wav\0") ✓ |
0x2C <n> |
0x45C3E0 |
push_ship |
push &ships[n] (DAT_0052951C + 0x4C·n) ✓ |
0x2D <n> |
0x45C560 |
push_flight_group |
push &flight_groups[n] (DAT_005267CC + 0x14·n, section 4) ✓ |
0x2E / 0x32 <n> |
0x45C6B0 |
push_byte |
push the operand byte ✓ |
0x2F <n> |
0x45DA50 |
push_percent |
push top × n × 0.01 (fimul, then fmul by the 0.01f at 0x4DC730, truncated by __ftol); the top is not popped ✓ |
0x30 <n> |
0x45C5A0 |
push_local |
push thread local n (thread + 0x18 + 4n; a trigger block's locals hold the event's values) ✓ |
0x31 <n> |
0x45C680 |
push_argument |
push [frame_base + 4n] ✓ |
0x33–0x36 |
0x45DAB0–0x45DBD0 |
greater_f … less_equal_f |
the four compares through the FPU on zero-extended 64-bit loads (fild; fcompp) — the same results as 0x04–0x07 ✓ |
0x37–0x3A |
0x45DC30–0x45DCF0 |
add_assign_f … div_assign_f |
the assigns into a float target (fadd/fsubr/fmul/fdivr dword ptr [target]) ✓ |
0x3B–0x3E |
0x45DD30–0x45DE20 |
add_f … div_f |
integer arithmetic computed in floating point and truncated (_ftol, 0x4CF28C) ✓ |
0x3F <n> / 0x40 <n> |
0x45C790 / 0x45C7D0 |
select_array / select_global |
set the store target (DAT_00537408) to &array[n] / &globals[n].value and push its value ✓ |
0x41 <n> |
0x45C810 |
select_argument |
set the store target to argument n of the current frame and push its value ✓ |
0x42 <d16> |
0x45C2B0 |
jump |
IP = displacement address + d16 ✓ |
0x44 <n> |
0x45C850 |
push_squad |
push &squads[n] (DAT_005294FC + 0x0C·n, section 12) ✓ |
0x45 / 0x46 |
0x45C890 / 0x45C8D0 |
in_squad / not_in_squad |
FUN_00452AC0(a, b, 0xFF): the membership walk over sections 12 and 13, nested squads included ✓ |
0x47 / 0x55 <n> <c> |
0x45C460 |
push_component |
push &ships[n], then tag the slot with component c (FUN_0045D8B0); two operand bytes ✓ |
0x48 |
0x45C4B0 |
push_null |
push −1, for parameters labelled "can be NULL" ✓ |
0x49 <n> / 0x54 <n> |
0x45C4D0 / 0x45C520 |
push_curve / push_section_19 |
push §ion16[n] (stride 0x44) / §ion19[n] (stride 0x0C); push_curve is openreliant's name for section 16 ✓ |
0x4A / 0x4E / 0x4F |
0x45C110 / 0x45C1E0 / 0x45BF20 |
call_part_b / spawn_part_b / command_b |
the same as 0x22 / 0x4D / 0x21, through the part table [0x538C98] and the AI-function catalogue 0x4F3AD0. Wired and dead: see § The vestigial AI layer ✓ |
0x4B <c> <v> <o> |
0x45C5E0 |
push_event_value |
push value v kept for condition c on object o (records of 0x28 bytes at DAT_00538CA0; descriptor slot [DAT_0052952C + 0x1C·c + 0xC]) ✓ |
0x4C |
0x45C650 |
push_result |
push the last command's result: [[0x537578] + 0xB0], the slot 0x21 writes after every call ✓ |
0x4D <p> |
0x45C070 |
spawn_part |
move the part's arguments to a new thread (FUN_0045B960), start it on the part (FUN_0045B8D0) and carry on ✓ |
0x51 … |
0x45C910 |
random_branch |
count, big-endian default, then count four-byte arms of (big-endian target, threshold, a byte the handler never reads). Rolls rand() % 100 and takes the first arm whose threshold exceeds it — thresholds are not cumulative, so a later arm with a lower threshold can never win — or the default; an arm target of 0xFFFF means the default. Targets count from the opcode; it never falls through. See § random_branch slack ✓ |
0x52 <n16> / 0x53 |
0x45C420 / 0x45C510 |
push_ship_wide / nop |
push &ships[n], n big-endian / do nothing ✓ |
Blocks and constants. A block is a u16 length that counts its own two bytes, then
instructions ending in return, padded to a four-byte boundary; its constant table — the dwords
push_constant reads — follows immediately. mission1's first trigger block, byte for byte:
1c 00 length 28 (counts itself)
22 01 call_part 1 (F)Jumping to CONVOY
21 17 command 0x17 InterruptTriggerCode
27 00 28 00 02 push_global 0 (GV)convoykilled; push_constant 0 (= 1); equal
24 00 07 branch_if_zero -> +7 (to the call_part 24 below)
22 15 call_part 21 (F)GO HOME (Total Loss)
42 00 04 jump -> +4
22 18 call_part 24 (F)Jumping to Sherman
21 17 32 01 43 command 0x17; push_byte 1; return
32 01 padding to the 4-byte boundary
01 00 00 00 constant 0 = 1
08 00 22 0d filler dword (rounding to 8 bytes; never read)
Read with the corrected opcodes the block says if convoykilled == 1 then "GO HOME (Total Loss)"
else "Jumping to Sherman", which is the mission's plot. Read with the old 0x02 = != it said the
opposite — the clearest single piece of evidence that the old table was wrong.
Win/lose pattern (Starlancer ME's flagship observation, matching our 0x27/0x40
decode): when the kill condition is met, 0x40 selects a global and assign writes it; at
mission end 0x27 reads it, equal/not_equal compares, and branch_if_zero picks the
SUCCESS or FAIL branch — which play different comms (…_001.ut vs …_002.ut). Missions
default to "failed" and are promoted to one of the five outcome grades (see dte-format.md
§Outcomes).
tools/dte_parse.py decode <mission> --section script (rebuilt 2026-09-29) lists every routine:
a block plus the constant table that follows it. Entry points are the parts of section 8 (the
loader FUN_00452F50 → FUN_00452FD0 sets a part's block to script + start × 2 from +0x0A and
its argument count from +0x0D) and the links of the triggers some object's slice of section 5
holds. It follows control flow from each entry rather than sweeping, because jump, return and
random_branch never fall through. A misread width can't pass silently. A path that leaves its
block, two instructions sharing a byte, or a null opcode is an error. The self-test
(tests/dte_selftest.py) proves each tripwire, including the old tool's one-byte push_string.
dte_parse.py sweep <dir> --script checks the result against the shipped missions. Over all 44:
| check | result |
|---|---|
| routines / instructions decoded | 3,703 / 122,236, zero decode errors |
| routines tiling section 6 exactly (block + constants, rounded to 8, up to the next routine) | 44 / 44 missions |
part extents (+0x10) agreeing with the decoded layout |
all |
paths that run into a block's limit instead of a return or jump |
none |
| bytes inside a block that nothing reaches | none, besides four random_branch slack regions (below) |
The same sweep counts opcode use. return is always 0x43, exactly once per routine (3,703 of
3,703). Never used: 0x25, 0x29, 0x2F, 0x41, 0x53–0x55, the whole floating-point
family 0x33–0x3E, and every arithmetic op except += and -= (mul_assign, div_assign,
add, sub, mul, div). random_branch occurs four times in the entire game.
openreliant listed "four 32-byte regions that nothing reaches" as open. They are unused slots of
random_branch's arm table. All four shipped random_branches (mission15 ×2, mission18,
mission23) have two arms, so the live instruction is 12 bytes. Yet each one's first arm targets
+44 from the opcode, which is exactly where its gap ends. 44 = a 4-byte head + ten 4-byte arm
slots: the mission compiler laid the instruction out with room for ten arms and filled two. The
handler reads only count arms and never falls through, so slots 3–10 are never read.
The slack isn't zeroed. It holds stale bytes that look like code: mission15's first gap contains
2c 48 32 0a 28 00 2c 06 21 0b at byte 4094, which is the SetAI sequence that appears for real
44 bytes later (4138) as the second arm. The arms' never-read fourth bytes carry the same residue (0x2C, 0x42, 0x28
are opcode values). So does the padding before a constant table: mission1's first block pads with
32 01, the bytes of push_byte 1. The compiler reused a buffer without clearing it. Since every
instance has count 2, "ten slots always" can't be told apart from "count + 8". The tool labels such
a gap as slack, not as unreached code.
A second, AI-owned script layer is wired through the engine end to end, and never used:
| piece | where | state |
|---|---|---|
opcodes 0x4A call_part_b, 0x4E spawn_part_b, 0x4F command_b |
handlers 0x45C110, 0x45C1E0, 0x45BF20 |
0 uses in 122,236 instructions |
| part descriptors | section 17 (DAT_005294EC), built into [0x538C98] by FUN_00452F50 |
empty in all 44 |
| bytecode | section 18 (DAT_00525FB4): FUN_00453020 sends any descriptor outside section 8 there |
empty in all 44 |
| per-function flags | section 25 (DAT_00525F90), read by 0x4F as section 24 is by 0x21 |
empty in all 44 |
| function catalogue | 0x4F3AD0, installed beside the command catalogue by FUN_0045CE30 into DAT_005267D4 |
one entry, Test_AI_Function ("Testttt Param 1", "Testttt Param 2"), no implementation; counted as 0 |
| dispatcher | 0x4F calls FUN_0045D800(impl, params) instead of the implementation |
mov eax, 1; ret 8 — a stub that reports success |
command_b pops its arguments, stores 1 as the result, and does nothing else. A script that used
the layer would run, and every AI function would quietly succeed.
These two enums are Starlancer ME's data (full lists on their Ship list and Pilot list
pages); reproduced here as anchors and cross-referenced to our SHIPSTATS.BIN ordering (see
stats-format.md). Faction = ship-name prefix (Us/Uk/Jap/Ger/Fr/It =
Alliance; Ussr/Mid/Chi/Coal = Coalition).
Ship types (0x00–0xFF, anchors): 00 Us Predator · 04 Us Coyote · 07 Us Patriot ·
0C Uk Reliant (carrier) · 0D Jap Yamato (carrier) · 0F Uk Kestrel · 16 Us Ulysses ·
1F Us Ripper · 20 Ger Lueneburg · 21 Uk Mammoth · 2B Ussr Sabre · 44 Ussr Dark Reign ·
48 Ussr Boridin · 4A Torpedo · 4E–5B debris/corpses · 5F–6C,C9–D3 planets ·
79–8B asteroids · F4–FF "Tiger"-variant fighters.
Pilots / IFF (0x00–0x7B, exactly 124): named pilots & callsigns, e.g. 14 Cat Foster,
7A Cat Foster Prowler. The pilot page does not map pilot → faction; faction must be
inferred from the ship prefix. (Open item — see contribution notes.)
Resolved 2026-09-10 — the ID is a record index.
pilotstats.bin, decompressed out ofresource.hog, holds exactly 124 records of 352 bytes, and the mission-script pilot ID is that record's index:0x14=Cat Foster,0x7A=Cat Foster Prwlr, matching the values above, which were derived from mission data alone. The full name table can therefore be read rather than inferred:python tools/hog_extract.py resource.hog -o out/ -d -f pilotstats.bin python tools/slstats.py list out/pilotstats.bin
slstats listgives the correct index and name for each pilot, but ignore its numeric columns here: it applies the ship field layout, and pilot records are not float stats (seestats-format.md).This closes the "~124" estimate and confirms the
0x00–0x7Bbound exactly. Faction is still not carried on the pilot record, so the ship-prefix inference stands. The RefPack audit that made the file readable is credited inexternal-re-credits.md.
The developers' own per-parameter descriptions, recovered from the catalogue at 0x4F0F50 (commands
not listed take no parameters). Spelling, casing and copy-paste leftovers are preserved verbatim.
00PrintShipName — Test Param 1; Test Param 201CreateTimer — Unique ID to identify timer; Function to execute when timer activates; Number of seconds before timer activates; Number of activations (0 == continuous)02DestroyTimer — Unique ID of timer to be destroyed03CreateFlightGroup — Flight Group name to initialize04DestroyFlightGroup — Flight Group name to destroy05Wait — Number of Seconds to wait06PlaySpeech — Name of speech file08PlayCommsMovie — Name of movie file; Name of speech file; GText thingy hangover err....0APrintDebugMessage — Text to print0BSetAI — Entity to be controlled; AI Mode; Initialize Immediately (T/F); Entity to target (can be NULL)0CClearAI — Entity to be cleared0DSetPatrolRoute — Entity to send to patrol route; Patrol route to follow0ESetPilot — Ship to host pilot; Pilot to fly ship0FSetTriggerState — Entity owning trigger; Trigger type to enable/disable; TRUE for enable; FALSE for disable10StartDirectorCam — Curve for camera to follow (or Ship for static cam); Ship for camera to track (can be NULL); Duration of camera (seconds); Tracks curve to this ship's speed (can be NULL); Ships to disable for duration11StartShipAnimation — Ship to animate; Animation Name12ShipFollowCurve — Entity to follow curve; The curve for the entity to follow; Duration of movement (seconds)13SetupLaunch — Entity to be launched; Ship to launch from; Launch position14StartLaunch — Entity to be launched15DisplaySubTitle — GText pilot define16ResetCodePriority — Entity to have priorities reset18CommsFromShip — Ship sending comm; Head movement; Name of speech file19CommsFromPilot — Ship sending comm; Head movement; Name of speech file1ASetInvulnerability — Ship concerned; Invulnerability (0 - non, 1 - player can hit, 2 - fully invulnerable, 3 - Eject before exploding1BMovingShipFollowCurve — Entity to follow curve; The curve for the entity to follow; Duration of movement (seconds); Entity for curve to use as its start offset1CDisableObject — Entity concerned; True/False1DPositionRelative — Entity to position; Ship/Point to use as relative marker1FStartMissileCam — Ship that fired missile20StartChaseCam — Ship to follow21SetPlayerTarget — Player Ship; Ship to target22SetTargetable — Entity; true - object targetable, false - not targetable23PlayMusic — Name of music file; True - Play Immediately, false - Fade old tune first25SetActionCentre — Object to action around; Radius of sphere - 0 for default26Dock — Ship to dock; Object ship is to dock to; Docking port27DisableTaunts — true - Disable bad guy taunts28Fly — Ship; Point to fly to; Speed (0 - Default)29CommsFromShipOnce — Ship sending comm; Head movement; Name of speech file2ACommsFromPilotOnce — Ship sending comm; Head movement; Name of speech file2BDisableLights — Entity; True or False2CSetEnvironmentFX — Effect type to set; On(TRUE) or Off(FALSE)2EDisableGenericComms — True - Disable all hard coded comms events2FDisableGuns — Entity; TRUE - disable guns, FALSE enable guns30SetNavPoint — Entity; Nav Point31SetEscortPoint — Entity; Escort Point33DisableMissiles — Entity; TRUE - disable missiles, FALSE enable missiles34DisableEngines — Entity; TRUE - disable engines, FALSE enable engines35DisableEject — Entity; TRUE - disable eject, FALSE enable eject36SetHostile — Entity; true - entity(s) hostile, false - friendly39SetPrimaryTarget — Entity3AWaitForJumpOrLaunch — Entity3BDoNotDisturb — Entity; true - dont disturb, false - can disturb3CSetEnvironmentFXNebula — Index of nebula material (0..6)3DStartShipAnimationReverse — Ship to animate; Animation Name3ESnapToPoint — Ship to move; Point to move to40OpenInstrument — Instrument number to open41CloseInstrument — Instrument number to close42DestroySubObject — SubObject to destroy; true - keep damaged model, false - no damaged model43SetObjective — Objective number; State(0=Inactive, 1=Active, 2=Current)44SetRescueProbabilities — Probability of Nanny Rescue ( 1-100% ); Probability of Antanov Capture ( 1-100% ); Probability of being Destroyed ( 1-100% )45IsShipThisPlayer — Ship to test46SetFlybackMarker — Entity concerned; Range48StopShipAnimation — Ship to stop animation for; Animation Name49SetShipAvoidance — Entity concerned; TRUE - Disable Avoidance code, FALSE - Enable avoidance code4AMatchSpeed — Entity concerned; TRUE - Enable Match Speed, FALSE - Disable Match Speed4BMovingShipBackupCurve — Entity to follow curve; The curve for the entity to follow; Duration of movement (seconds); Entity for curve to use as its start offset4CWaitForKey — Key number to wait for4ETurretSetTarget — Turret; Entity to target4FSetAnyTriggerState — Entity owning trigger; Trigger type to enable/disable; TRUE for enable; FALSE for disable; Trigger Type Number(for triggers of same type - Count from 0)52StackDirectorCam — Curve for camera to follow (or Ship for static cam); Ship for camera to track (can be NULL); Duration of camera (seconds); Tracks curve to this ship's speed (can be NULL); Ships to disable for duration53Scanner — Object to scan for - NULL to disable54ReplaceSubObject — SubObject to replace; Object to replace it with55Fire — Ship to fire; Duration56MultiplayerScriptSync — Sync number58Cloak — Entity to cloak; True - Cloak on, False - cloak off59ReplenishWeapons — Entity to cloak5AWillsBlag — Entity to cloak5BShowHudIcon — Icon; 0 - off, 1 - on, 2 - flash5CDisableListing — Ship; true - stop listing, false - enable listing5DDisableObjectAtNextJump — Ship; true - disable, false - enable5EDarrensNaughtyBlag — Ship; Ship
What a mission maker can copy from the 44 shipped missions, counted over the unmodded
resource.hog with openreliant's sltool dte export (2026-10-02). The leads are mini's
mission-maker guide in StarLancerEditor
(docs/mission-maker-guide.md, MIT). Each was checked against the missions and against
openreliant's engine, and one did not hold as stated.
- The jump. Every campaign mission but 23 has exactly one
PlayerReadyToJumptrigger, and mission 23 has none. In 19 of those 23 it repeats (always), and its routine yields withInterruptTriggerCode(0x17) between the jumps, so each firing runs one leg; missions 8, 16 and 21 fire it once, and 11 repeats it without yielding. The arrivals'JumpedIntriggers on the player mostly take the same shape. A script brings the jump drive online by setting variable 0, the value the JUMP DRIVE prompt on the display checks (openreliant calls itjump_ready). - Landing. Setting variable 10 (openreliant's
landing_cleared) is how a script lets the player land; mission 1's script does it on the Reliant's arrival. - Never used. No shipped mission calls
SetNavPoint(0x30).WaitForKey(0x4C) appears only in mission 31. - Speech.
CommsFromShipis followed byWaitForMovie1,364 times andCommsFromPilot727 times;PlaySpeech, which opens no comms window, is followed byWaitForSpeech115 times. A plainWaitfollows now and then. - The player's wing. A flight group in wing 0 holds six ships in 26 of the 31 such groups.
Mission 29 holds one, 99 two and 31 five (29 and 31 are among the simulator's and Instant
Action's missions, 29 to 35), and 28 and 271 hold seven, past the wing's six slots
(openreliant's
docs/engine/missions.md, "The wings"). A player's wing of fewer than six therefore does not crash the original by itself, though mini reports a template crashing with fewer than five wingmen. Unverified: what crashed it.
The numeric opcode/command/AI/ship/pilot tables and the observed runtime semantics are the
work of Captain Foster / "Starlancer ME" (https://starlancerme.blogspot.com/,
@CaptainFoster), who also
maintains a Python "Mission Ship Editor". This document pairs that black-box research with our
static disassembly (implementation addresses, parameter counts, the VM, struct layouts). Where
the two overlap they agree; where they differ it is noted above and in
dte-format.md.
The stack-machine reading of the opcodes — equal/not_equal, push_constant, push_byte,
push_string, branch_if_zero, call_part, return and the rest — follows openreliant
(https://github.com/vdmkenny/openreliant, docs/formats/dte.md, CC BY-SA 4.0), which corrected
this document's earlier pattern-level readings; every row marked ✓ above was re-verified here
against the handler bytes. The names are theirs; the verification is ours. See
dte-format.md §9b for the evidence table.