Mission source¶
Mission source is a mission written as text. sltool dte decompile writes a mission's source from
its .DTE file, and sltool dte build compiles source into a standard .DTE that plays in the
original too (#1126). The syntax is a
subset of Luau, which Luau's own parser reads as the mission is built: nothing in the file runs, in
OpenReliant or anywhere else. The reference lists everything it
holds: the declarations and their fields, the script's functions, and the game's commands and
conditions. Writing missions is a guide for beginners.
-- mission1.dte as source.
reliant_group = flight_group("(FG)Reliant", { wing = "player" })
player_ship = ship("Player_Ship", {
type = "grendel",
group = reliant_group,
position = { -11608147, -3, 11596510 },
yaw = 90,
})
convoy_nav_point = nav_point("Convoy Nav Point", { position = { 1234884, 0, 11312658 }, yaw = 175 })
convoykilled = global("(GV)convoykilled", 0)
on(player_ship, "player_ready_to_jump", { repeats = "always" }, function()
jumping_to_convoy()
InterruptTriggerCode()
if convoykilled == 1 then
go_home_total_loss()
else
jumping_to_sherman()
end
InterruptTriggerCode()
return 1
end)
jumping_to_convoy = part("(F)Jumping to CONVOY", function()
-- ...
return 1
end)
The file¶
A mission's source is one file, named <mission>.mission.luau, such as mission1.mission.luau.
At its top level it holds declarations and nothing else:
name = constructor(...)declares a record, such as a ship or a part, undername.on(...)declares a trigger.mission(...)gives the mission's own details.
Comments go anywhere, and blank lines don't count. Every name is the file's: a declaration can use
any name the file declares, before or after it. A name is a Luau identifier,
[A-Za-z_][A-Za-z0-9_]*, that isn't a Luau keyword, a command's name or one of the names this page
gives the source itself.
The order of the declarations of a kind is the order of the mission's records of that kind, since the script and the records refer to records by their place: the first ship declared is ship 0, the first part part 0, and so on.
The mission¶
mission({ name = "Patrol" })
name is OpenReliant's name for the mission (OpenReliant's mission
name). A mission without the declaration has none.
Records¶
Each record has its display name, the name the original's tools and the game show, and fields in a table. A field left out takes its default.
A value the game names is written by the name the scripting API gives it
(Scripting reference): a ship type as "grendel", a pilot as
"bandit_tigers_leader", an order as "find_new_target", a condition as "shot_at". The names come
from the tables the game runs on, so mission source, mods' scripts and the editor share them. A
value the game doesn't name, such as a ship type or an order a mod adds, stays a number.
Globals¶
convoykilled = global("(GV)convoykilled", 0)
The display name and the value the global starts with.
Flight groups¶
reliant_group = flight_group("(FG)Reliant", { wing = "player" })
Its fields are in the reference (FlightGroupFields).
Ships¶
naginata = ship("(A1)Naginata", {
type = "naginata",
group = reliant_group,
pilot = "bandit_tigers_leader",
position = { -11616031, 187, 11597653 },
yaw = 90,
})
ship places a ship, a capital ship or a station. nav_point, waypoint, point and curve_point
place the mission's markers (Ships), which take the same fields but type, and a
point the place on a curve it marks. Their fields are in the reference
(ShipFields,
MarkerFields and
PointFields).
Squads¶
sabre_targets = squad("(AI)sabretargets", { sabre_1, sabre_2, component(kurgan, 3), escort_group })
The display name and the members in their order: ships, a ship's component as component(ship,
index), flight groups and other squads.
Formations¶
sabres = formation("sabres", { { 0, 0, 0 }, { -400, 0, 300 }, { 400, 0, 300 } })
The display name and the formation's points, each where it stands from the formation's origin. A
ship names a point as sabres[1] for the first.
Curves¶
camera_curve = curve({
start = convoy_nav_point, finish = escort_point,
from = { 1234884, 0, 11312658 }, to = { 1240000, 0, 11300000 },
leaving_handle = handle_1, arriving_handle = handle_2,
leaving = { 10, 0, 0 }, arriving = { 0, 0, -10 },
})
A curve has no display name. start and finish are the ships it runs between, from and to
their places, leaving_handle and arriving_handle the ships its tangents are drawn to, and
leaving and arriving the tangents (Curves). A ship left out is none.
Parts¶
launchfunction = part("(F)launchfunction", function()
-- ...
return 1
end, { start = true })
go_home = part("(F)GO HOME (Total Loss)", function()
-- ...
return 1
end)
unused = part("(F)Not written yet")
The display name and the part's code, a function whose parameters are the part's arguments. A part
without a function has no block. start = true runs it as the mission starts, before any trigger is
armed.
Triggers¶
on(black_guard, "shot_at", { repeats = "always", attacker = any_ship },
function(attacker, shield_damage, hull_damage, victim, weapon_fired)
if hull_damage >= 50 then
-- ...
end
return 1
end)
on takes the trigger's subject, the ship, flight group or squad whose events it watches, its
condition, its fields, which can be left out, and the code its events run. The function's
parameters are the values the condition's events carry, named after their labels in the condition
catalogue (Conditions). The fields are in the reference
(TriggerFields), and each condition with the values
its events carry (Conditions).
The triggers are in the order of their declarations, and a subject's triggers must follow one
another. A trigger whose subject is nil belongs to no object, so it never fires. A trigger without
a function runs nothing.
Triggers that share their code name it once:
convoy_hit = handler(function(attacker, shield_damage, hull_damage, victim, weapon_fired)
-- ...
return 1
end)
on(convoy_1, "shot_at", { repeats = "always" }, convoy_hit)
on(convoy_2, "shot_at", { repeats = "always" }, convoy_hit)
The script¶
The code of parts and triggers is the script's statements (Statements), in Luau's syntax:
| Statement | Source |
|---|---|
| A command | SetAI(sabre_targets, "find_new_target", 1, nil). A command the catalogue doesn't have is command(250, ...) |
| A part called | go_home(), with its arguments |
| A part started on a thread of its own | spawn(go_home), with its arguments after the part |
| An assignment | convoykilled = 1, convoykilled += 1, and -=, *= and /=. Floating-point stores are add_assign_f(target, value) and the like |
if |
if ... then ... elseif ... else ... end. An else with nothing in it is kept |
| A random choice | local roll = random(), then if roll < 50 then ... elseif roll < 100 then ... else ... end: one roll below 100, and the first arm whose threshold is above it, the else being the default. The roll can have any name; sltool dte decompile names a function's later rolls roll_2, roll_3 and so on |
return |
return 1. The code of each part, trigger and handler must end with one, as every routine of the shipped missions does: a thread would run on past the end of its block (Blocks) |
| Expression | Source |
|---|---|
| A number | 50. Where the parameter takes a named value, it is written by its name: "find_new_target" for an order, "shot_at" for a condition, "coalition_fighter" for a pilot, "talking" for a head movement, "ice_field" for an environment effect, and a part's name for a part. A value the game doesn't name stays a number |
| Text | "new_sim02.wav", as a Luau string |
| A ship, flight group, squad, curve or global | Its name |
| A ship's component | component(ship, 3) |
| None | nil |
| One of the game's variables | game.mission_over, by its name (The game's variables), or game[37] for one without |
| A part's argument | Its parameter |
| An event's value | Its parameter, in a trigger's function; thread_value(1) in a part, for the value of the event that started the thread |
| A value an object keeps of its last event | last_event(ship, "shot_at", "Hull Damage") |
| The last command's result | result() |
| A comparison | ==, ~=, <, <=, >, >=; in floating point greater_f(a, b) and the like |
and, or |
and, or |
| Arithmetic | +, -, *, /; in floating point add_f(a, b) and the like |
| Membership | in_flight_group(ship, group), not_in_flight_group, in_squad(object, squad), not_in_squad |
The script has no loops, since a branch only goes forward. A thread waits with commands such as
Wait, and runs again on a timer or a trigger.
What the source leaves out¶
The source holds what the game reads of a mission. It leaves out:
- what the game works out again as it binds the mission or starts its script: the ships' runtime places and angles, which equal the authored ones in every shipped mission, the objects' trigger slices, a flight group's ships, the parts' and triggers' places in the script, the string pool, and the directory;
- the objects' numbers. The shipped missions number their ships, flight groups and squads in the
order their authors made them, with gaps. The compiler numbers them itself: first the objects
that
last_eventnames, in the order the source names them, since the script names those by a byte, then the ships, the flight groups and the squads, each in order; - the script's flags, which the editor link sets (The editor link).
The rest is parked as unknown and unused: fields and sections that nothing in the game reads. The
compiler writes the fields as most of the shipped missions' records hold them (dte.Ship.blank and
the other records'), or 0 where they hold no value most, and leaves the sections empty, as mission 0
does. OpenReliant could keep data
of its own there, as it keeps the mission's name in section 21, since the original ignores them.
Unverified: that the original reads none of them; OpenReliant's engine, which follows the
original's disassembly, reads none.
| Where | What |
|---|---|
| Sections 1, 22 and 26 | The operand tables. mission_build_part_tables (0x00452F50) hands an entry of one, by the part's kind, to mission_fill_part (0x00452FD0), which pops it unread |
| Sections 9, 11 and 23 | Unread (Directory) |
A ship's bytes 0x06, 0x16, 0x1A, 0x2A, 0x37, 0x3C, 0x3E and 0x42 |
Unread |
A trigger's bytes 0x04 to 0x13, 0x17, 0x18 and 0x1B, and its armed flag |
Unread; the game arms every trigger an object's slice holds as the mission starts |
A trigger's firings left, but for { counted = n } |
Read only for a counted trigger |
A part's bytes but its name, flags and argument count, and its kind |
Unread, but for the operand tables above |
| A curve's last word, a squad member's last bytes, the globals' and the flight groups' unused words | Unread |
| The objects' tool flags | For the original's tools (Objects) |
| A random choice's arms' last byte | Unread |
References past the records¶
A reference to a record that the mission doesn't have, such as a curve's ship in some of the
leftover missions, is written by its index: ship_index(59031), and likewise flight_group_index,
squad_index, curve_index, global_index, part_index, object_index, formation_index and
formation_point_index. object_index names an object by its number in the built mission, and a
number past its records gets an entry of no record in the object table.
Building a mission¶
sltool dte decompile mission1.dte > mission1.mission.luau
sltool dte build mission1.mission.luau mission1.dte
sltool dte build compiles the source with
compile.zig. It stops at the first error, which it
gives with its line and column, such as patrol.mission.luau:12:5: `nobody` is not declared. Each
kind's records are written in the order of their declarations. The script holds the blocks the
triggers run, in the order the triggers first run them, then the parts' blocks in order, each
written as the developers' compiler wrote its statements (Statements).
For a debugger, compileMapped also gives a map of the script's statements. For each statement, it
gives the offset in the script of the instruction the statement ends with, as the editor link gives
a stop, the line and column of its source, and whether the script stops before it. The script stops
before an assignment, a command, a call, a spawn and an if, but not before a return or a random
choice (Stopping and stepping).
source_parser.buildMapped parses source and compiles it with the map.
OpenReliant's own Mission 0, the sandbox, is mission source too:
mission0.mission.luau, which the build compiles
with the same compiler, built for the host (build_mission.zig).
Editors¶
luau-lsp, the Luau language server, gives editors such
as VS Code completion and descriptions for mission source, from the definitions file
mission.d.luau, which comes with each release and which
sltool dte definitions writes. Writing missions says how to set
it up.
The definitions take the declarations, the script's functions and the tables of fields from
language.zig, which the compiler checks fields
against too. The commands, the conditions and the names of values come from the game's tables.
A type of names of values takes the name scripts know it by, such as ShipType or FaceFilm
(vocabulary.zig), and accepts text where mods add
values under names of their own, as the scripting API's type does. The definitions name a mission's
ship MissionShip, apart from the scripting API's Ship, a ship type's record. So both files can
be loaded together, and a test checks that the names they share are declared the same way.
on has one signature for every condition: its code's parameters are any, and its table has a
field for every value that a trigger can check, whatever the condition
(TriggerFields). Luau can't type them by the
trigger's condition:
- It only types a function's parameters from a single signature without generics, as with the
hooks'
ein scripts (The reference, luau#3045, luau#2373). One overload ofonper condition would also be far past the eight overloads that the new type solver accepts (luau#1853). - Looking the fields up by the condition's name (
index<Fields, C>) does complete the right fields, but strict mode then rejects every trigger, valid ones included (luau#2215), and the old solver can't load the file at all.
The reference lists each condition's values (Conditions).
Reading the text¶
Luau's own parser reads mission source, so the syntax is exactly Luau's, comments included.
src/source_parser.zig calls the parser through a C++ shim, since
it has no C API, and copies its syntax tree into the library's
(syntax.zig): each statement and expression with the
line and column it starts at, and each error the parser finds with its place. Only the tools that
read source link the parser. sltool does; the library the tools share doesn't.
Mission source uses a subset of Luau. The parser gives back the rest, such as a loop, a function
declared by name, a method call, an if expression or a string with values put into it, as an
unsupported node with its place, for the compiler to refuse. An expression in brackets, or with a
type assertion such as x :: number, is the expression itself, type annotations on locals and
parameters change nothing, and a string in backquotes without values is text like any other.
The round trip¶
sltool dte decompile names each record after its display name: the name in lower case, a leading
tag such as (F) or (GV) left out, each run of other characters an underscore, and an underscore
where a small letter meets a capital, with a number after it where two would clash. The name is the
record's from then on, and renaming the display name doesn't change it. A squad's members are
written as the game walks them, from the squad's first member while they are its own, and a
formation's points as OpenReliant finds them, from the formation's first point on, those that name
it (dte.FormationPoint.Walk). A place or another floating-point value is written as the shortest
number that reads back as the same 32-bit float, with an exponent from 2^53 on, such as
-1.701416e38: Luau warns about a whole number that large written in digits, since a double can't
hold every one exactly.
Built again, a decompiled mission is the same mission
(same.zig): the same records and code, decoded, with names
compared as text, an object by the record the game takes it for, and a formation's point by its
place among the formation's. The string pool's layout, its stale strings, the objects' numbers, the
sections' layout and the parked bytes may differ. Every shipped mission comes back as the same
mission. To keep a shipped mission byte for byte, edit its .DTE in place instead.
sltool dte check runs the round trip: it writes the mission as source, parses it, fails on an
error or a construct mission source doesn't use, builds the source again, compares the two missions
and binds the new one as starting a mission does.