.DTE missions¶
Each of the 44 missions is one image: the ships it places, the globals it seeds, the triggers it arms, and the script they run.
sltool dte info <mission> # counts and sizes
sltool dte sections <mission> # the 27-entry directory
sltool dte ships <mission> # placed ships and nav points
sltool dte triggers <mission> # triggers, with the object whose slice holds each
sltool dte strings <mission> # the string pool
sltool dte parts <mission> # the script's named routines
sltool dte script <mission> # every trigger block and part, disassembled
make check-missions # parse all 44
Container¶
A mission inside a .HOG is RefPack compressed; one loose in missions\ is stored expanded. The
archive's reader expands a member that begins 10 FB, but a loose file is read as it is, so it
must be stored expanded (Missions). A retail install carries two
loose missions, mission18.dte and mission25.dte, which begin 08 33 where their archive copies
begin 10 FB. sltool hog expands members as it extracts.
Directory¶
The image opens with 27 entries of 8 bytes:
| Offset | Type | Field |
|---|---|---|
| 0 | u16 | Records in use, except where noted below |
| 2 | u8 | Unused |
| 3 | u8 | Four format flags, in the low bits, which the binder notes and nothing reads. Every entry of a shipped mission holds the same: 15 in most, 7 in mission191 and mission271, 3 in mission801 and 1 in mission88 |
| 4 | u32 | Offset of the section, or 0xFFFF when unused |
Capacity is fixed: a section's offset is the same in every mission built from the same template, and the count says how much of the reserved room is filled, so most missions are exactly the same size.
| # | Section | Stride | Contents |
|---|---|---|---|
| 0 | strings | String pool | |
| 1 | operands_a | 2 | Operand resolution |
| 2 | globals | 0x0C |
Named values the script reads and writes |
| 3 | ships | 0x4C |
Placed ships, stations and nav points |
| 4 | flight_groups | 0x14 |
Flight groups |
| 5 | triggers | 0x30 |
|
| 6 | script | Bytecode. The count is in halfwords | |
| 7 | objects | 8 | The object table, indexed by object ID: see Objects |
| 8 | parts | 0x1C |
One descriptor per named script routine |
| 10 | script_flags | 1 | One flag per script byte, which the interpreter consults for the script debugger |
| 12 | squads | 0x0C |
Squads |
| 13 | squad_members | 0x0C |
Squad membership records |
| 14 | formations | 8 | Ship formations: the first of each one's points in section 15 at +4 |
| 15 | formation_points | 0x10 |
The formations' points |
| 16 | curves | 0x44 |
The curves the director's camera flies along: see Curves |
| 17 | parts_b | 0x1C |
Part descriptors for section 18 |
| 18 | script_b | A second bytecode section | |
| 21 | openreliant_name | 1 | OpenReliant's own: the mission's name, see OpenReliant's mission name |
| 22 | operands_b | 2 | |
| 24 | command_flags | 2 | One word of flags per Executor command (dte.CommandFlags): bit 0, players, has for_each_ship walk the players' ships |
| 25 | command_flags_b | 2 | The same for the second command catalogue |
| 26 | operands_c | 2 | A third operand table, beside sections 1 and 22: 0x004529D0 picks one of the three by a bank number |
Sections 17 to 21 and 25 are empty in all 44 missions. The engine reads nothing of sections 9, 20,
21 and 23: the binder binds 21 into a local variable of its own, and the rest into globals nothing
reads. Sections 9 and 23 hold records in some missions, likely the original editor's. Of the
directory's 128 slots before the first section, at 0x400, the binder reads the first 27. Slot 27
holds the file's size in most missions and is unused in the rest, and slots 28 on are unused in all
of them. Section 24 holds one word per command of the catalogue (dte.CommandFlags):
command passes bit 0 of the word, inverted, to the engine before each call, and with the bit clear
for_each_ship passes over the players' ships in a flight group, a squad's too
(Script VM). command reads the low byte of the word the command's number
places past the section's offset, whatever the section's count. In the 36 missions of the template
each word has a bit for each of the command's parameters, the first's bit 0, save the words of
ClearAI, SetPatrolRoute, SetTriggerState, SetAnyTriggerState, MovingShipFollowCurve and
MovingShipBackupCurve, which are 0. The other missions either leave the section unused, at
0xFFFF, which lies inside section 1, so that its operands serve as the flags, or start it at the
file's end, where the game reads past its copy of the file. Fix: OpenReliant takes no flags past
the file. In every mission script_flags holds twice the count of section 6: one entry per script
byte.
OpenReliant's mission name¶
This is OpenReliant's convention, not the game's. OpenReliant keeps a name for a mission in
section 21, which the game binds but never reads and no shipped mission uses. OpenReliant shows it
(openreliant missions); the game plays a mission with it as it plays any other, and a mission is
complete without it.
The section's count is its size in bytes. It holds an 8-byte header, then the name:
| Offset | Type | Field |
|---|---|---|
0x00 |
char x4 | Tag: ORMN |
0x04 |
u16 | Version: 1 |
0x06 |
u16 | The name's length in bytes |
0x08 |
The name, in UTF-8, then a NUL |
OpenReliant reads a name only where the tag is ORMN, the version 1, and the name fits in the
section; anything else in section 21 it leaves alone. Other mission tools may not keep the section
when they write a mission out.
String pool¶
A run of NUL-terminated names, referenced by byte offset into the pool, not by index, which is
why a u16 suffices: the pool reserves 65,535 bytes. Byte offsets resolve every ship name in all 44
missions, for example Player_Ship, (A1)Naginata, (WL)Viper's Coyote, Convoy Nav Point.
Ships¶
Stride 0x4C, one per placed object, nav points included.
| Offset | Type | Field |
|---|---|---|
0x00 |
u32 | Object ID |
0x04 |
u16 | Name, as a string pool offset |
0x08 |
f32 x3 | Position, copied from 0x1C when the mission loads |
0x14 |
u8 | Flight group, or 0xFF for none |
0x15 |
u8 | Pilot: the record of pilotstats.bin that flies the ship, or 0xFF for none, as the player's own record, the nav points and the planets have |
0x17 |
u8 | Flags, the engine's own: bit 0 marks the ship destroyed. Zero in the files |
0x18 |
u16 | Kind: the ship's type below 0x100; nav points and markers use 999 and 0x3E3 to 0x3E8, waypoints 0x3E5. The mission makes a marker of a nav point (999, dte.Ship.nav_point_kind), a waypoint, or a point of kind 0x3E3 or 0x3E4 (dte.Ship.isMarker, 0x00457CD9) |
0x1B |
u8 | Set for a waypoint once binding the mission has listed it |
0x1C |
f32 x3 | Position as authored |
0x28 |
u16 | The kind of the ship it launches from, the first of the mission's ships of that kind |
0x2B |
u8 | The gate of that ship it launches through, or 0xFF for a ship that does not launch |
0x2E, 0x3A, 0x4A |
i16 | Yaw, pitch, roll, in whole degrees |
0x30 |
u32 | The ship's intact components, a bit each |
0x34 |
u16 | The formation point Formation Regroup flies the ship to, or 0xFFFF for none |
0x3D |
u8 | The loadout tier its missile racks are fitted by (create.settledTier): 0 or 255, as most records hold, asks for the campaign's |
0x40 |
i16 | For a point of kind 0x3E3, the index of the curve it marks a place on, or -1 for none |
0x44 |
f32 | The share of the way along that curve the place lies at |
When the mission's script starts, the engine clears the flags at 0x17 and sets every bit at
0x30. Destroying component n of the ship clears bit n & 31, and destroying the ship sets bit 0
of the flags and raises its Destroyed event, which it raises no more.
Each angle sits two bytes after its runtime copy, at 0x2C, 0x38 and 0x48. Every record holds
angles within [-360, 360]. Positions are absolute, on the order of 10^7. in_flight_group reads
the flight group byte.
Flight groups¶
Stride 0x14.
| Offset | Type | Field |
|---|---|---|
0x00 |
u16 | Object ID |
0x04 |
u16 | Name, as a string pool offset, such as (FG)Reliant |
0x08 |
u8 | The wing the mission lists the group's ships in (dte.FlightGroup.Wing): 0 the player's, 1 and 2 two more, 0xFF none |
0x09 |
u8 | How many of the mission's ships are in the group |
0x0C |
u32 | Where the group's first ship stands in the list of the groups' ships, or -1 |
Binding the mission works out 0x09 and 0x0C, whatever the file holds
(Missions).
Curves¶
Stride 0x44: a cubic Hermite spline from one of the mission's ships to another, which the
director's camera flies along (The director's camera).
| Offset | Type | Field |
|---|---|---|
0x00 |
u32 | The ship it starts at, as a trigger operand names it (Operands) |
0x04 |
u32 | The ship it ends at, or 0xFFFF in the low halfword for none |
0x08 |
f32 x3 | Where it starts |
0x14 |
f32 x3 | Where it ends |
0x20 |
u32 | The ship its leaving tangent is drawn to |
0x24 |
u32 | The ship its arriving tangent is drawn from |
0x28 |
f32 x3 | Its leaving tangent: from where it starts to the first of those ships |
0x34 |
f32 x3 | Its arriving tangent: from the second of them to where it ends |
0x40 |
u32 | Unknown |
Its ships are points of kind 0x3E4 in the shipped missions, and the record holds their places as
the mission placed them. The engine weighs each tangent ten times as it stands.
Objects¶
Ships, flight groups and squads each start with an object ID, and events name their subject by one. Section 7 is indexed by it:
| Offset | Type | Field |
|---|---|---|
| 0 | u8 | Kind: 0 ship, 1 flight group, 2 squad |
| 1 | u8 | Number of triggers in the object's slice |
| 2 | u16 | Index of the first |
| 4 | u32 | Unknown |
In all 44 missions every ship's ID is unique within its mission and lands on a kind-0 entry, and the
kind-1 and kind-2 entries correspond one for one to the flight groups and squads. The squad
membership walk behind in_squad reads the kind: a flight group member is tested with
in_flight_group, and a squad member recursively.
A squad's +0x08 is the index of its first record in section 13, or 0xFFFF. A membership record
holds the member's object ID at +0, the owning squad's index at +4, and at +8 one of the
member's components by index, or 0xFF for the whole member. A squad's records are consecutive.
Members may be ships, flight groups, other squads, or single components of a ship, such as a capital
ship's turrets.
Triggers¶
Stride 0x30. A trigger runs a block of script when an event it watches happens to its subject.
| Offset | Field |
|---|---|
0x00 |
Condition |
0x01 |
Repeat mode |
0x02 |
Link: the block to run, as a halfword offset into the script; 0xFFFF for none |
0x14 |
Armed, set at mission start for every trigger an object's slice holds |
0x15 |
Qualifier: the component of the subject watched, by index; 0xFF for the subject itself |
0x16 |
Zero runs the block's thread at once, inside the event; otherwise the scheduler does |
0x19 |
Firings left, for repeat mode 2 |
0x1A |
The firings repeat mode 2 has each time the script arms the trigger (SetTriggerState), which 0x19 takes again then |
0x1C |
Operands, four bytes each, one for each value the condition's events carry: see Operands |
A trigger holds no subject: it sits in its subject's slice of the trigger list, in the
object table. When an event happens to an object, trigger_match (0x0045CEA0) walks
the slice and fires each trigger that is armed, has the event's condition and qualifier, and whose
operands pass. Firing starts a thread at script + link * 2, unless a thread the trigger started
is still running. An operand whose low halfword is 0xFFFF is not checked. The
VM at run time describes the matching in full.
No trigger is in two slices, and a trigger in no slice can never fire. The condition decides which kinds of object a trigger can belong to, and every trigger agrees with its condition's.
Repeat mode 0 disarms the trigger when it fires, 1 never disarms it, and 2 disarms it when the
counter at 0x19 runs out. A trigger whose operands fail a check stays as it is.
The qualifier must equal the event's. A ship's components, such as a capital ship's turrets and
subsystems, are numbered among the
components of its live object. A hit on one raises ShotAt with
its index and, except for hits of one kind, ShotAt for the ship; destroying one raises Destroyed
with its index. Every other event carries 0xFF, the subject itself.
An event on a ship is raised on its flight group and on the squads that hold it too. Destroyed on a flight group or squad goes ahead only once every member, or the member's named component, is destroyed; until then it fires only the group's triggers with repeat mode 1. For ShotAt the group's event carries the members' average damage value instead of the ship's. See Events.
Operands¶
Operand n goes with value n of the condition's events, such as the attacker of ShotAt or the
distance of Proximity. The matcher checks an operand against the event's value when the condition
marks the value as checked and the operand's low halfword is not 0xFFFF. It reads the operand by
the value's kind mask (trigger_operand_value, 0x004530A0):
- A number is taken as it is.
- For a ship value, bit
0x2000of the low halfword matches any of the players' ships: in a game of one, the player's. - Anything else is a reference to a ship, a flight group or a squad, which the matcher turns into the address of its record, the form in which events pass them:
| Bits | Field |
|---|---|
| 0 to 15 | Index into the section the tag selects |
| 16 to 23 | Tag: 0x00 ships, 0x01 flight groups, 0x16 squads |
| 24 to 31 | Unknown. Ignored |
Any other tag in a checked operand is a fatal error: the game reports NULL entity referenced in
script, or oh dear dear, and exits. sltool dte triggers lists each trigger's set operands under
their values' labels, naming referenced objects by object ID.
Conditions¶
The engine's descriptor table at 0x4F6698 lists 35 conditions, named in the payload as TT_*
constants; the last two are internal. A descriptor gives the kinds of object the condition applies
to, the values its events carry, and the handlers that can veto them: see
Conditions. The table lies just past the VM's dispatch table,
followed by the lists of event values. make vm-conditions regenerates
src/engine/vm/conditions.zig from the binary alone.
Script¶
Section 6 is the mission's bytecode: count * 2 bytes. The interpreter is a dispatch loop:
handler = table[code[ip]] // 86 entries at 0x004F6350
ip += 1
continue while handler() != 0
Instruction set¶
71 opcodes: 0x02 to 0x07 and 0x14 to 0x55, less 0x50. The other entries of the
86-entry table are null. The condition descriptor table follows it, and its handler pointers look
like further entries.
Five opcodes run the same handler as another and are the same operation: 0x24 and 0x23, 0x2B
and 0x2A, 0x25 and 0x43, 0x32 and 0x2E, 0x55 and 0x47.
Every opcode's size and effect on the instruction pointer is read from its handler. The dispatcher
calls a handler with ECX pointing at the cell holding the instruction pointer, already advanced
past the opcode, and execution resumes wherever the handler leaves that cell. src/tools/tablegen
reads the dispatch table from the binary, parses each handler from Ghidra's exported disassembly,
and symbolically executes every path through it, tracking that cell. Paths must agree, or it reports
the handler instead of guessing. Its output is
src/engine/vm/opcodes.zig:
make ghidra-annotate # define and name the handlers, among the rest
make ghidra-export-game
make vm-opcodes
Nothing calls a handler directly, so auto-analysis leaves most of them undefined; make
ghidra-annotate defines each one from the committed opcode table.
| Form | Opcodes | Next instruction |
|---|---|---|
sequential |
the rest | After the operands |
branch |
0x23, 0x24, 0x42 |
The operands are a displacement |
inline_data |
0x2A, 0x2B |
After the run the operand byte measures |
transfer |
0x22, 0x25, 0x43, 0x4A, 0x51 |
Not known statically |
The rest are a fixed size, with up to three operand bytes. Three carry their own length:
0x2Apush_string (and0x2B) takes a length byte that counts itself, pushes a pointer to the bytes after it, and steps over them. They are a NUL-terminated string, usually the name of a.wavof speech or a.utcutscene:mission81opens by cueingnew_sim02.wav.0x51random_branch takes a count, a big-endian default target, then that many four-byte arms of big-endian target, threshold and a byte the engine never reads:3 + 4nbytes. It rolls a number below 100 once (vm_random_branch,0x0045C910) and takes the first arm whose threshold exceeds it, or the default, which an arm targeting0xFFFFalso takes. Its targets count from the opcode. This is the one encoding read by hand: its length depends on a byte the analysis cannot follow. The shipped missions' four have two arms each but room for ten: the first arm's code starts 44 bytes past the opcode, and the eight slots between hold stale bytes nothing reaches.
The analysis also records whether any path leaves the instruction pointer just past the operands.
None does for 0x42 jump, so it never falls through. The transfers are classified by name, and a
compile-time check rejects any transfer in the table without a classification:
| Transfer | Opcodes | Next |
|---|---|---|
| Call | 0x22 call_part, 0x4A call_part_b |
Into a part, then the following instruction |
| Return | 0x43 return, 0x25 |
Out of the part, or out of the thread |
| Random branch | 0x51 |
One of its arms |
The analysis cannot classify these itself: a call's only sequential path is its missing-part early-out, and return's path that leaves the pointer alone is the one that ends the thread, which it signals through its return value.
Opcodes¶
A stack machine. Names follow the handlers; a is the second value from the top, b the top.
| Opcodes | Names | Effect |
|---|---|---|
0x02 to 0x07 |
equal, not_equal, greater, greater_equal, less, less_equal |
Pop b and a; push the unsigned comparison |
0x33 to 0x36 |
greater_f ... less_equal_f |
The same through the FPU, with the same results |
0x1B to 0x1E |
add, sub, mul, div |
Pop b and a; push the result |
0x3B to 0x3E |
add_f ... div_f |
The same computed in floating point and truncated |
0x1F, 0x20 |
logical_and, logical_or |
|
0x14, 0x15 |
in_flight_group, not_in_flight_group |
Whether ship a is in flight group b |
0x45, 0x46 |
in_squad, not_in_squad |
Whether object a is in squad b, following nested squads |
0x3F to 0x41 |
select_array, select_global, select_argument |
Make slot n the store target; push its value |
0x16 to 0x1A |
assign, add_assign, sub_assign, mul_assign, div_assign |
Pop the value and the target's old value; store into the target |
0x37 to 0x3A |
add_assign_f ... div_assign_f |
The same into a float target |
0x26, 0x27, 0x30, 0x31 |
push_array, push_global, push_local, push_argument |
Push slot n's value. A trigger block's locals hold its event's arguments |
0x28, 0x29 |
push_constant, push_constant_wide |
Push constant n of the running block |
0x2A (0x2B) |
push_string |
Push a pointer to inline text and step over it |
0x2C, 0x52 |
push_ship, push_ship_wide |
Push a pointer to ship n |
0x47 (0x55) |
push_component |
The same, naming the ship's component given by a second operand |
0x2D, 0x44, 0x49, 0x54 |
push_flight_group, push_squad, push_curve, push_section_19 |
Push a pointer to record n of sections 4, 12, 16 and 19 |
0x32 (0x2E) |
push_byte |
Push the operand byte |
0x2F |
push_percent |
Push n percent of the top value |
0x48 |
push_null |
Push -1, for parameters labelled "can be NULL" |
0x4C |
push_result |
Push the last command's result |
0x4B |
push_event_value |
Push a value the event matcher stored for an object |
0x21, 0x4F |
command, command_b |
Call an Executor command, popping its arguments |
0x22, 0x4A |
call_part, call_part_b |
Call a part |
0x4D, 0x4E |
spawn_part, spawn_part_b |
Start a part on a new thread and carry on |
0x43 (0x25) |
return |
Leave the part, or end the thread |
0x23 (0x24) |
branch_if_zero |
Pop a value; branch if zero |
0x42 |
jump |
|
0x51 |
random_branch |
|
0x53 |
nop |
A compile-time check in src/formats/dte.zig keeps the names and the
derived table in step.
push_component tags the stack slot it fills with the component, in a list at 0x537420 that
empties after every command. A command whose argument can be a component, such as
DestroySubObject, ReplaceSubObject, SetPrimaryTarget or SetPlayerTarget, looks its
arguments up there (vm_argument_component, 0x0045D950) to learn which component they name.
Control flow¶
call_parttakes a part index into the runtime part table (stride0x74: block pointer at+0, argument count at+4), pushes the argument count, return address, frame base and block end, sets the frame base tostack - (4n + 16), and enters the block.returnunwinds that, or ends the thread when the call depth is zero.spawn_partmoves the part's arguments from this thread's stack to a new thread's, starts the new thread on the part, and carries on. At most 32 threads run at once.call_part_bandspawn_part_bdo the same through the second part table, which serves section 18.command_breads the second command table (0x004F3AD0), whose one command,Test_AI_Function, has no implementation, so the table counts none; the game calls a stub in its place that gives 1 (0x0045D800), socommand_bpops the command's arguments and gives 1. Sections 17, 18 and 25 are empty in every shipped mission, and no script uses these three opcodes.branch_if_zeroandjumptake a big-endian displacement, counted from its own position. The handler loads it as an unsigned 16-bit number and adds it to the instruction pointer (vm_jump,0x0045C2B0), so a branch only goes forward. The script's other two-byte operands, the wide indices ofpush_constant_wideandpush_ship_wide, are big-endian too; the rest of the format is little-endian.
A thread keeps its block's end in [0x5373F0], which is where push_constant reads from.
Commands¶
command n calls entry n of the Executor catalogue at 0x4F0F50, which vm_install_commands
(0x0045CE30) installs and counts up to the first entry with no implementation: 95 commands.
An entry is 0x74 bytes and describes itself in the developers' words:
| Offset | Field |
|---|---|
0x00 |
Implementation |
0x04 |
Parameter count |
0x08 |
Name, such as CreateTimer |
0x0C |
Up to eight parameters of 12 bytes: a kind mask, an unidentified word, a label |
0x6C |
Description, such as Creates a timer to invoke a function |
0x70 |
Unknown. Set on seven commands |
A parameter's kind mask says what it accepts. The bits are named from the labels that carry them:
| Bit | Accepts |
|---|---|
0x80 |
A number: an ID, a count, seconds |
0x100 |
A speech or movie file name |
0x200 |
Text, or an animation name |
0x400 |
A ship |
0x800 |
A flight group or patrol route |
0x4000 |
A function, meaning a part |
0x80000 |
A named constant: a pilot, an AI mode, a text ID |
0x100000 |
Unknown; set in entity parameters alongside ship and flight group |
0x200000 |
A trigger condition |
0x400000 |
A camera or flight curve |
make vm-commands regenerates
src/engine/game/executor/commands.zig from the
binary alone. Every command call in the 44 missions resolves.
Parts¶
Section 8 holds one 28-byte descriptor per part, a named routine. The loader expands it into the 256-entry runtime part table.
| Offset | Size | Field |
|---|---|---|
0x00 |
2 | Name, as a string pool offset |
0x0A |
2 | Start, in halfwords from the start of the script; 0xFFFF for none |
0x0C |
1 | Flags; bit 0 runs the part when the mission starts |
0x0D |
1 | Argument count |
0x10 |
2 | Extent, in halfwords |
0x19 |
1 | Passed by the loader to the routine that fills the runtime entry |
The loader sets block = script + start * 2, which fixes the halfword unit. In every mission the
parts are in address order and contiguous, each one's start + extent being the next one's start,
and the last ends at the end of section 6.
Missions carry their authors' names for them, as in mission1:
# offset bytes args block name
0 1740 192 0 168 (F)launchfunction
1 1932 160 0 144 (F)Jumping to CONVOY
2 2092 252 0 220 (F)Arrival at CONVOY
...
31 7524 28 0 20 <F>Objective window
Nearly every part takes no arguments. Every mission has exactly one start part, with names such as
<F>Start Launch, (F)start or (F)setup: mission_script_start, 0x0045CBC0, runs it before
arming the triggers.
The other parts run when a trigger or another part calls them. To check a late moment of a mission,
openreliant --mission <number> --part <name> also runs a part, on a thread of its own as
part_run (0x0045BAA0) runs the start part, once the player's launch is over
(Configuration).
Blocks¶
A block is a u16 length, which counts its own two bytes, then instructions: the engine starts
a thread at block + 2 with its limit at block + length. The instructions end with a return,
padded with up to three bytes to a four-byte boundary.
Routines¶
The script section is a sequence of routines, each a block followed by its constant table:
dwords that push_constant n reads, starting at the block's end. First come the blocks triggers
run, then the parts:
- A part's extent covers its block and its constants.
- A trigger block's constants run to the next routine. In
mission1the trigger blocks start at bytes 0, 36, 72 and so on, and the first part follows the last of them.
In every mission this tiles the section exactly, with one trigger block for each trigger that can fire and has a link; a trigger that can fire may also have no link and run nothing. Each constant table is exactly as long as the highest index its block pushes, rounded up to 8 bytes; the filler dword that rounding adds is not read.
sltool dte script <mission> # every routine, with its constants
sltool dte script follows control flow from each block's entry rather than sweeping, because
jump, return and random_branch never fall through, and shows the value behind each
push_constant, the name of each command, and the part, ship or global an index names. Every
routine disassembles completely, but for the unused arm slots of the four random_branches. The
first block of mission1, which its first trigger runs, calls one of two parts depending on a
global:
0 to 36: trigger 0
2 22 01 call_part 1 (F)Jumping to CONVOY
4 21 17 command InterruptTriggerCode
6 27 00 push_global 0 (GV)convoykilled
8 28 00 push_constant = 1
10 02 equal
11 24 00 07 branch_if_zero_alt -> 19 if zero
14 22 15 call_part 21 (F)GO HOME (Total Loss)
16 42 00 04 jump -> 21
19 22 18 call_part 24 (F)Jumping to Sherman
21 21 17 command InterruptTriggerCode
23 32 01 push_byte 1
25 43 return
constants: 1 220332040
The links of the triggers no slice holds are never followed, and most of them do not point at a block.
Open¶
- What the qualifier byte selects.
Writing¶
dte/write.zig writes a mission file laid out as 36 of the
shipped missions are, mission1 among them: a directory of 128 slots, 0x400 bytes, each section
at the same offset with the same room up to the next, and 850,919 bytes in all. The directory's 28th
slot holds the file's size, and the rest are unused, as in those missions; every entry carries the
flags 15. Section 21 has no room there, starting where section 22 does, so OpenReliant's name goes
after the template's end, which a loose file's buffer of 0xFA000 bytes still holds.
A section's records are its count times its stride: bytes for the string pool, the script flags and OpenReliant's name, halfwords for the scripts, and the records' sizes for the rest. The strides of sections 9, 11 and 23, 4, 4 and 2 bytes, are Unverified: the engine reads nothing of 9 and 23, and 11 holds one record in every mission. Section 20's is not known, and no mission uses it.
sltool dte check <mission> writes a mission again and checks what comes back. The 36 missions of
the template, written again from their sections' whole rooms, stale bytes and all, come back byte for
byte. Every mission, written again from its records alone, reads back the same records.
A mission of OpenReliant's making holds what the template's missions hold: its section 24 is theirs
(write.template.command_flags), and its records carry the values most of theirs carry where their
fields are not known. Mission 0, the sandbox, is written so by the build
(mission0.zig).
Writing the script¶
dte/assemble.zig builds a routine as the shipped scripts
are built: the block's length word, which counts itself, the instructions, and zeros to a four-byte
boundary; then the constant table, each constant once in the order first pushed, and zeros to an
eight-byte boundary. Labels stand for the branches' targets: a branch's displacement counts from its
own position and a random_branch's targets from its opcode, both big-endian, and a branch
backwards is refused, since the engine would take it 64 KiB forward. push_string's length byte
counts itself and the NUL after the text.
Assembled again from its disassembly, every routine of the 44 missions comes back the same, but for
the padding after its last instruction, which in the shipped missions holds stale bytes, and the
three routines that hold unused arm slots of a random_branch.
Mission source groundwork¶
Mission Source v1 defines a YAML tool
input that compiles to a standard .DTE. The first implemented slice is source identity and
typed symbol resolution (#609).
dte/source.zig provides Symbols(Record), a table for one
collection of existing format records or source records. Each entry has an explicit source id
and its record. IDs match [A-Za-z_][A-Za-z0-9_-]* and are unique within that collection. They
are independent of display names, original object IDs and binary offsets.
Reference(Record) identifies the collection's record type. Resolution uses the selected table,
so equal IDs in different collections or script banks do not conflict. There is no separate list
of supported record kinds. Adding fields to a record or introducing another record type does
not require changing the symbol table. Binary width and sentinel checks belong to the compiler
that lowers resolved indices into the file.
Source IDs also do not depend on the base game's asset catalogue. A record can reference a numeric type introduced by a mod. Named content resolution in the future compiler must use the selected game's effective mod records and resources, including load order, rather than a fixed list of shipped types. Source identity and asset lookup are separate operations.
The table borrows immutable records and ID bytes. Rebuild it after changing IDs or collection order: a reference then resolves to the record's new index. Display names may repeat or change without affecting resolution. The caller selects the script bank explicitly.
Not implemented: YAML parsing, source compilation, export and preservation. These follow in separate slices under #608. The existing binary reader, writer and assembler remain the format implementation.
Prior art¶
The container, directory, record strides and condition list are from Starlancer-OSS
docs/dte-format.md
and its scripting reference, which build on Captain Foster's Starlancer ME work. Everything above
was re-checked against the 44 shipped missions and the engine's own code. Where the two differ,
this document follows the code: the trigger's condition is at 0x00 and its subject implicit,
sections 4, 12 and 13 hold flight groups, squads and squad members, a ship's 0x00 is its object
ID, 0x02 tests equality and 0x03 inequality, 0x28 pushes a constant, and 0x32 pushes a byte.
The byte-offset string pool, the object table and everything about the script beyond the dispatch
loop are additions. StarLancerEditor, which reads and
writes missions through YAML, names sections 14 and 15 the ships' formations and their points, which
the engine's formation code bears out, and its writer lays missions out on the same template.