Mods can include scripts, written in Luau, a version of Lua 5.1:
- Load scripts change the game's records, such as a gun's damage, as OpenReliant starts.
- Global and mission scripts run as the game plays. They react to what happens and change it, through hooks on the game's functions and events.
- Object scripts run on the ships and other objects of a mission, each on its own object; missile scripts on each missile in flight; and turret scripts on each turret.
- Player and menu scripts decide what the player sees, hears and does. They draw over the flight display and the menus, react to the keys, and add camera views, game modes and shader effects.
This page explains how to write them. The scripting reference lists everything
they can use, What mods can do shows each feature with an example, and
examples/mods holds example mods for mod makers, which show how the
scripting works and aren't supported mods.
New to scripting? Start with a first script, then read Kinds of scripts and the example closest to what you want to do. Luau's website explains the language itself. The sections after that can be read in any order, as you need them.
Improvement: the original has no scripting apart from its mission scripts.
- A first script
- Kinds of scripts
- Packages and values
- Engine handlers
- Hooks
- Objects and object scripts
- Orders
- Events and interfaces
- The records
- Saved games, storage and timers
- Player and menu scripts
- Menus, game modes and campaigns and options
- Post effects, surface and lighting functions and replacing OpenReliant's shaders
- Files
- The console and editors
- Limits and when something goes wrong
- Compatibility: what stays stable from one version to the next
Scripts live in a mod (Modding); a folder mod is the easiest to work on. This one
makes the player's ship tougher. Make the folder mods/tough in the game folder, with this
manifest, mod.ini:
[Mod]
Name=Tough
OpenReliant=0.7
[Scripts]
Global=tough.luauand this script, tough.luau:
local hooks = require("openreliant.hooks")
-- Every hit on the player's ship lands at half strength.
hooks.after("damage_by_difficulty", function(e)
if e.object.is_player then
e.result *= 0.5
end
end)Start OpenReliant from a terminal and fly a mission (--mission 0 flies the sandbox at once). The
log in the terminal shows info(scripts): tough: started tough.luau, and the player's ship takes
half the damage.
print output and script errors go to the log, after the mod's name. In the developer mode, they
go to the console too, and a folder mod's scripts reload as soon as one is saved, carrying on from
where they were (The console). Otherwise, go back to the main menu and start a game
again. After changing mod.ini, adding a file or changing a load script, restart OpenReliant.
Each example mod shows one part of the scripting, with comments in its files:
| Example | What it shows |
|---|---|
arena |
A game mode with its own rules, a HUD display fed from storage, and the radio_say hook |
balance |
The records, changed from a load script |
bananas |
A mod's own gun, missile, pilot and ship type, tuned in the records, and a pilot set on enemy ships |
campaign |
A campaign with a briefing screen, a movie and an ending screen |
cel-shading |
Surface and lighting functions |
crt |
A post effect |
custom-order |
A custom AI order started by a mod action |
drawing-assets |
A mod's pictures, the game's shapes and fonts |
hud-layout |
A HUD display that stands in for the game's radar, moves and scales other instruments, and reads what they show |
dvd |
A menu script that draws over the menus |
interceptor |
A mod's ship type, flown in a game mode |
main-menu |
Replacing a screen of the front end |
rules |
Hooks on the game's functions |
strafe-run |
A custom order, a HUD display, a camera view, a screen and actions, through the built-in interfaces |
tally |
Saved games, storage and timers |
teapot |
A mod's ship type with its own model, and the radio_say hook |
trent |
The game's text in the records, changed from a load script, beside face films that replace the game's |
wingmen |
Object scripts, events, interfaces, nearby and an options page |
| Kind | In mod.ini |
When it runs | What it's for |
|---|---|---|---|
| Load | Load= under [Scripts] |
Once, when OpenReliant starts, before the main menu | Changing the records, declaring options and game modes |
| Global | Global= under [Scripts] |
For the whole game | Hooks, orders and the mission's objects |
| Mission | The mission's file name under [Missions] |
While that mission runs | The same as global scripts, for one mission |
| Object | A class, such as Fighter=, or a type, such as Type.predator=, under [Scripts] |
On each object of that class or type, while it's in the mission | Object scripts |
| Missile | Missile= under [Scripts] |
On each missile, from its launch to the end of its flight | Missile scripts |
| Turret | Turret= under [Scripts] |
On each turret of each object, while the object is in the mission | Turret scripts |
| Player | Player= under [Scripts] |
For the whole game, even while it's paused | What the player sees and does in flight |
| Menu | Menu= under [Scripts] |
From OpenReliant's start until it quits: in the menus, the rooms, the movies and the loading screens, and over the missions | Drawing over the menus, replacing screens, actions, options and game modes |
[Scripts]
Load=balance.luau
Global=rules.luau, wingmen.luau
Fighter=wingman.luau
Type.reliant=reliant.luau
[Missions]
mission2.dte=escort.luau- A key can list several scripts, separated by commas. They run in that order, after the scripts of the mods before (Load order).
- A game starts when you start a campaign, load a saved game, or fly a mission on its own (INSTANT
ACTION, the simulator, a game mode or
--mission), and ends at the main menu. Global scripts start again with each game. For a loaded game, they start from the state they saved with it (Saved games). [Missions]matches the mission's file name in any case:mission2.dteis mission 2, andmission251.dtethe second part of mission 25. A mission script starts as its mission begins, before its ships appear, and stops as it ends. Each attempt starts it again. Mission scripts are global scripts in everything else.- The classes are
Fighter,Capital,Support,Torpedo,Mine,Planet,DebrisandOther. A type isType.and its name (ShipType) or its number, such asType.12. A ship type a mod adds is named by its qualified name,Type.teapot:teapot, or in the mod that adds it by its own name,Type.teapot. Its number changes with the mods that are on, so don't name it by number. - Each script has its own global variables, and the scripts on each object have their own.
Some functions only work in some kinds of script:
| Function | Scripts |
|---|---|
core.register_game_mode, options.register_page |
Load and menu, as OpenReliant starts |
input.register_action, ui.replace_screen, ui.go_to and the other front end functions |
Menu |
ui.register_screen |
Player and menu |
camera.register_view, hud.register_display, postprocessing, shaders, debug |
Player |
orders.register, object:add_script, object:remove_script |
Global and mission |
| Changing records | Load |
Packages lists which scripts can use each package.
Network games come after 1.0 (#55). The scripts are already split the way they will run in them, so a mod written now works there too if it keeps to these rules:
- Global, mission, object, missile and turret scripts run where the game is hosted, and in a network game only there.
- Load scripts run on every machine, and must give the same records on each one with the same mods.
So they mustn't depend on the time, on storage, or on anything else that differs between
machines. Their
math.randomhas a fixed seed, so it gives the same numbers everywhere. A load script that changes the records by one of its mod's options gives different records where the options differ, and a network game will need them to match. - Player and menu scripts run on each player's machine. They read the game, and change it only by
sending events to the global scripts (
core.send_global_event), whose data is copied as it would be sent to another machine. They can't change objects or records. Menu scripts drive the menus of the machine they run on. - Interfaces and timers stay within one side: the game's (load, global, mission, object, missile and turret scripts) or the presentation's (player and menu scripts).
- Storage is each machine's own, so keep game rules out of it. So are a mod's options: the global scripts read the host's.
require("name") runs another script of the same mod once and returns what it returns. The name is
the file name, with or without .luau, in any case. require("openreliant.<name>") gives one of
OpenReliant's packages, such as openreliant.hooks or openreliant.world.
Packages lists them, with what each holds and the kinds of script that can
use it. require("openreliant.core").version is OpenReliant's version, such as "0.7.0", for a mod
that needs to know which features it has.
- Numbers use the game's units (developer documentation). Positions and velocities are Luau vectors, and a velocity is the distance moved in a simulation step, of which there are 25 a second.
- Values OpenReliant has names for are strings, such as
"fighter"or"laser_cannon". One without a name is a number (Names of values). math.randomgives the same numbers on every computer for the same game: it is seeded again as each mission starts, from the game's random numbers.math.randomseedandosaren't there.
What a mod registers or adds gets a qualified name: the mod's folder name, or its archive's name
without .hog, a colon, and the name the mod gave it. A gun banana_gun in the mod folder
bananas, or in bananas.hog, is bananas:banana_gun, and the camera view chase of the mod
strafe-run is strafe-run:chase. A mod's names stay the same whether it's packed or not.
- Two mods can use the same name for their own things; the qualified names keep them apart.
- The functions that register something return its qualified name.
- The functions that take something registered by its name, such as
camera.set_view,hud.set_display_enabledororders.info, take the qualified name, and within the mod also the name without the prefix. The game's own names come first, so a mod's view calledcockpitis namedstrafe-run:cockpiteven within the mod. - The records, and values such as an object's
type, take what a mod adds by its qualified name. - Keep names, not numbers. The numbers OpenReliant gives what mods add change with the mods that are on.
A script returns a table, whose engine_handlers holds the functions OpenReliant calls, such as
on_update each frame, or on_mission_start. Each one is optional:
return {
engine_handlers = {
on_mission_start = function(mission)
print("mission " .. mission.number .. " starts")
end,
on_update = function(seconds) end,
},
}| Handler | Scripts | When |
|---|---|---|
on_init(data) |
Global, object, player, menu | The script starts |
on_save(), on_load(saved) |
Global, player | The game is saved, or loaded (Saved games) |
on_records_loaded() |
Load | Every mod's load scripts have run |
on_update(seconds) |
Global, object | Each frame in which game time passes |
on_step() |
Global, object | Each simulation step, 25 a second |
on_frame(seconds) |
Player, menu | Each frame drawn, even while paused |
on_mission_start(mission), on_mission_end(outcome) |
Global, player, menu | A mission starts or ends |
on_object_added(object), on_object_removed(object) |
Global | An object joins or leaves the mission |
on_added(), on_removed() |
Object | The script's object joins or leaves the mission |
on_key_press(key), on_key_release(key), on_action(action) |
Player, menu | Keys and actions |
on_console_command(text) |
Player, menu | A line typed in the console that isn't one of its commands |
on_window_resized(width, height) |
Player, menu | The window changes size |
on_interface_override(base) |
Global, object, player, menu | Interfaces |
on_option_changed(key, value) |
Menu | The player sets one of the mod's options |
Engine handlers says what each one gets. They're called in the order the scripts started: the global and mission scripts' first, in load order, then each object's. A handler that raises an error is logged and isn't called again, and so is one that runs out of time three times in a row (Limits).
on_mission_end gets the mission's outcome: its ending for the player, such as "destroyed" or
"left" (Ending), and the rating the mission's script gave it, such as
"success" (Rating).
Global, mission and object scripts add handlers to hooks with require("openreliant.hooks"). A hook
is one of:
- a function of the original game, by its name, such as
object_damageorbullet_fire. The developer documentation describes them. - a function of OpenReliant's own: a step that the original takes inside a larger function,
such as
mission_lost, which OpenReliant makes a function of so that scripts can hook it. - an order routine: what a ship does each frame for an order, such as
order_fight. - a mission event, which a mission's triggers can wait for, such as
destroyedorlaunched. - an engine event, such as
mission_startedorobject_added.
The scripting reference, or openreliant hooks in a terminal,
lists them with the fields each handler sees.
local hooks = require("openreliant.hooks")
-- Each enemy the player destroys makes the player's shots hit 10% harder.
local bonus = 1
hooks.add("destroyed", function(e)
local by_player = e.attacker and e.attacker.is_player
if e.component == nil and by_player and e.object.side == "hostile" then
bonus += 0.1
end
end)
hooks.add("object_damage", function(e)
if e.kind == "bullet" and e.attacker.is_player then
e.value *= bonus
end
end)A handler gets one value, e, with the hook's fields. e can only be used while the handler runs.
examples/mods/rules has more.
For a function, e holds its arguments, and changing a field changes what it does. A handler that
returns false stops the call: the function doesn't run, and neither do the handlers after it.
-- Every hit does twice the damage, but collisions don't damage the player's ship at all.
hooks.add("object_damage", function(e)
if e.object.is_player and e.kind == "collision" then
return false
end
e.value *= 2
end)hooks.after adds a handler that runs after the function. For a function with a result, it sees
the result in e.result and can change it. A handler added with hooks.add that stops the call can
set e.result too, which is then the function's result.
-- Hits on the player's ship land softer.
hooks.after("damage_by_difficulty", function(e)
if e.object.is_player then
e.result *= 0.75
end
end)e:original() runs the rest of the call at once (the handlers after this one, then the function,
with the values in e) and returns the function's result, so a handler can do something both
before and after it. The function runs only once: where a handler after this one calls
e:original() too, the function runs there, and the earlier call returns its result. If a handler
after this one stops the call before the function runs, e:original() returns nil.
The game's functions that have hooks:
| Hook | What it is | Filter tests |
|---|---|---|
object_damage |
Damage to an object's shield, and what gets through to its armour | object |
object_armor_damage |
Damage to an object's armour, once its shield is down | object |
component_damage |
Damage to a component, such as a capital ship's turret | object |
damage_by_difficulty |
How hard damage lands at the game's difficulty; its result is the damage | object |
object_destroyed |
An object's pilot ejects, or it explodes | object |
bullet_fire |
A ship fires a shot | owner |
missile_launch |
A ship launches a missile at a target | launcher |
missile_launch_turret |
A missile turret launches a Screamer at a target | launcher |
order_push |
An object is given an order, aimed at a target; its result says whether it took | object |
order_pop |
An object ends the order it runs; its result says whether it had one | object |
object_orders |
An object runs its order, as it does each frame | object |
order_retaliate |
A fighter turns on whoever last hit it | object |
fight_choose_maneuver |
A ship under the Fight order chooses its next combat maneuver; its result is the maneuver | object |
maneuver_run |
A ship under the Fight order flies its combat maneuver for a frame | object |
radio_say |
The radio says a line | Only a function filter |
vm_command |
The mission's script runs one of its commands; its result is what the command gives | Only a function filter |
restart_screen |
The restart screen after a mission is lost or left; its result is the player's choice | Only a function filter |
OpenReliant's functions that have hooks choose the movies that play as a mission ends. Each takes
the movie the game would play as e.movie, which a handler can change to the name of another Bink
file, the game's or a mod's, or set to nil for none. They run once the mission is over, so only the
handlers of global scripts see them, as restart_screen's do:
| Hook | What it is |
|---|---|
mission_lost |
A mission is lost or left, before the restart screen: the campaign's funeral, capture or execution, and nothing for a game mode's mission |
career_over |
The pilot's career in the campaign ends, before the main menu |
medal_ceremony |
A mission of the campaign awards a medal, and its ceremony plays |
local hooks = require("openreliant.hooks")
-- A funeral of the mod's own when the player's ship is destroyed.
hooks.add("mission_lost", function(e)
if e.ending == "destroyed" then
e.movie = "my_funeral.bik"
end
end)
-- No restart screen: a lost mission is flown again from its launch at once.
hooks.add("restart_screen", function(e)
e.result = "replay_from_launch"
return false
end)An order's or a missile's target is a table (Target): object and
component for a ship, whole where component is nil, or the index of one of the mission's
flight_groups or squads, and every field nil for nothing. The table can't be changed in place;
a handler sets the field to a new one.
local hooks = require("openreliant.hooks")
-- Missiles fired at the player's ship are aimed at nothing instead.
hooks.add("missile_launch", function(e)
if e.target.object and e.target.object.is_player then
e.target = {}
end
end)
-- No ship is told to run away.
hooks.add("order_push", function(e)
if e.order == "run_away" then
return false
end
end)order_push's result is "taken", "refused" or "conflict" (OrderPushed).
A handler that stops it leaves "refused", and the object's orders stay as they were.
radio_say runs as the radio says a line: from a mission's script, the simulator, the game's
chatter or a mod's script (The radio). e.speech is the file of the line, such as ms_hudtr_001.ut, and e.film the film of
the speaker's face. e.mode says when it's said: "now", "queued" after the lines before it, or
"if_idle", only if the radio has nothing else to say.
- Changing
speechorfilmsays another line, or shows another face. - Returning
falsedrops the line. Whatever waits for it, such as a mission script that waits for each line to end, carries on at once.
local core = require("openreliant.core")
local hooks = require("openreliant.hooks")
-- The simulator's talk about the display (ms_hudtr_001.ut and the lines after it) is skipped in
-- this mod's game mode.
hooks.add("radio_say", function(e)
if core.game_mode == "teapot:arena" and e.speech:find("ms_hudtr", 1, true) then
return false
end
end)examples/mods/arena and examples/mods/teapot
do this.
vm_command runs as a mission's script runs one of its commands. e.command names it, such as
"set_invulnerability" or "play_music" (MissionCommand), and
e.arguments holds its arguments, the first first, with 0 past the ones it takes.
- Returning
falsestops the command, and the script goes on after it. - The arguments are the script's own values. A ship or a text is the offset of its record in the
mission's file, so most handlers only look at
e.command, or change a number such as a time. The list can't be changed in place; a handler setse.argumentsto a new one. e.resultis what the command gives:"run_on","wait", which ends the script's run until it runs next, or a number, the command's value (MissionCommandResult).- A handler that stops a command and sets
e.result = "hold"holds it: the script's thread waits, and runs the command again, on the same arguments and through the handlers, the next time it runs. A mission's own waiting commands, such as WaitForSpeech, wait this way.
local hooks = require("openreliant.hooks")
-- The missions' scripts can't make ships invulnerable.
hooks.add("vm_command", function(e)
if e.command == "set_invulnerability" then
return false
end
end)Each order a ship follows runs routines of the original game: an init as the order starts, an
update each frame, and for a few an exit as it ends. Each routine is a hook, named after the order:
order_fight_init, order_fight and so on (The order routines).
Their handlers see the ship as e.object, and e.object.order is the order.
-- Hostile fighters never run away: their Run Away routine does nothing.
hooks.add("order_run_away", function(e)
return false
end, { side = "hostile", class = "fighter" })- Returning
falsefrom an update skips the routine for that frame. The ship keeps the order. - Launching, landing, docking, jumps, gates and explosions are all orders, so their routines'
hooks change them, such as
order_jump_out_initororder_explode.order_pushcan refuse the order before it starts. - Where OpenReliant doesn't run a routine yet, its handlers still run.
- These hooks change the game's orders. To add an order of your own, see Custom AI orders.
An event's fields can only be read, and a handler returning false only stops the handlers after
it. Events have no hooks.after.
- Mission events (The mission's events), such as
destroyed,launchedanddocked, come for the ships the mission's file lists, whether or not a trigger waits for them. The exceptions areproximity_closeandproximity_general, which tell of a ship close to another: the game looks for them once a second, and only while one of the ship's triggers waits for them. - Engine events (The engine's events) are
mission_started,mission_ended,object_added,object_removed,order_started,order_endedandtrigger_fired, the last for each of the mission's triggers that fires.
A third argument limits a handler to the objects it's for. The engine checks it without calling the handler, so the game doesn't slow down for the rest:
-- The Reliant and the Yamato take half the damage.
hooks.add("object_damage", function(e)
e.value *= 0.5
end, { type = { "reliant", "yamato" } })The filter takes object (a handle), type, class and side, each a name or a list of names;
every test given must hold. It tests the hook's main object, which the table above names: object
for most hooks. A filter can also be a function, which gets e and returns true for a call the
handler is for. radio_say and vm_command concern no object, so they only take a function.
- Handlers run newest mod first (the mod that loads last), and within a mod in the order they were
added. So a later mod's handler that returns
falsestops an earlier mod's. hooks.addandhooks.afterreturn a handle, andhandle:remove()removes the handler.- A handler that raises an error is logged with its file and line, and removed, and the changes it
made to
eare undone. One that runs out of time is removed once it has three times in a row, since a call can be slow once for reasons that aren't its own.
Scripts see objects (ships, stations, nav points) as handles, such as e.object. Every script can
read their fields, such as type, side, position or hull. Objects
lists the fields and the methods.
-- In a global script: every hostile fighter turns on the player.
local world = require("openreliant.world")
for _, object in world.objects() do
if object.class == "fighter" and object.side == "hostile" then
object:give_order("fight", world.player)
end
end- The same object always gives the same handle, so
==compares objects, and handles work as table keys. - A handle is valid until its object leaves the mission or the mission ends. Reading a field of one
that isn't valid is an error.
object:is_valid()tells which. openreliant.worldgives global scriptsworld.objects(), the missiles in flight asworld.missiles(), the player's ship asworld.player, and the mission asworld.mission, with itsnumberand itsfile's name.typeis a name such as"predator", or the qualified name of a mod's type, such as"teapot:teapot".shieldsandarmorgive each quadrant:left,right,foreandaft.hullis the share of armour left in the weakest quadrant, from 1 down to 0.
Global scripts can change many of an object's fields on any object, and an object script can change them on its own object. The reference marks each with Changes:
throttle: 1 is full, 2 the afterburner and -1 reverse thrust.roll_input,pitch_inputandyaw_input: how hard it turns, from -1 to 1.pilot: the pilot who flies it, whose record sets how it flies and fights. A pilot of the game's by number, a mod's pilot by its qualified name, or"none".position,orientationandvelocity: setting the first two moves or turns it at once, and setting its velocity pushes it.side,shields,armor,afterburner_fuelandcountermeasures. Shields and armour go from 0 up to what a whole ship of the type has.- What a mission's commands set:
invulnerable,cloaked,targetable,lights,disabled,guns_disabled,missiles_disabled,engines_disabled,eject_disabled,do_not_disturbandavoidance_disabled.
An order usually sets the throttle and the turning each frame, so a change to those lasts until the order sets them again. To change what a ship does, give it an order (Orders).
-- In a global script: a mod's pilot flies every enemy fighter.
local world = require("openreliant.world")
for _, object in world.objects() do
if object.class == "fighter" and object.side == "hostile" then
object.pilot = "bananas:trooper"
end
endexamples/mods/bananas does this every half second in its game
mode, so that fighters that join the mission later get the pilot too.
orientation is where an object's axes point: right, down and forward, in the game's frame,
where Y points down. openreliant.util turns points between the world and an object's own frame,
and works with orientations and angles:
local util = require("openreliant.util")
local world = require("openreliant.world")
-- How far off the player's nose its last attacker is, in degrees, and whether it's above.
local player = world.player
local attacker = player.last_attacker
if attacker then
local off = math.deg(util.angle_off(player.position, player.orientation, attacker.position))
local above = util.to_local(player.position, player.orientation, attacker.position).y < 0
end
-- A point 100 units ahead of the player, and an orientation that looks at the attacker.
local ahead = util.to_world(player.position, player.orientation, vector.create(0, 0, 100))
local facing = util.look_at(attacker.position - player.position)
-- The same orientation turned 10 degrees to the right, about its own Y axis.
local right = util.turn(facing, "y", math.rad(10))util.angles and util.from_angles turn an orientation into pitch, yaw and roll and back, and
util.normalize_angle brings an angle within half a turn either way.
object:parts() lists the parts of an object's model, destroyed ones included, in the order the
game numbers them: each part, followed by the parts of the models it carries, such as a turret's
gun or a missile on its rail. object:attachments() lists the attachment points on those parts,
such as engine glows, gun muzzles and missile hardpoints (ShipPart,
ShipAttachment). Every script can read them.
-- Whether a ship can still fly: it has no engines, or one that isn't destroyed.
local function can_fly(ship)
local engines, working = 0, 0
for _, part in ship:parts() do
if part.class == "engine" and not part.damaged then
engines += 1
if not part.destroyed then
working += 1
end
end
end
return engines == 0 or working > 0
endclasssays what a part is, such as"hull","engine"or"shield_generator"(PartClass). Most parts, such as a hull's plating, have none.parentis the index in the list of the part it hangs from, and an attachment'spartthe index of the part that carries it.- Components are the parts a ship lists by number, such as a capital ship's engines, shield
generators and turrets. Missions, the
destroyedevent andobject:give_ordername them by that number, from 0.componentgives it, and a component keeps it once it's destroyed.armorandfull_armorare what a component has left and what it starts with, andinvulnerablewhat a mission's SetInvulnerability gave it:"full"keeps off every hit. damagedmarks the parts of a component's damaged model, which stay hidden until the component is destroyed.positionis the current position of a part or an attachment in the world, and nil once its part is destroyed.- Each call gives a new list, which doesn't change as the ship does.
Global scripts can destroy any object, and an object script its own.
object:destroy_component(component) destroys a component the same way a hit that takes its last
armour does: it blows up, takes the rest of its assembly with it, such as a turret's barrels, shows
its damaged model and sends its destroyed event. Losing a hull part ends the ship.
object:destroy() destroys a whole object the same way running out of armour does: a ship that
lists components loses its hull components, and any other ship explodes, unless its pilot ejects
first. Both return false when there's nothing to destroy, such as for a nav point, a component
that's already destroyed, or a ship that is already exploding or jumping.
-- Every shield generator the convoy has left goes down at once.
for _, part in convoy:parts() do
if part.class == "shield_generator" and part.component and not part.destroyed then
convoy:destroy_component(part.component)
end
endworld.set_player_target(target, component) makes a ship, or its component number component,
the player's target, as a mission's SetPlayerTarget does. It returns false for a target the player
can't aim at, such as a destroyed component or a ship that can't be targeted.
world.set_primary_target(target, component) makes it the mission's primary target, as
SetPrimaryTarget does. The PRIMARY TARGET key selects the primary target. Nil leaves the mission
without one.
local hooks = require("openreliant.hooks")
local world = require("openreliant.world")
-- As each shield generator goes down, the nearest one left that can be hit becomes the player's
-- target.
local function nearest_generator()
local nearest, ship, distance = nil, nil, math.huge
for _, object in world.objects() do
if object.side == "hostile" then
for _, part in object:parts() do
if part.class == "shield_generator" and part.component and not part.destroyed
and part.invulnerable ~= "full" then
local away = vector.magnitude(part.position - world.player.position)
if away < distance then
nearest, ship, distance = part, object, away
end
end
end
end
end
return ship, nearest
end
local function class_of(ship, component)
for _, part in ship:parts() do
if part.component == component then
return part.class
end
end
end
hooks.add("destroyed", function(e)
if e.component == nil or class_of(e.object, e.component) ~= "shield_generator" then
return
end
local ship, generator = nearest_generator()
if ship then
world.set_player_target(ship, generator.component)
world.set_primary_target(ship, generator.component)
end
end)An object script runs on one object, from when the object is added to the mission until it
leaves, or the mission ends. The manifest starts it on every object of a class or a type, and a
global script starts it on one object with object:add_script(name, data), which passes data to
its on_init. object:remove_script(name) stops it. require("openreliant.self") gives the
script its own object.
-- wingman.luau, listed as Fighter=wingman.luau: a badly damaged wingman runs from its attacker.
local self = require("openreliant.self")
return {
engine_handlers = {
on_update = function()
if self.is_player or self.side ~= "friendly" or self.order == "run_away" then return end
if self.hull < 0.3 and self.last_attacker then
self:give_order("run_away", self.last_attacker)
end
end,
},
}- The script gets
on_initand thenon_addedas it starts, andon_removedas its object leaves the mission. As a mission ends, its objects' scripts stop withouton_removed. - Each object's scripts have their own globals, so the same script on two ships keeps two sets of variables.
self:hook(name, handler)adds a handler for the calls that concern the object only, likehooks.addwith the object as the filter.require("openreliant.nearby").objects(radius)gives the objects withinradiusof the script's object, nearest first.
A global script can start an object script on any object it chooses, such as each ship of a type a mod adds:
return {
engine_handlers = {
on_object_added = function(object)
if object.type == "teapot:teapot" then
object:add_script("teapot_ship.luau")
end
end,
},
}A missile script runs on one missile, from its launch until its flight ends, as it strikes
something, runs out of time or is set off, or until the mission ends. Missile= starts it on every
missile, and require("openreliant.self") gives it its missile, a handle
(Missiles) with the missile's type, launcher, target, position and
velocity.
-- Raptors fired at the player's ship turn to the nearest other ship of the player's side, if any.
local self = require("openreliant.self")
local nearby = require("openreliant.nearby")
return {
engine_handlers = {
on_added = function()
local player = self.target.object
if self.type ~= "raptor" or not (player and player.is_player) then return end
for _, other in nearby.objects(20000) do
if other.side == player.side and not other.is_player then
self.target = { object = other }
return
end
end
end,
},
}- The script gets
on_initandon_addedas the missile is launched, its target set, andon_removedas its flight ends. As a mission ends, its missiles' scripts stop withouton_removed. - Each missile's scripts have their own globals, as each object's have.
self.targetcan be set to aim the missile elsewhere,position,orientationandvelocityto move, turn or push it, andself:detonate()ends its flight at once. Global scripts can do all of these to any missile.nearby.objects(radius)gives the objects around the missile, nearest first.- Global scripts hear of each missile with the events
missile_addedandmissile_removed.
A turret is one of a ship's guns that turns to aim ("aimed"), spins its barrels while the ship
fires ("spinning"), or launches missiles ("launcher"). A turret script runs on one turret while
its ship is in the mission: Turret= starts it on every turret of every ship as the ship is added,
after the ship's own scripts. require("openreliant.self") gives it its turret, a handle
(Turrets) with its object, its kind, its gun_type, its position and
its target.
-- Missile turrets on the Coalition's capital ships hold their fire for the player's ship.
local self = require("openreliant.self")
return {
engine_handlers = {
on_update = function()
if self.kind ~= "launcher" or self.object.side ~= "hostile" then return end
local aimed = self.target and self.target.object
if aimed and aimed.is_player then self.target = nil end
end,
},
}- The script gets
on_initandon_addedas its ship is added, andon_removedas the ship leaves the mission. A turret destroyed with its base stays:self.destroyedbecomes true, and it turns and fires no more. - Each turret's scripts have their own globals.
- Setting
self.targetaims an aimed turret or a missile turret; nil leaves it to look for a target of its own. A spinning gun fires where its ship points, so it has no target. object:turrets()gives global scripts the turrets of any object.
A ship does what its orders say. object:give_order(order, target) gives it one, by the order's
name (Order), a mod's order by its qualified name, or an order with no name,
such as 200, by its number, aimed at target or at nothing. The new order goes on top of the ship's
orders, as a mission's SetAI does, and the ones below carry on as it ends. Global scripts can give any object orders, and an object script its own
object.
A third argument aims the order at one part of target, as a mission's orders can: for a Launch,
the carrier's launch gate, counting from 0, and for a Dock, its port. A ship given a Launch waits at
its gate until object:start_launch() starts it, as a mission's StartLaunch does:
local ship, carrier = world.objects()[2], world.objects()[1]
ship:give_order("launch", carrier, 1) -- waits at the carrier's second gate
ship:start_launch() -- and goes, after a short random waitopenreliant.orders shows and ends them:
local orders = require("openreliant.orders")
local world = require("openreliant.world")
-- What a ship is doing, from the top order down.
local ship = world.objects()[2]
for _, entry in orders.stack(ship) do
print(entry.order, entry.target.object)
end
orders.cancel(ship) -- ends the top order; the one below carries on
orders.clear(ship) -- drops all of them, as a mission's ClearAI does
print(orders.info("fight").priority)Global and mission scripts can register an order of their own with orders.register(name, definition). It returns the order's qualified name, which give_order takes. A mission script's
orders end with its mission.
-- pulse.luau, from examples/mods/custom-order: a short burst of throttle.
local orders = require("openreliant.orders")
local world = require("openreliant.world")
local elapsed = {}
local pulse = orders.register("pulse", {
flags = { avoidance = true },
init = function(ship)
elapsed[ship] = 0
end,
update = function(ship, target, seconds)
elapsed[ship] += seconds
ship.throttle = 0.25
return elapsed[ship] < 2
end,
exit = function(ship)
elapsed[ship] = nil
ship.throttle = 0
end,
})
-- Elsewhere: world.objects()[2]:give_order(pulse)updateis required. It runs each frame for each ship that follows the order, with the ship, its target (nil for none, or for a flight group), and the seconds of game time. Returningfalseends the order; returning nothing ortruecarries on.initruns as the order starts or starts again, andexitas it ends or another order replaces it. A one-shot order runs onlyupdate.priorityis 0 by default, for an order that any other order replaces. Once an order with a higher priority has started, only an order of higher priority, a one-shot order orexplodereplaces it, as with the game's orders.flags(OrderFlags) are each false by default:players, for an order the player's ship can be given;one_shot, for one that runs its update once and ends;retaliate, to let the ship turn on whoever hits it hard enough;avoidance, to watch for objects the ship could hit while it follows the order; andsend_flight, to send the ship's steering and speed to the other players in a multiplayer game.- The functions can steer the ship, but can't give or end orders or register another. To do something later, send an event.
- Keep each ship's state in a table keyed by the ship, as above, and clear it in
exit. - A function that fails turns its order off: the ships following it drop it, and their orders below carry on.
- The order goes away when the scripts that registered it stop or reload. A loaded saved game starts the global scripts again, and they register their orders again.
openreliant.radio says lines on the radio, as a mission's comms commands do. Global and object
scripts can use it while a mission runs.
local radio = require("openreliant.radio")
-- As CommsFromPilot: Diceman says the mod's line, with his face.
radio.say_pilot("diceman", "ms_dice22_001.ut")
-- As CommsFromShip: the ship says it with its pilot's face, after the lines before it.
radio.say(ship, "ms_dice22_002.ut", { mode = "queued", face = "laughing" })saytakes a ship and shows its pilot's face.say_pilottakes a pilot by its name or number, or one a mod adds by its qualified name.- The speech file is the game's or a mod's (Lines).
- A line is said at once, ending the line playing, as the comms commands say theirs. With
mode = "queued"it's said after the lines before it, and with"if_idle"only if the radio has nothing else to say. facepicks the film of the speaker's face:"talking","laughing","squadron"or"dying". Withonce = truethe film plays once, then the dead channel's while the line goes on, as the commands' Once forms play it.- Each line goes through the
radio_sayhook, so other mods can change or drop it (Changing what the radio says). - Between missions nothing is said, and both functions return false.
radio.busy()says whether the radio is saying a line, or has lines waiting.
A mission that prints its radio as debug text, such as the Dreamcast's mission 22, can be voiced
from the vm_command hook (The mission script's commands). The
argument of a text is its offset in the mission's file, which is the same each time the mission
runs, so a mod can keep a table that gives the line for each text. print(e.arguments[1]) shows
the offsets as the mission runs. To work one out ahead, add the script section's offset in the file
(sltool dte sections), the offset of the text's push_string in the script (sltool dte script), and 2.
A script whose waits were timed for reading text runs ahead of the voices. Holding the wait that follows each voiced text while the radio is busy keeps the mission in step with what is said.
local hooks = require("openreliant.hooks")
local radio = require("openreliant.radio")
-- The mod's table of lines, by the offset of the text each one replaces:
-- { [place] = { "diceman", "ms_dice22_016.ut" }, ... }
local lines = require("lines")
local voiced = false
hooks.add("vm_command", function(e)
if e.command == "print_debug_message" then
local line = lines[e.arguments[1]]
voiced = line ~= nil
if line then
radio.say_pilot(line[1], line[2], { mode = "queued" })
end
elseif e.command == "wait" and voiced then
-- The script waits on after the line, once the radio has said it.
if radio.busy() then
e.result = "hold"
return false
end
voiced = false
end
end)A mission's script shows its objectives in the objectives window with SetObjective, which numbers
them from 0 to 9. world.set_objective(objective, state) does the same from a global or mission
script: "listed" shows the objective, "current" shows it as the current one, which the window
then shows, and "hidden" takes it away. It returns false between missions, and in a mission whose
objectives nothing names.
local world = require("openreliant.world")
-- The convoy's escort is down: on to the troop carriers.
world.set_objective(0, "listed")
world.set_objective(1, "current")The objectives' names come from the game's table, by the mission's number. A campaign mission's can come from its record (Each mission of the campaign), and a game mode's mission's from the mode (Game modes).
Scripts send each other events: core.send_global_event(name, data) to the global and mission
scripts, and object:send_event(name, data) to the scripts of one object. Global, mission and
object scripts handle them in the event_handlers they return, by the event's name:
-- The wingman's script tells the global scripts it has fled.
local core = require("openreliant.core")
local self = require("openreliant.self")
core.send_global_event("WingmanFled", { ship = self })
-- A global script hears it.
return {
event_handlers = {
WingmanFled = function(data)
print(tostring(data.ship) .. " has fled")
end,
},
}- An event arrives at the next update, before the scripts'
on_update. - Player and menu scripts send events to the global scripts too, which is how they change the
game. They don't receive events: to show the game's state, a global script writes it to a
game section, and the player script reads it each frame, as
examples/mods/arenadoes. datamust be plain data: nil, booleans, numbers, strings, vectors, objects, and tables of these. It's copied as it's sent, so changing the table afterwards changes nothing.- Each script with a handler for the event gets it, newest mod first. A handler that returns
falsestops the rest.
A script offers functions to other scripts by returning interface_name and interface.
Other scripts reach it through require("openreliant.interfaces"), as I.<name>:
-- In mod A's global script.
local fled = 0
return {
interface_name = "Wingmen",
interface = { fled = function() return fled end },
event_handlers = { WingmanFled = function() fled += 1 end },
}
-- In another mod's global script.
local I = require("openreliant.interfaces")
if I.Wingmen and I.Wingmen.fled() > 2 then
-- ...
end- Global and mission scripts see each other's interfaces. The scripts on an object see the interfaces of the other scripts on that object, and player and menu scripts see each other's.
- An interface that nobody offers is nil.
- Other scripts get a read-only copy of
interface, taken as the script starts. Keep what changes in the script's own variables, asfledabove, not in the table. - A later script that offers the same name takes its place, and gets the earlier interface in its
on_interface_override(base)handler, so it can call through to it.
I also holds groups of the packages' functions, under names that suit what a mod does:
| Group | What it holds | Scripts |
|---|---|---|
Flight |
The orientation and frame functions of util |
All |
AI |
orders, and give_order(ship, order, target, component) |
Global, object |
Combat |
add_hook and after_hook, which are hooks.add and hooks.after |
Global, object |
Carriers |
give_order, start_launch, add_hook and after_hook |
Global, object |
Camera, HUD |
The camera and hud packages |
Player |
Controls, Audio |
The input and audio packages |
Player, menu |
FrontEnd |
The ui package |
Player, menu |
Missions |
The world package |
Global |
Campaign |
world.mission and core.send_global_event |
Global |
Campaign is about the mission that runs, not about a mod's campaigns. A group a
script can't use is nil, and its functions keep the rules of their packages. A mod can offer an
interface under a group's name, such as interface_name = "Flight", which then takes its place for
the scripts that see it, and gets the built-in group in on_interface_override(base). When it
stops, the built-in group comes back. Built-in interfaces lists
each group's functions, and examples/mods/strafe-run uses
them.
require("openreliant.records") gives the game's records. Load scripts can change them; other
scripts can only read them.
| Table | Contents | First number | Names |
|---|---|---|---|
ships |
Ship stats, shipstats.bin, then the types the mods add |
0 | Every ship type's name, such as predator, and the qualified names of the types the mods add, such as teapot:teapot |
ship_types |
What the executable holds for each ship type, then for the types the mods add | 0 | As ships |
guns |
Gun stats, gunstats.bin, then the guns the mods add |
1 | laser_cannon, pulse_cannon and the rest, and the qualified names of the mods' guns |
missiles |
Missile stats, missilestats.bin, then the missiles the mods add |
0 | screamer, raptor and the rest, and the qualified names of the mods' missiles |
pilots |
Pilot stats, pilotstats.bin, then the pilots the mods add |
0 | frenchy, viper, ronin_leader and the other pilots the game's code singles out, and the qualified names of the mods' pilots |
faces |
The pilots' faces, which the game keeps in its executable, then those of the pilots the mods add | 0 | As pilots |
text |
The game's text, language.dll, by string id |
1 | |
itac_text |
The ITAC's text, itaclang.dll, by string id |
1 |
The package also holds the campaign's order of missions, campaign
(The campaign's missions), each mission's settings, missions
(Each mission of the campaign), the KILLBOARD's pilots,
killboard (The KILLBOARD's pilots), the combat maneuvers,
maneuvers (Combat maneuvers), and the planets, planets
(Planets).
Records are looked up by number or by name, with the field names of the stat
tables. The definitions file for editors (Editors) lists every
field of Ship, Gun, Missile and Pilot.
A ship type's record (ShipTypeRecord) holds what the game keeps in its executable rather than in
shipstats.bin: targetable, whether ships can target it; name, the id of the string that names
it; class, such as fighter or support; side, the side its objects start on; and display,
how the target display draws it. The class decides more than how the type is targeted: fighters
fly with the fighters' flight model, and only fighters turn on a fighter that hits them
(Retaliation).
A pilot's face (Face) has name, the id of the string the radio's window shows over the face,
and the film the face plays for each way it moves as the pilot speaks: talking, laughing,
squadron (the 45th's own pilot, in most faces) and dying. A film is the name of a face film
pilots\<film>.fm8 in pilots.hog, or <film>.fm8 in a mod (Face films), at
most 116 characters long. An empty name plays the dead channel's static.
local records = require("openreliant.records")
records.guns.laser_cannon.damage.hull = 30 -- by name
records.ships[12].max_speed *= 1.1 -- by number
records.pilots[66].skill = "high" -- values with names use their names
records.text[568] = "Laser Cannon Mk II" -- text is a string
records.faces.frenchy.talking = "45Tigers_Plt" -- a face's film
for number, missile in records.missiles do -- every record, in order
missile.lock_time *= 0.8
end- Assigning a table to a record changes only the fields in it. With a
templaterecord, the record is first copied from the template:records.guns[2] = { template = records.guns[1], speed = 5 }. - A wrong field name or a value of the wrong type is an error.
- Text is UTF-8; characters the game can't show become
?. - Records can't be removed, because missions refer to them by number.
- A load script that fails has its changes undone, and the next one runs.
on_records_loadedruns once every mod's load scripts have run, so a mod can adjust what the mods before and after it changed (examples/mods/balance).
A mod adds a record by adding a ship type, a gun, a missile or a pilot in its manifest (New ships, guns, missiles and pilots). The record starts as a copy of its base's, and a load script tunes it by its qualified name:
-- records.luau, from examples/mods/bananas.
local records = require("openreliant.records")
-- The Banana Gun's shots fly faster and hit harder than the Pulse Cannon's.
local gun = records.guns["bananas:banana_gun"]
gun.speed *= 1.5
gun.damage.shield *= 1.5
gun.damage.hull *= 1.5
-- A Banana locks on in half the time of a Bandit.
local missile = records.missiles["bananas:banana"]
missile.lock_time = math.floor(missile.lock_time / 2)
-- The Trooper flies like a beginner.
local trooper = records.pilots["bananas:trooper"]
trooper.tier_b = "level_0"
trooper.skill = "low"Wherever scripts see a ship type, a gun, a missile or a pilot, a built-in one is OpenReliant's name
for it, such as "predator", or a number where it has none. Every ship type has a name. One a mod
adds is its qualified name, such as "teapot:teapot". A pilot can also be "none".
examples/mods/interceptor tunes a mod's ship type.
records.campaign is the list of missions the game's campaign flies, by their numbers, in order.
The game's list is 1 to 11, 14, 15, 16, 18 to 21, and 23 to 28: the campaign has no missions 12, 13,
17 and 22. Reading it gives a new list each time, and a load script changes the campaign by
assigning a list back. With mission files of its own, a mod can put missions into the campaign, or
make a campaign of its own.
local records = require("openreliant.records")
-- Missions 12 and 13 after 11, 17 after 16, and 22 after 21, from the mod's own mission files.
local campaign = records.campaign
for _, mission in { 12, 13, 17, 22 } do
table.insert(campaign, mission)
end
table.sort(campaign)
records.campaign = campaign- The list holds numbers from 1 to 28, rising, each at most once, and at least one. The saved game keeps a record for each of those 28 missions. To fly missions in another order, number their files in that order.
- The campaign starts at the list's first mission, goes on to the next one after each, and comes to the story's end after the last.
- The number the player sees for a mission, in the autosave's name and the saved games' list, is its place on the list. The ITAC's debriefings list the missions on it.
- What the campaign makes of each mission, such as its briefing and its carrier, is in
records.missions(Each mission of the campaign). - The campaign takes the list once, as OpenReliant starts, after every mod's load scripts and their
on_records_loadedhandlers. Changing it in a game mode'srecordsscript does nothing.
records.missions holds the campaign's settings for each of its missions, indexed by the
mission's number: what the briefing room plays before it, the carrier it's flown from, the names of
its objectives, the date the launch shows, what it awards, Enriquez's report and debriefing, the
ITAC's news, the landing and the chapter's news after it, the wing's pilots, and the rules the game
applies to particular missions. The game decides each of these by the mission's number. A mission's fields start with the game's values, and load scripts change
them in place, as they do a record's. The reference lists the
fields.
A mod that changes one thing changes one field:
local records = require("openreliant.records")
records.missions[5].objectives = { "Destroy the convoy", "Protect the Patriot" }A mod that puts back missions the campaign doesn't fly gives them what the game lacks. A mod can't make a Bink movie for the briefing room's screen, so Enriquez can speak the briefing over the room instead, as she does at the campaign's end, while another mission's movie that fits plays on the screen without its sound:
local mission = records.missions[12]
mission.hologram = "new_m05.bik"
mission.speech = "dreamcast_brief12.ut"
mission.date = "February 2, 2161"A campaign of the mod's own sets each of its missions. Assigning a table to a mission changes the fields the table gives:
records.missions[3] = { carrier = "yamato", hologram = "mycampaign_m03.bik", last_word = "ms_speech\\mycampaign_tag03.ut" }A restored mission can also have its own report on the rooms' television, its own debriefing and its own news in the ITAC:
local mission = records.missions[12]
mission.television_report = { { scene = "dreamcast_0115.box" } }
local debriefing = mission.debriefing
debriefing.success = { "Good work out there.", "The Reliant is safe for now." }
mission.debriefing = debriefing
-- The news after mission 12, in the rooms before the next mission.
records.missions[14].news = { { title = "Reliant survives ambush", paragraphs = { "..." }, picture = 13 } }It can award a medal, land on the other carrier, play a report of its own after the chapter's movie, and bring a pilot into the wing:
local mission = records.missions[13]
mission.medal = "valour"
mission.landing_carrier = "yamato"
mission.chapter = 2
mission.chapter_reports = { { movie = "mymod_news13.bik", unless = { "krasnaya_alive" } } }
mission.alpha_5_pilot = "diceman"A campaign of the mod's own turns off the game's rules for particular missions where its missions don't want them:
-- Mission 25 is an ordinary mission here: no second part, no Kamovs, and a landing whatever
-- happens to the Yamato.
local mission = records.missions[25]
mission.second_part = false
mission.kamov_wing = false
mission.fort_bear_ending = false- Each mod changes only the fields it sets, in load order, so mods that change different fields of the same mission don't undo each other's changes.
- Reading
objectivesgives a new list each time. To change the names, assign a list. Nil gives back the names the game's own table has for the mission's number. - Where a mission has
speech, Enriquez speaks it over the room, and the briefing ends when she does. The hologram plays on the room's screen meanwhile without its sound, and starts over if it ends first, so another mission's movie can show something fitting. A hologram of nil leaves the screen empty. - The carrier decides the rooms the player walks, the briefing room, the loadout's backdrop and the hangar the launch starts in.
- The game reads the missions as it goes, so a game mode's
recordsscript changes them for the mode's missions alone. - A mission's awards are fields too: the loadout
tierit unlocks, thechapterit ends, which gives a ribbon and a movie, and itsmedal. So are the special cases the game ties to missions 1 and 23: the new pilot'sinductionand the loadout'slessonbefore mission 1, and the loadout'sonly_ship, the Shroud, before mission 23. A mod that moves or adds missions can move these with them. television_reportis Enriquez's report on the rooms' television before the mission, anddebriefingher debriefing in the ITAC after it, with paragraphs for each rating.newsandvideo_reportsare the ITAC's news items and video reports that appear in the rooms before the mission, and stay listed after it. So the news of how a mission went goes on the mission after it.- Reading
television_report,debriefing,newsorvideo_reportsgives a new table with the game's text in it, which you can change and assign back. - The rules the game applies to particular missions are fields too, each true or false: the wing's
t_twins from mission 14 (wing_twins), the Flying Tigers' name after mission 13 (flying_tigers), mission 25'ssecond_partand Kamovs (kamov_wing), mission 28'sclose_ion_cannons,hurried_turretsandterminate_ends_well, mission 26'sripper_from_below, mission 8'swide_advanced_gate,counts_killsup to mission 27, missions 25 and 27'sfort_bear_ending, and the ITAC'scobras_inquiryfrom mission 7 andfifty_first_listedup to mission 9. A replacement campaign turns off the ones its missions don't want, such as mission 25's second part. landing_carrieris where the landing plays after the mission, andyamato_visitwhether the ship lands on the Yamato instead, as after missions 7 and 8.chapter_reportsare the news reports after the chapter's movie, each waiting on the game's variables, such asrameses_alive, andalpha_5_pilotandalpha_6_pilotthe pilots the wing takes on.
records.killboard holds the pilots of the ITAC's KILLBOARD, numbered from 1, in the game's order.
Each has the text the board shows, the kills it starts with and adds, its portrait, and the
missions it joins the board at, leaves it after and sits out. A mod that restores or moves missions
keeps the board in step with its campaign, and a campaign of its own gives the board's places
pilots of its own:
local records = require("openreliant.records")
for _, pilot in records.killboard do
if pilot.name == "Klaus Steiner" then
-- He flies the restored mission 22 after all.
pilot.sits_out = { 19, 20, 21, 23 }
end
end
records.killboard[19] = { name = "Trent Ramsey", squadron = "(45th Volunteers)", kills = 12, joins_at = 12 }- The board always has the game's 19 places: a script changes pilots but can't add or remove them. A pilot who should never show can leave after mission 0.
- Text is read from the ITAC's strings and can be changed to any text, in the game's code page.
- A pilot with
in_45thset shows as one of the 45th Flying Tigers in the missions whoseflying_tigersrule is on (Each mission of the campaign).
records.maneuvers holds the combat maneuvers that ships fly under the Fight order, numbered from 0
as the game numbers them, from defend_dodge1 to run_to_ship (9). Each has a name, the turning
inputs it may mirror, the range of its length in ticks (min_ticks and max_ticks), and its
script: a list of lines in the maneuvers' language (Combat maneuvers).
A load script can change a maneuver, or add one by assigning to the number after the last:
local records = require("openreliant.records")
-- A shorter loop the loop.
records.maneuvers[8].max_ticks = 2000
-- A barrel roll, number 10.
records.maneuvers[#records.maneuvers] = {
name = "barrel roll",
mirror = { roll = true },
min_ticks = 1500,
max_ticks = 3000,
script = { "Cloak(off)", "SetSpeed(1)", "SetPitch(0.2)", "loop:", "SetRoll(1)", "Wait(500)", "Goto loop" },
}The game never chooses a maneuver that a mod adds. A handler of fight_choose_maneuver chooses
one by setting e.result, and object.maneuver is the one a ship flies: the game's by name, a
mod's by number, or nil while the ship isn't fighting.
local hooks = require("openreliant.hooks")
-- One time in four, a hostile fighter rolls instead.
hooks.add("fight_choose_maneuver", function(e)
if math.random(4) == 1 then
e.result = 10
return false
end
end, { side = "hostile", class = "fighter" })- A script is checked as it's set, and a line the language doesn't have is an error that gives its number. A script has 255 lines at most, and there can be 255 maneuvers.
- A game mode's records script can change and add maneuvers for the mode's missions alone (Game modes).
- A maneuver that a handler chooses runs for a length drawn from its range. A number with no
maneuver runs nothing, and the ship chooses again on its next frame. So does a handler that stops
the choice without setting
e.result. - A handler of
maneuver_runthat stops it flies the ship itself, by setting itsyaw_input,pitch_input,roll_inputandthrottle.
records.planets holds the object types that the game sets up as planets when a mission makes
them: lit by the sun alone, always drawn, and colliding with nothing
(Planets). The original's are Neptune to Venus and their far
models, numbered from 1 in the order of their types. Each entry has a type, how fast the planet
turns about its north pole (spin, in radians a second, positive eastward), how far and in which
direction its pole leans (tilt and tilt_toward, in radians), and its atmosphere: the colour of
the band round its rim (atmosphere_color, nil for none) and where the band starts and ends
(atmosphere_inner and atmosphere_outer, as shares of its radius as the camera sees it; the
planet hides the band within its edge), or a mod's surface function that draws the atmosphere
instead, with the numbers it reads (atmosphere_surface and atmosphere_parameters, A planet's
atmosphere). A load script can change a planet, or add one by assigning to
the number after the last:
local records = require("openreliant.records")
-- Saturn, number 4, turns eastward once every ten minutes, its pole leaning 26.7 degrees toward +X.
records.planets[4].spin = 2 * math.pi / 600
records.planets[4].tilt = math.rad(26.7)
records.planets[4].tilt_toward = math.pi / 2
-- Earth, which the original leaves out, with a thin blue atmosphere.
records.planets[#records.planets + 1] = {
type = "earth",
spin = 2 * math.pi / 1440,
tilt = math.rad(23.4),
atmosphere_color = vector.create(0.3, 0.5, 1),
atmosphere_inner = 0.97,
atmosphere_outer = 1.02,
}- A planet's north pole is its model's -Y, which is up on the screen when the planet isn't turned. The pole leans from the up direction of the planet as the mission places it, and the planet spins about the leaning pole.
- If a type has several entries, the first one counts. A type that a mod adds uses its base type's entry, unless the table has an entry for it.
- The table holds up to 64 planets. A game mode's records script can change and add planets for that mode's missions only (Game modes).
The planets have a light of their own, which shines from the sun and reaches nothing else
(Lights). It has the colour of the game's key lights, (1, 1, 0.8),
which the original lights everything with, unless a load script gives it another in
records.planet_light, such as white for planets whose textures hold their true colours. Nil gives
it the key lights' colour again. Ships and every other model keep their light.
local records = require("openreliant.records")
records.planet_light = vector.create(1, 1, 1)The game is saved between missions: in the Reliant's rooms, and by the autosave as the campaign
moves on. Global and player scripts save their state in a file next to the saved game
(saves\<call sign>GAME<slot>.scripts), so the saved game itself stays as the original writes it:
local missions = 0
return {
engine_handlers = {
on_mission_start = function()
missions += 1
end,
on_save = function()
return { missions = missions }
end,
on_load = function(saved)
missions = saved and saved.missions or 0
end,
},
}on_savereturns plain data (Events), which is kept with the saved game.- When a saved game is loaded, the scripts start again, and each gets
on_loadwith what itson_savereturned, in place ofon_init. A script that didn't run when the game was saved, such as one of a mod added since, getson_initinstead. - The same goes for the restart point. The scripts' state is kept as each mission of the campaign starts, and a replay or the pause menu's RESTART puts it back, so that the scripts start the mission again as they were.
- All the mods' state together, their game sections (Storage), what their scripts
saved and their timers, can take up to 256 MiB. If it would take more, the mods that keep the
most are left out until the rest fits, and the log names them. When the game is loaded, their
scripts get
on_init, like those of a mod added since. - Mission and object scripts don't run between missions, so they aren't kept. Menu scripts run across games, so they aren't kept either.
examples/mods/tally keeps a tally with each saved game.
openreliant.storage gives each mod named sections of plain data, which you read and change like
tables:
local storage = require("openreliant.storage")
local tally = storage.game_section("tally")
local best = storage.global_section("best")
tally.kills = (tally.kills or 0) + 1
if tally.kills > (best.kills or 0) then
best.kills = tally.kills
end
for name, value in tally do
print(name, value)
end- A game section goes with the saved game and the restart point, and starts empty with each new game. Global and object scripts change it; the other scripts can only read it.
- A global section is kept in the game folder, in
storage\<mod>.data, across every game. Any script can change it. OpenReliant writes the sections that changed at most every 2 seconds, and as it quits. A crash or a full disk during a write leaves the file as it was before. - The global section called
optionsholds the mod's options (Options), andglobal_sectiondoesn't open it. - Each mod has its own sections: two mods' sections of the same name are separate. All of a mod's scripts see the same sections.
- Values are plain data, copied as they're stored and as they're read, so changing a table read from a section changes nothing until it's stored again. Setting a field to nil removes it.
- A mod's sections hold up to just under 15 MiB in each place, the game's and the global, measured as the memory they take. A change past that is an error, but one that makes them smaller works. A value stored at once can take up to 16 MiB, a table that holds the same table twice counting it twice.
openreliant.async runs one of the mod's functions after a delay:
local async = require("openreliant.async")
async.register_timer("reinforce", function(data)
print("wave " .. data.wave)
end)
async.after(30, "reinforce", { wave = 2 })- A timer names a function rather than holding it, so that it can be kept with the saved game. Register the function when the script runs, at its top level, so that it's there again after a load.
- A timer only runs a function registered by scripts of the same kind in the same mod, and for an object script, on the same object. A player script can't run a function the mod's global script registered.
- Global and object scripts' timers count game time, which stops while the game is paused. Player and menu scripts' timers count real time.
- A timer runs at the first update after its time is up, before
on_update; for player and menu scripts, at the next frame, beforeon_frame. It gets its data, which must be plain data. - Global and player scripts' timers are kept with the saved game and the restart point. An object script's timers stop as its object leaves the mission.
Player and menu scripts decide what the player sees, hears and does. They run separately from the game's scripts: they can read objects, but change the game only by sending events to the global scripts.
-- clock.luau, listed as Player=clock.luau: the time spent flying, over the flight display.
local hud = require("openreliant.hud")
local flown = 0
return {
engine_handlers = {
on_frame = function(seconds)
if not hud.shown then return end
flown += seconds
hud.text(vector.create(16, 16, 0), string.format("%.0f s", flown), {
color = vector.create(0.4, 1, 0.4),
})
end,
on_key_press = function(key)
if key == "f9" then flown = 0 end
end,
},
}on_frameruns each frame drawn, even while the game is paused, with the seconds of real time since the last.- For a player script,
require("openreliant.self")gives the player's ship (nil between games), andopenreliant.nearbythe objects around it. - Player and menu scripts run, and draw with
uiover the screen, in the briefing, the loadout, the ITAC and the other rooms, over the movies and over the loading screens too.
examples/mods/dvd draws over the menus, and
examples/mods/wingmen over the flight display.
openreliant.hud draws over the flight display, and openreliant.ui over the menus: the front
end's screens and the pause menu. Both have text, line, rectangle, picture and shape, and
measure for the size of a text.
- Draw in
on_frame, or in a registered display's or screen'sframe: each frame starts with nothing drawn. Drawing at any other time is an error. shownsays whether the display or the menu shows this frame. Check it before drawing.- Places are in the window's pixels from its top left corner, and
widthandheightgive the window's size. - Each function takes a style table, whose fields are all optional
(Tables):
color(a vector of red, green and blue from 0 to 1) andalphafor all of them,widthfor a line,scale,align("left","center"or"right"),fontandbase_fontfor text. - Text is drawn in the game's font, at the game's text size times the style's
scale, unless the style picks another font (Pictures, shapes and fonts).
hud and ui can draw the mod's PNG pictures and the game's shapes, and text in the mod's fonts:
local ui = require("openreliant.ui")
return {
engine_handlers = {
on_frame = function()
if not ui.shown then return end
ui.picture(vector.create(20, 30, 0), "badge.png", vector.create(64, 64, 0), { alpha = 0.8 })
ui.shape(vector.create(100, 30, 0), 1, { scale = 1, color = vector.create(1, 1, 1) })
local style = { font = "menu_large", scale = 1.5 }
local size = ui.measure("Flight status", style)
ui.text(vector.create(20, 110, 0), "Flight status", style)
end,
},
}- Pictures are PNG files in the mod, named without folders.
sizeis in window pixels; without it, the picture is drawn at its own size. The style'scolorandalphatint it. - Shapes. A shape number picks a shape from the game's current sprite set: the flight display's
set for
hud, or the current menu screen's set forui(Shapes). Each shape keeps its own anchor point, and is drawn at the game's scale times the style'sscale. A set that isn't loaded, or a shape it doesn't have, is an error. - Fonts. A text style's
fontis one of the game's,"default","hud","menu_small"or"menu_large", or a font file in the mod. Text is always drawn in the style's colour.- A
.ttfor.otffile is drawn at the window's resolution. It takes the spacing of the game's font thatbase_fontnames ("default"unless given), and fits over it the way a mod's replacement for that font does (Fonts), so it works over any of them. The base font draws the characters the file doesn't have. With OUTLINE FONTS off under VIDEO, the text is drawn in the base font. - A
.fntfile is drawn like the game's fonts. Most fonts store each pixel as a level of the text's colour, as the menus' fonts do. If most of a font's letters use colours of its palette instead, as the flight display's font does, each pixel is as strong as its colour is close to the letters' brightest colour.
- A
measuretakes a text style, or just a number for its scale, and measures astextdraws:widthandheightlay the line out, andinkis the box its letters' pixels cover, from the point the text is drawn at as itsalignplaces it, or nil for text without any, such as spaces. An outline font's letters can start and end inside or outside the font's widths, soinklines up what shows, such as columns of text whose first letters differ.- The pictures and fonts stay loaded once drawn, and a reload reads the files again. All the mods' pictures and fonts together can hold up to 128 files and 128 MiB, a picture taking 4 bytes a pixel: a 4096x4096 picture takes 64 MiB. When a new one doesn't fit, the pictures drawn longest ago make room for it, so a mod can cycle through more pictures than that, such as the frames of an animation. Only the fonts and the pictures drawn in the last two frames have to fit at once.
- A missing or broken file is an error in the script, and so is a picture or font that doesn't fit.
examples/mods/drawing-assets draws each of them.
A player script can register a display, which draws over the flight display each frame:
local hud = require("openreliant.hud")
local status = hud.register_display("status", {
frame = function(seconds)
hud.text(vector.create(20, 30, 0), "Shift F12: strafe run")
end,
})
hud.set_display_enabled(status, false) -- hides it until it's turned on againWhile the flight display shows, each enabled display draws in the order it was registered, after
the scripts' on_frame. A display whose frame fails is turned off; the others carry on.
A display can stand in for the game's own instruments, and move or scale them:
hud.register_display("radar", {
replaces = { "radar" }, -- the game's radar isn't drawn; this display draws one
layout = {
gauges = { scale = 1.25 }, -- the targeting cluster, drawn larger
clock = { offset = vector.create(-120, 70, 0) }, -- the clock, moved
},
frame = function(seconds)
local box = hud.bounds("radar") -- where the game's radar would draw
if box then
-- draw a radar of the mod's own in the box
end
end,
})-
replaceslists the instruments (HudInstrument) the display stands in for: the radar, the readouts, the targeting cluster, the reticle, the clock, the target's markers, the messages, each window and the rest. They aren't drawn while the display is on, but they keep working: RADAR RANGES still changes the radar's range, and the windows still open and close with their keys. -
layoutmoves and scales the instruments it names.offsetis in the game's pixels, which grow with the window as the flight display's own do, so a layout looks the same at any size.scale, above 0 and up to 8, draws the instrument that many times its size, sharp at the size it's drawn, growing from where it stands on the screen, as the whole display grows with the window. -
Everything goes back as it was as soon as the display is turned off, its
framefails, or its mod stops. Where two displays place the same instrument, the one registered later does. -
hud.bounds(instrument)gives where an instrument last drew, in the window's pixels, as the layouts place it, and also while a display stands in for it, so that a display can draw its own in the game's instrument's place. -
hud.power_ball(at, size, style)draws the power window's ball, turning with the player's power setting, with its top left corner atatandsizein window pixels (nil for its size on the game's display), for a display that stands in for the power window. Unlike the window's, it doesn't shake when the ship is hit. -
What the instruments show can be read in any view during a mission, for a display that stands in for one or one drawn outside the game:
hud.guns, the gun group, how the guns fire, their charge, whether the gunnery window shows how a pair fires, and the rounds left on the ships whose guns fire them;hud.missiles, the ring of missiles, each in its place round the ring, and the armed one;hud.target, the target the display shows and its subtarget; andhud.open_windows, withhud.window_state(instrument)for how far a window has opened and whether it's opening or closing.hud.radar, the radar's range and reach, where its rings stand as they move between the ranges', and its contacts: each object it shows, where its dot stands from the radar's middle in the game's pixels, how far below the rings' plane it stands, and how it shows: the target, the ship whose line the radio's window names, a hostile, the nav point or another.hud.target_display, what the target display shows of its target in either form, open or not: the form, the type's and the pilot's names, the range and the speed, the arcs of its shields and armour, the quadrants that flash as its armour is hit, the class of the subtarget's part (PartClass), its name and the share of its armour's bar lit, and the share of the hull's bar lit.- The readouts:
hud.fuel, the seconds of afterburner fuel;hud.kills; andhud.countermeasures, withhud.countermeasures_lit, false while a mission's script flashes the readout dark. hud.gauges, the targeting cluster: the speed and the speed the throttle asks for, as their figures show them; where the speed's and the throttle's markers stand on the left arc, from 0 to 1, and how bright the throttle's is (nil while it doesn't show); and how far up the right arc is lit, from 0 to 1, and whether it shows the Nova Cannon's charge.hud.ship_status: how many of the shields' and the armour's five arcs show in each quadrant, the arcs for what SHIELD BALANCING has shifted fore and aft, and the quadrants that flash as the armour is hit.hud.lights, the status lights that show, steady or flashing;hud.lights_lit, those lit, which leaves out a flashing light while it's dark;hud.charges, the ECM's, the cloak's and the spectral shields' charges, which their lights show as bars;hud.clock, the minutes and seconds the clock shows;hud.view_name, the name written at the top of the screen in the views that have one; andhud.caption, the launch's date as far as it has typed it.- The windows, whether or not they're open:
hud.damage, how well the weapons, engines and shields still work, from 0 to 1;hud.power, the shields', weapons' and engines' shares of the power as the percentages the power window writes;hud.wingmen, the wing's ships with their numbers and the share of their armour bars lit;hud.objectives, the objectives the window can show, with their names, which is current and which it's showing; andhud.comms, the items of the radio's menu. - The text the display writes:
hud.messages, the message lines, oldest first;hud.subtitle, the lineDisplaySubTitleshows;hud.speaker_name, the name the radio's window writes over the speaker's face while their line plays;hud.key_prompt, the actionWaitForKeywaits for; andhud.jump_prompt,jumporwarpwhile the prompt for what the mission has ready flashes.
hud.instruments_shownsays whether the game's instruments show this frame, which is in the view ahead from the cockpit. Each reading is worked out as its instrument works it out, when the scripts run, before the flight display draws; what the display keeps between frames, such as its target, how far its windows have opened and what flashes, is as it last drew it. Each is nil, or empty, outside a mission.
examples/mods/hud-layout draws a radar of its own in the game's
radar's place, with the guns and missiles beside it.
A display can also place the parts of an instrument on their own, without standing in for it: the text each instrument writes, the face in the radio's window, and the power ball.
hud.register_display("comms", {
parts = {
-- The speaker's name, ending over the face's right edge.
radio_speaker = { offset = vector.create(132, 0, 0), align = "right" },
-- The face, half as large again.
radio_face = { scale = 1.5 },
-- The power window's title, in other words.
power_title = { text = "POWER" },
-- The target's pilot, left out.
target_display_pilot = { hidden = true },
},
frame = function(seconds) end,
})offsetandscalemove and scale a part aslayoutdoes an instrument, on top of its instrument's layout. A part grows from its own place: a text from the point its alignment lines it up on, and a picture from its top left corner.alignlines a text part up on its point another way:"left","center"or"right".textgives a text part other words, at most 64 characters, written wherever it writes. A part that writes several lines, such asmessages_text, writes them on each.hiddenleaves a part out. Its instrument still works, andhud.part_bounds(part)gives where the part last drew as the displays place it, also while it's hidden, so that a display can draw its own in its place.- A picture takes no
alignortext. Where two displays place the same part, the one registered later does.
| Part | What it is |
|---|---|
caption_text, caption_cursor |
The date the launch types out, and the cursor after it while it types |
key_prompt_press, key_prompt_action, key_prompt_key, key_prompt_modifier, key_prompt_plus |
WaitForKey's prompt: PRESS, the action's name, the key's name on its cap or the joystick's button, and the modifier's name on its cap with the plus after it |
view_name_text |
The view's name at the top of the screen |
subtitle_text |
The line DisplaySubTitle shows |
messages_text |
The message lines |
fuel_figure, kills_figure, countermeasures_figure |
The readouts' figures |
gauges_speed, gauges_throttle |
The targeting cluster's figures: the speed the ship makes, and the speed the throttle asks |
target_markers_range |
The target's range by its brackets, and by the arrow toward a target off the screen |
clock_text |
The clock's figures |
radio_speaker |
The speaker's name over the face in the radio's window |
radio_face |
The face of whoever speaks, in the radio's window (a picture) |
gunnery_gun, gunnery_rounds |
The gunnery window's gun name, or FULL GUNS, and its rounds left |
missiles_count, missiles_name |
The missile window's count and missile name |
target_display_name, target_display_pilot, target_display_range, target_display_speed, target_display_subtarget |
The target displays' lines, in either form: the target's name, its pilot's, its range and speed, and the subtarget's name |
damage_title, damage_names |
The damage window's title, and the systems' names |
power_title, power_figures |
The power window's title, and its percentages |
power_ball |
The power window's ball (a picture) |
objectives_title, objectives_heading, objectives_name |
The objectives window's title, the objective's heading, and its name |
comms_title, comms_numbers, comms_items |
The radio's menu, in either window that shows it: its title, the items' numbers, and the items |
wing_status_title, wing_status_numbers |
The wing status window's title, and the wingmen's numbers |
A player or menu script can register a screen: a panel that draws with ui and gets the keys while
it's shown.
local ui = require("openreliant.ui")
local help = ui.register_screen("help", {
frame = function(seconds)
ui.rectangle(vector.create(20, 60, 0), vector.create(500, 160, 0), { alpha = 0.8 })
ui.text(vector.create(30, 70, 0), "Escape closes this panel.")
end,
key = function(key, down)
if key == "escape" and down then ui.show_screen(nil) end
end,
})
ui.show_screen(help)ui.show_screen(name)shows a screen, andui.show_screen(nil)closes it. One shows at a time.- In flight, a shown screen draws over the flight display. The game's controls still get the keys.
- A screen whose
frameorkeyfails closes, and so does a screen whose scripts stop. - A menu script can also make a screen take the place of one of the front end's (Replacing a screen).
A player script can register a camera view, which places the camera each frame:
local camera = require("openreliant.camera")
local util = require("openreliant.util")
local chase = camera.register_view("chase", {
frame = function(ship, seconds)
return {
position = util.to_world(ship.position, ship.orientation, vector.create(0, -200, -1000)),
orientation = ship.orientation,
}
end,
letterbox = false,
})
camera.set_view(chase) -- looks at the player's ship
camera.set_view("cockpit") -- back to the cockpitframegets the object the view looks at and the seconds since the last frame, and returns the camera'spositionandorientation. The orientation's axes must be unit length, at right angles and right-handed.set_view(view, object)switches to a view ofobject, or of the player's ship. It takes the game's views by name, such as"cockpit","chase"or"target"(View), and the mods' by their qualified names, or within the mod by its own names (Qualified names).camera.viewis the view that shows.- A mod's view is an outside view: it draws no cockpit, and
letterboxadds bars above and below. - The game's camera keys and a mission's cutaways override a script's view, and a script can't change the view while the mission holds the camera.
- If
framefails, or the object leaves the mission, the camera goes back to the cockpit. - One run of OpenReliant has room for about 200 views, displays and screens together, counting the ones registered before a reload. Registering more is an error.
on_key_press(key) and on_key_release(key) hear the keys, by name, such as "f9" or "escape"
(Key). input.key_down(key) says whether a key is held.
In flight, on_action(action) hears the controls bound to the game's actions, by name, such as
"fire_lasers" (Action), and input.action_down(action) says whether
they're held.
A menu script can register an action of its own, which the controls screen lists after the game's, for the player to bind:
-- action.luau, from examples/mods/custom-order: Shift F12 starts the mod's order.
local input = require("openreliant.input")
local core = require("openreliant.core")
local pulse = input.register_action("pulse", {
label = "Custom order: throttle pulse",
key = "f12",
modifier = "shift",
})
return {
engine_handlers = {
on_action = function(action)
if action == pulse then
core.send_global_event("CustomOrderPulse", {})
end
end,
},
}register_actionreturns the action's qualified name, such ascustom-order:pulse, whichon_actionpasses.action_downtakes it too, or within the mod the name without the prefix, such as"pulse". Registering the same name twice is an error, even in another case.- The definition needs a
label, and can give a defaultkeywith amodifier("none","shift"or"control"), a joystickbuttonand agamepad_button. A default that the game or another mod already uses stays unbound. - The player rebinds, clears and resets the actions on the controls screen, as the game's.
starlancer.inikeeps the bindings by the actions' qualified names. - A control held when the action registers, or held outside flight, must be released before it counts as pressed again.
openreliant.audio plays sounds, music and Betty's lines:
local audio = require("openreliant.audio")
audio.play_sound(3, 0.5) -- the game's standard sound 3, at half volume
audio.play_music("New_Pensive.wav") -- from the music folder, or a mod's file of that name
audio.say("countermeasures_low") -- Betty's lineplay_soundtakes the number of one of the game's standard sounds, the menus' and the display's. A mod replaces a sound by replacing its file (Modding).play_musicplays a piece from the game'smusicfolder, looping, in place of the music that plays.saytakes one of Betty's lines by name (BettyLine).
openreliant.debug draws lines and text at points of the world, over the flight display, where the
camera sees them. It's for working out what a script does:
local debug = require("openreliant.debug")
local self = require("openreliant.self")
return {
engine_handlers = {
on_frame = function()
local target = self and self.last_attacker
if target then
debug.line(self.position, target.position, { color = vector.create(1, 0, 0) })
debug.text(target.position, "attacker")
end
end,
},
}A menu script can replace the front end's screens with its own, and a load or menu script can add game modes, which the main menu's GAME MODES lists. A campaign is a game mode that flies its missions in order and remembers how far the player got.
ui.replace_screen(screen, name) replaces one of the front end's screens, given by its name
(FrontEndScreen), such as "main_menu", with the mod's registered
screen name (Screens). While the front end shows that screen, it runs the mod's screen
in its place: the front end draws the screen's background, the mod's screen draws over it and takes
the keys, and the pointer is drawn on top. ui.pointer gives the pointer's place and whether its
left button is down.
local ui = require("openreliant.ui")
local screen = ui.register_screen("main_menu", {
frame = function()
ui.text(vector.create(ui.width / 2, ui.height / 2, 0), "PRESS ENTER", { align = "center" })
end,
key = function(key, down)
if down and key == "enter" then ui.go_to("pilot_roster") end
if down and key == "escape" then ui.quit() end
end,
})
ui.replace_screen("main_menu", screen)- To move on, the mod's screen asks the front end:
ui.go_to(screen)goes to another of its screens (or to the mod's screen that replaces it),ui.start_game_mode(name)starts a game mode, andui.quit()quits the game. If the front end can't show the screen asked for, nothing happens and the log says so. ui.play_movie(name)plays a Bink movie from the game folder or a mod, such as"thread01.bik", on a cleared screen. Escape or the pointer's right button ends it.ui.replace_screen(screen, nil)gives the screen back to the front end. If the mod's script stops, or its screen's callback fails, the front end shows its own screen again.- These functions are for menu scripts only.
examples/mods/main-menu replaces the main menu.
core.register_game_mode adds a game mode. The main menu then shows a GAME MODES button, which
opens a list of every mod's modes (Front end).
local core = require("openreliant.core")
core.register_game_mode({
name = "arena",
label = "ARENA",
description = "Wave after wave in a Phoenix.",
missions = { 29 },
ship = "phoenix",
loop = true,
})missionsare mission numbers, flown in order. Each is a standard.DTEfile, the game's or a mod's, so a mode doesn't change the mission format. A mod brings a mission of its own as a file such asmission90.dte(How files are replaced). A mode has up to 64 missions.shipis the ship the player flies, one of the game's or a mod's by its qualified name, such as"teapot:teapot". Without it, each mission's own ship is used.- Without
loop, the mode goes back to the main menu after its last mission. Withloop, it starts again from its first mission, until the player leaves a mission from the pause menu. - Leaving a mission from the pause menu always ends the mode.
- Only load and menu scripts register modes, and only as OpenReliant starts. A mod that is off has no modes.
core.game_modegives the qualified name of the mode that runs, such as"arena:arena", and nil otherwise.core.game_mode_missiongives the mission the mode is at: itsnumber, itsplacein the mode from 1, and thecountof the mode's missions.
Every script can read core.game_mode, so a mod's global and player scripts apply its rules only
while its mode runs:
-- rules.luau, a global script: in the arena, the player's hits count double.
local core = require("openreliant.core")
local hooks = require("openreliant.hooks")
hooks.add("object_damage", function(e)
if core.game_mode == "arena:arena" and e.attacker.is_player then
e.value *= 2
end
end)A mode can also change the records for its own missions (The records). records
names a script of the mod that runs as a load script before each of the mode's missions. It can
read core.game_mode_mission to know which mission is next. When the mission ends, the records go
back to what they were. The game's campaign, the other modes and the mode's own briefing screens
never see the changes.
-- menu.luau: a campaign of two missions the mod brings, mission91.dte and mission92.dte.
core.register_game_mode({
name = "prequel",
label = "PREQUEL",
missions = { 91, 92 },
campaign = true,
records = "prequel_records.luau",
})-- prequel_records.luau: in the prequel, the Yakob Shuttle flies as a pirate fighter.
local records = require("openreliant.records")
local shuttle = records.ships.yakob_shuttle
shuttle.max_speed = 300
shuttle.shield_power = 14
shuttle.armor_class = 16
records.ship_types.yakob_shuttle.class = "fighter"
records.text[1104] = "PIRATE"examples/mods/arena adds a game mode with rules and a HUD of its
own, and examples/mods/interceptor,
teapot and bananas each fly a mode
in a mod's ship.
A game mode with campaign = true is a campaign:
- It flies its missions in order. When a mission is lost or left from the pause menu, the restart screen offers to fly it again from its briefing or from its launch, or to go back to the main menu. A mission is lost when the player's ship is destroyed, the pilot is captured or sent home for shooting a friend, or the mission's script rates it a total failure.
- The mission the player has reached is kept in the mod's global storage, in the section
campaigns, under the mode's name without the mod's prefix (Storage). GAME MODES shows it, and the campaign carries on from it the next time. After the last mission, the campaign starts from its first again. A script can set the value, from 0 for the first mission, to move the campaign on or back:storage.global_section("campaigns").tour = 1. - A campaign can't loop.
Any game mode can name a briefing: the name of one of the mod's registered screens, which the
front end shows before each of the mode's missions. The briefing reads core.game_mode_mission
to know which mission is next, flies it with ui.launch_mission(), and ends the mode with
ui.go_to("main_menu"). Without a briefing, the next mission starts at once.
local core = require("openreliant.core")
local ui = require("openreliant.ui")
ui.register_screen("briefing", {
frame = function()
local mission = core.game_mode_mission
ui.text(vector.create(ui.width / 2, 100, 0), `MISSION {mission.place} OF {mission.count}`, { align = "center" })
end,
key = function(key, down)
if down and key == "enter" then ui.launch_mission() end
if down and key == "escape" then ui.go_to("main_menu") end
end,
})
core.register_game_mode({
name = "tour",
label = "FIRST TOUR",
missions = { 1, 2, 3 },
campaign = true,
briefing = "briefing",
})A game mode can also brief its missions in the game's briefing room, as the StarLancer trial
briefs its two: briefing_room = "reliant" or "yamato". Before each mission, after the mod's
briefing screen where the mode has one, the player sees the room's door, then Enriquez at the
room's screen playing the mission's hologram, then the loadout on the same ship, and last her
last_word. The hologram is a Bink file, the game's or the mod's, and the last word a speech
file, such as ms_speech\enrbr_tag01.ut; without one, her animation plays without a line. Give a
mod's movie a name of its own, such as prequel_m01.bik, since a file in a mod replaces the game's
file with the same name everywhere.
The loadout offers what a new pilot gets for the number the mission flies as. It keeps its choices
apart from the campaign's, and starts fresh as the mode starts. A mode can list the ships its
loadout offers instead, in its own order, with loadout_ships, such as { "grendel", "predator" }
or ship type numbers. The loadout then starts on the first, and later on the ship the player chose
last. Ships the player can't fly, such as capital ships, are left out.
A mode's missions fly with a wing of their own, which starts anew as the mode starts, so the
campaign's wing loses nobody to them. In missions flown as the campaign's numbers, 1 to 28, the
game gives the player's wingmen, Alpha 2 to 6, the wing's pilots in place of those the mission file
names. wing_pilots seats other pilots there, each a pilot of the game's by its name, such as
"viper", or its number, or one a mod adds by its qualified name; none keeps a place's pilot. A pilot it lists flies every mission,
even after dying in one.
core.register_game_mode({
name = "prequel",
label = "PREQUEL",
missions = {
{ number = 91, as = 1, hologram = "prequel_m01.bik", last_word = "ms_speech\\enrbr_tag01.ut" },
{ number = 92, as = 2, hologram = "prequel_m02.bik", last_word = "ms_speech\\enrbr_tag02.ut" },
},
campaign = true,
briefing_room = "yamato",
debriefing = true,
-- As the trial does, the Ronin wing's leader, Tanaka, flies Alpha 6 in Viper's place.
wing_pilots = { "none", "none", "none", "none", "ronin_leader" },
})With debriefing = true, the ITAC debriefs each mission that goes on to the next, as the trial's
ITAC does. It opens DEBRIEFINGS alone, with the debriefing of the number the mission flies as for
its rating, and REPLAY MISSION, which flies the mission again from its briefing. The mode's records
still stand then, so its records script can give the debriefing's text (records.itac_text). The
ITAC keeps the mode's missions apart from the campaign's.
An ending names one of the mod's registered screens, which the front end shows after the mode's
last mission. It leaves for the main menu with ui.go_to("main_menu"). Without one, the main menu
follows at once.
The missions are flown as INSTANT ACTION flies its mission: without the game's rooms, medals or saved games (#641).
examples/mods/campaign is a short campaign of the game's first
three missions, with a briefing, a movie and an ending.
A mod can offer the player options, which the player sets on the mods screen: GAME OPTIONS, then
MODS, then OPTIONS with the mod chosen. A load or menu script declares the mod's page as
OpenReliant starts, with openreliant.options, and any script of the mod reads the values:
local options = require("openreliant.options")
options.register_page({
title = "WINGMEN",
options = {
{ label = "THE PANEL", kind = "heading" },
{ key = "show_panel", label = "SHOW PANEL", kind = "toggle", default = true },
{ label = "IN A FIGHT", kind = "heading" },
{ key = "pull_out_below", label = "PULL OUT BELOW", kind = "choice", default = 0.3,
choices = { { value = 0.2, label = "20%" }, { value = 0.3, label = "30%" } } },
{ key = "rejoin_after", label = "REJOIN AFTER", kind = "number",
min = 5, max = 60, step = 5, default = 20,
description = "The seconds a wingman stays out of the fight." },
{ key = "panel_reach", label = "PANEL REACH", kind = "slider",
min = 5000, max = 100000, step = 5000, default = 50000 },
{ key = "panel_title", label = "PANEL TITLE", kind = "text", default = "WINGMEN" },
},
})
local rejoin_after = options.get("rejoin_after")- A
"toggle"is a check box, with a boolean default. A"choice"steps through itschoices, each a number or a stringvaluewith thelabelthe screen shows, and its default is one of the values. A"number"steps frommintomaxbystep, and its default is in the range. A"slider"is a number set by dragging a knob, for a wide range: it takes the same fields, and the knob stops on the steps. A"text"is a line the player types in a box, of up to 24 characters, with a string default: a click in the box starts typing, Enter or a click elsewhere keeps the line, and Escape puts it back. Each of these needs akey, which scripts read it by. - A
"heading"has only alabel, which the list writes in white over the options after it, to split a long page. - An option's
descriptionshows under the list while the pointer is on it. - A page has up to 64 options, a choice up to 32 choices, and a mod one page. A mistake in the page is an error in the script that declares it.
- Only load and menu scripts declare a page, and only as OpenReliant starts: the pages are fixed before the front end shows. A mod that is off has no page until it's turned on and OpenReliant has restarted.
options.get(key)gives what the player set, or the default. A value that no longer suits the option, such as a choice the mod has since dropped, reads as the default.options.set(key, value)sets one of the mod's own options, as the player does on the mods screen, such as from a key the mod binds. A number is held to its range, and a value that doesn't suit the option otherwise is an error.- The player sets options in the front end, before a game starts. A script that runs in a game
reads them as it starts. A menu script can also hear a change at once, with the engine handler
on_option_changed(key, value). - The values are kept in the mod's global storage, in a section of its own that
storage.global_sectiondoesn't open. A value that is the default isn't kept. - A key the player sets is an action the mod registers, which the controls screen binds (Keys and actions), rather than an option.
examples/mods/wingmen offers five options under two headings.
A player script can draw a post effect over the whole frame: a GLSL fragment shader from its mod,
registered with openreliant.postprocessing.
local post = require("openreliant.postprocessing")
post.register({
name = "crt",
shader = "crt.frag",
stage = "after_hud",
order = 0,
parameters = { 0.3, 0.08, 0.6 },
})
post.set_parameters("crt", { 0.5, 0.08, 0.6 })
post.set_enabled("crt", false)stageis"before_hud"(the default), which draws over the scene before the flight display and the menus are drawn, or"after_hud", which draws over everything.- Within a stage, effects draw by
order, lowest first. Effects with the same order draw in the order they were registered. Each effect reads what the one before it drew. parametersholds up to four numbers, which the shader reads. Those left out are 0.enabled = falseregisters the effect turned off, forset_enabledto turn on later.registerreturns the effect's qualified name, such ascrt:crt.set_enabledandset_parameterstake the effect's own name or the qualified one.- The shader compiles when the script registers it. A shader that doesn't compile is an error in the script, with the file and the line.
- A shader can take in another file of its mod with
#include "name.glsl"on a line of its own, such as helpers that several shaders share. An included file can include others, and each file is taken in once, so two files can include the same helpers. A mistake in an included file names that file and its line, and so does an#includeof a file the mod doesn't have. A shader and the files it includes hold at most 7#includelines. - An effect is removed when the script that registered it stops. If a script fails to load, the effects it registered are removed.
- The mods can register at most 64 effects at once.
- Compiled shaders are kept in the game folder's
cache/shaders, so a shader compiles again only when it or OpenReliant's shader compiler changes. The folder can be deleted at any time. - In the developer mode, saving a folder mod's shader reloads its scripts, which compiles the shader again (Reloading).
- MOD EFFECTS on the VIDEO tab,
ModEffectsinstarlancer.iniand--no-mod-effectsturn all the mods' shaders off: their post effects, and their surface and lighting functions. Choosing a GRAPHICS preset doesn't change it.
The shader is GLSL 450, and reads:
#version 450
// The frame as the effects before this one left it.
layout(set = 2, binding = 0) uniform sampler2D source;
// The frame before any effect.
layout(set = 2, binding = 1) uniform sampler2D frame_image;
layout(set = 3, binding = 0, std140) uniform Frame {
vec4 size_time; // x and y: the frame's size in pixels; z: the seconds passed
vec4 parameters; // the script's numbers
} frame;
layout(location = 0) in vec2 uv; // 0 to 1 across and down the frame
layout(location = 0) out vec4 color;
void main() {
color = texture(source, uv);
}A shader file's name ends in .frag or .glsl. Like scripts, shader files belong to the mod and
don't replace game files. examples/mods/crt draws an old curved
monitor over the game: Shift F8 turns it on and off, and Shift F7 changes the scanlines.
A player script can change how surfaces are lit, with GLSL functions from its mod, registered with
openreliant.shaders. OpenReliant compiles each into a variant of its own surface shader.
- A surface function changes a pixel's colour, normal, roughness, metalness, glow and alpha
before it is lit. It applies to the textures it lists, by the names the models use, such as
Pred_cp01, in any case. Witheverywhere = true, it also applies to every lit surface in the scene that has no surface function of its own.object:set_surface(name, parameters)gives one object's whole model a surface function, which wins over the functions on its textures, andobject:set_surface(nil)takes it away. - A lighting function changes how much of each light reaches a pixel, on every surface lit for each pixel. One draws at a time: the enabled one registered last.
local shaders = require("openreliant.shaders")
local self = require("openreliant.self")
shaders.register_lighting({ name = "bands", shader = "bands.glsl", parameters = { 3 } })
shaders.register_surface({
name = "ink",
shader = "ink.glsl",
textures = { "Pred_cp01" },
everywhere = false,
parameters = { 3, 8 },
})
shaders.set_enabled("bands", false)
return {
engine_handlers = {
on_mission_start = function()
-- The player's ship, which is there once a mission has started.
self:set_surface("ink", { 2, 8 })
end,
},
}The file holds the function alone, without #version, and can define helpers before it, or take
them in from another file of its mod with #include, as a post effect's shader can. A surface
function is called surface, and a lighting function lighting:
// The pixel, which the function reads and sets.
struct Surface {
vec3 color; // the texture's colour, in its sRGB encoding
float alpha;
vec3 normal; // in camera space, unit length; zero for an unlit pixel
float roughness; // 1 and 0 where the texture has no material maps
float metallic;
vec3 glow; // emitted light, added after lighting; starts as the emissive map's
vec3 sunlight; // how much of the sun lights it, in each channel; starts as OpenReliant's own
vec3 scattered; // sunlight that haze in front of it scatters toward the eye; starts as none
vec2 uv; // read only: the texture coordinates
vec3 position; // read only: its position in camera space
vec3 toEye; // read only: the direction toward the eye
vec2 frameSize; // read only: the frame's width and height in pixels
vec3 toSun; // read only: the direction toward the sun, unit length; zero where it doesn't reach
vec3 local; // read only: its position in its object's own frame, as the model holds it
float bloom; // how much of its light blooms, from 0 to 1; starts as the registration's `bloom`
};
void surface(inout Surface s, vec4 parameters, float time) {
s.glow = vec3(0.0, 0.2, 0.0) * (0.5 + 0.5 * sin(time));
}
// cosine: from 0 to 1, between the pixel's normal and the light. Returns how much of the light
// reaches the pixel; `return cosine;` changes nothing.
float lighting(float cosine, vec4 parameters) {
return ceil(cosine * parameters.x) / parameters.x;
}timeis the seconds passed, andparametersthe script's numbers. Those left out are 0.s.frameSizeis the size of the frame the pixel is drawn into, whichgl_FragCoordcounts in. An effect in screen space, such as scan lines, divides by it to look the same at any resolution:gl_FragCoord.y / s.frameSize.yruns from 0 to 1 up the frame.s.toSunpoints toward the sun: the light that casts shadows, which is the only light that reaches a planet. It needs PER-PIXEL LIGHTING, which gives each pixel its normal, and is zero without it.s.sunlightstarts as how much of it OpenReliant lets through, the cosine between the normal andtoSun, which a planet's atmosphere carries a little way past the terminator. Set it to light the pixel by a law of your own, such as one measured for a planet, or to shade it, as a ring shades its planet; the shadows of other objects still fall on it. Where the function leaves it as it was, the sun lights the pixel at the normal the function leaves.s.scatteredis sunlight that haze or air in front of the pixel scatters toward the eye, such as a planet's haze over its disk. Each channel is a share of the light that a white surface facing the sun sends back. The sun lights it with its colour and strength, and it is added to the pixel's light in linear light.s.glowis added to the encoded colour instead, which over a lit surface gives more light than adding the light itself.s.localis where the pixel stands in its object's own frame, in the units and axes of the object's model.objectDirection(v)turns a direction such ass.toSun,s.normalors.toEyeinto that frame, andobjectPoint(p)a point such ass.position. With them, a function can work out shapes the model holds, such as where a planet's ring shades the planet.s.bloomis how much of the pixel's light blooms, from 0 to 1. It starts as the function'sbloom, 1 unless the registration gives one. Lower it for a glow or a haze that shouldn't glare like a light. It counts where the surface is drawn solid or added over what lies behind it, not where it blends by its alpha, and only while BLOOM is on. A bright thing behind the surface still blooms in full.- The functions can call the shader's own helpers, such as
encodedanddecoded, which turn a colour into and out of linear light. - If the function sets roughness or metalness on a surface without material maps, the surface is lit as a material (Material maps).
- Alpha shows on surfaces the game blends, such as glass and effects. A function registered with
see_through = truealso makes the solid surfaces it draws on objects and textures blend by the alpha it sets, sorted with the game's other blended draws. They still write depth, as a solid model does, so that of two models that cut into each other the nearer hides the other. Setwrites_depth = falsefor something with no solid shape, such as a glow or a cloud. A function that applieseverywhereleaves the surfaces solid. - A lighting function only changes surfaces lit for each pixel, with PER-PIXEL LIGHTING. It changes the light falling on them, not their highlights.
- Give helper functions names unique to your mod. The lighting function and a surface function can come from different mods, and they are compiled into one shader. If they don't compile together, or the GPU can't draw with them, the log says so, and that surface function's surfaces draw without it.
- Each registration compiles the function on its own, and a mistake in it is an error in the script, with the file and the line. Compiled variants are kept in the shader cache.
enabled = falseregisters a function turned off.- The rules for names, removal, reloading and MOD EFFECTS are those of post effects. The mods can register at most 64 functions at once.
examples/mods/cel-shading draws the ships as a cartoon: a
lighting function makes each light fall in flat bands, and a surface function on every lit surface
draws a dark line round the outlines. Shift F6 turns it on and off, and Shift F5 changes the
number of bands.
A load script can have one of its mod's surface functions draw a planet's atmosphere in place of the band, by the function's own name or the qualified one, with up to four numbers it reads (Planets):
local records = require("openreliant.records")
for n = 1, #records.planets do
local planet = records.planets[n]
if planet.type == "titan_hi" then
planet.atmosphere_surface = "haze"
planet.atmosphere_parameters = { 0.02 }
planet.atmosphere_inner = 0.95
planet.atmosphere_outer = 1.15
end
endA player script registers the function, as it registers any surface function, and each mission looks
it up by its name as it makes the planet. The function draws on a ring that faces the camera through
the middle of the planet's sphere, from atmosphere_inner to atmosphere_outer times the sphere's
radius as the camera sees them, and its light is added to what lies behind it, such as the sun's
glow, in linear light, as light adds. The function works in the planet's own frame, as the planet's
model holds it, with the sphere's middle as the origin and the sphere's radius as the unit, so it
can trace the line of sight through each pixel:
void surface(inout Surface s, vec4 parameters, float time) {
// The camera, the line of sight through the pixel and the sun, in the planet's radii.
vec3 eye = objectPoint(vec3(0.0));
vec3 ray = normalize(s.local - eye);
vec3 sun = objectDirection(s.toSun);
// Where the line passes nearest the middle, and how high above the sphere that is.
vec3 nearest = eye - dot(eye, ray) * ray;
float height = length(nearest) - 1.0;
// A haze that thins with height, on the side toward the sun. It ends a pixel inside the
// planet's edge rather than at it, so that the edge's pixels, which show the planet and the sky
// around it, get it too.
float lit = max(dot(normalize(nearest), sun), 0.0);
float edge = clamp(1.0 + height / fwidth(height), 0.0, 1.0);
s.color = vec3(0.0);
s.sunlight = vec3(0.0);
s.scattered = vec3(0.3, 0.5, 1.0) * exp(-max(height, 0.0) / parameters.x) * lit * edge;
}- For a flattened planet, the unit is its equatorial radius. Its north pole is the frame's -Y, as in the planet's model, so a function can draw its haze round the flattened shape.
- The sphere is half the widest span of the model's first part, so for a model that holds rings,
such as Saturn's, the unit is the rings' outer edge, and
atmosphere_innerandatmosphere_outerare shares of that. - A bright haze glares with the bloom like any bright light. Register its function with a lower
bloomto keep more of it as it is. - The ring is drawn behind the planet, which hides it within the planet's edge. A sharp cut at the edge leaves the edge's pixels without the haze, as the function runs once for each pixel, at its middle.
- Haze in front of the planet's disk belongs in a surface function on the planet's texture, which
works out the same line of sight from
s.localand setss.scatteredas the ring's function does. The sun lights both alike, so they meet at the edge. - The band's colour isn't used where a function draws the atmosphere. A name no mod registers leaves the planet its band, and the log says so.
- The ring isn't drawn while the camera stands within its outer edge.
A mod can replace one of OpenReliant's shaders with its own, without a script: a file at the top level of the mod with the same name as one of OpenReliant's shaders.
| File | What it draws |
|---|---|
device.glsl |
The scene, the flight display and the menus |
bloom.glsl |
The bloom, the frame's finish and the gamma ramp, and the vertex stage of post effects |
shadow.glsl |
The shadow maps |
Start from OpenReliant's own, in src/platform/shaders of the
version you play. A replacement is at your mod's risk: OpenReliant's shaders change between
versions, and a replacement made for one can stop fitting the next.
- Each file holds a vertex stage and a fragment stage, which it picks with
#ifdef VERTEXand#ifdef FRAGMENT, as OpenReliant's do.#include "color.glsl"takes the mod's owncolor.glsl, or OpenReliant's where it has none. Other includes are errors. - The last mod in the load order that has the file replaces OpenReliant's.
- Both stages compile as OpenReliant starts, through the shader cache, and are checked against OpenReliant's own. A replacement may only use textures and uniform blocks that OpenReliant's shader binds, with the same types and no larger. It may only read inputs that OpenReliant's shader reads, and it must write every output that OpenReliant's shader writes. A fragment stage writes no others, and the fragment stage reads only what the vertex stage writes.
- A replacement that doesn't compile or doesn't fit is left out, and OpenReliant uses its own shader instead. The log says which file, which stage, and why, with the line of a compile error.
- A replaced
device.glslis also the shader the mods' surface and lighting functions are compiled into. Keep its hooks (MOD_SURFACE,MOD_LIGHTING) and the line// mod_functionsfor them; without that line the functions draw nothing, and the log says so. - Replacements are chosen as OpenReliant starts, while MOD EFFECTS is on. Changing MOD EFFECTS takes effect for them at the next start.
openreliant.vfs reads files. A file comes as a string of its bytes.
vfs.read(name)reads a file the way the game does. It looks in three places in turn: the latest mod's copy, the file in the game folder, and the game'sresource.hog.- A mod's file is found by its name alone, so
missions\mission1.dtefinds a mod'smission1.dte. - The game folder's file is found by its path from the game folder, in any case, such as
missions\mission1.dteormusic\theme.wav. A name without a folder, such aspalette.tga, is looked up in the game folder itself, then in the archive.
- A mod's file is found by its name alone, so
vfs.read_mod(name)reads a file of the calling mod.- Both return nil if there is no such file, or if the name is a folder.
vfs.exists(name)says whethervfs.readwould find it. - A script can read a file of up to what its mod's scripts have left of their memory. A bigger file is an error.
The developer mode turns on the tools for writing scripts: the console, and reloading. Turn it on
with DeveloperMode=1 in the [OpenReliant] section of starlancer.ini, or with
--developer-mode (Configuration); it's off by default.
F11 then brings up the console, in the menus, the Reliant's rooms and the briefing, and in flight, where any mod has scripts. It pauses the mission, and F11, Escape or CLOSE takes it away again. It shows what the scripts print and their errors, and runs the lines typed into it:
| Command | What it does |
|---|---|
help |
Lists the commands |
help <name> |
What a package (storage or openreliant.storage), an engine handler (on_update) or a hook (object_damage) is |
mods |
Lists the mods, and their scripts that run |
reload |
Reads the folder mods' scripts again, and starts them again from where they were |
clear |
Empties the console |
global <mod>, player <mod>, menu <mod> |
Runs Luau with the mod's global, player or menu scripts, until exit |
> global wingmen
wingmen global> player = require("openreliant.world").player
wingmen global> player.hull, player.speed
0.8 120
wingmen global> require("openreliant.interfaces").Wingmen.pulled_out()
2
wingmen global> exit
- In Luau, a line runs as an expression where it is one, and its values are shown; otherwise it
runs as statements. Variables you set stay for the next line, but they are separate from the
scripts' globals.
requiregives the mod's modules as its scripts have them, and the packages its scripts can use. - Enter or RUN runs the line, Up and Down bring back the lines typed before, and Page Up, Page Down, the mouse wheel and the arrows scroll the output.
- Any other line goes to the player and menu scripts'
on_console_command(text), so that a mod can add commands of its own.
In the developer mode, a folder mod's scripts reload as soon as one of them or one of its shaders
is saved, as reload reloads them:
- Global and player scripts start again from their state, as a saved game would keep it
(Saved games):
on_saveruns, and the new scripts geton_load. - Partway through a mission, its mission scripts start again, and so do the scripts on each of its
objects, with
on_initand thenon_added, but noon_mission_startoron_object_added. - Menu scripts start again with
on_init. - What the scripts registered (views, displays, screens, actions, orders, effects) goes away and is registered again as they start, so a changed shader compiles and draws (Post effects).
- Load scripts and
mod.iniare read only as OpenReliant starts, so they need a restart. So does a file added to a folder mod.
luau-lsp, the Luau language server, gives editors such as VS Code completion and type checks:
-
Install its extension, and set its platform to Standard (
luau-lsp.platform.type). -
Add
openreliant.d.luauto its definition files (luau-lsp.types.definitionFiles). It comes with each release, and is indocs/guide. -
Give each package's variable its type:
local hooks: Hooks = require("openreliant.hooks")
The editor then knows every hook with the fields of its e, and the fields of objects and records.
It may report that it can't find the packages themselves, which OpenReliant provides.
Scripts run in a sandbox: they can't use the network or run programs, and the only files they can
read are the game's and the mods' (Files). Luau's getfenv and setfenv aren't there,
so no mod can reach another's globals, and getmetatable on a handle or a package gives "The
metatable is locked". An error in a script never stops the game: it's logged with the file and the
line, and the game carries on.
| What | Limit |
|---|---|
| A call into a script | 1 second in a load script, and 100 milliseconds in any other |
| A mod's scripts' memory | 64 MiB |
| A file a script reads | What the mod's scripts have left of that memory (Files) |
| A mod's storage | Just under 15 MiB in each place, the game's and the global (Storage) |
| The state kept with a saved game | 256 MiB, all mods together. The mods that keep the most are left out until the rest fits (Saved games) |
| Drawing each frame | 4096 things and 64 KiB of text over the flight display (hud and debug together), and as much over the menus (ui). What's past that isn't drawn |
| The pictures and fonts scripts draw | 128 files and 128 MiB at once, all mods together, the pictures drawn longest ago making room for new ones (Pictures, shapes and fonts) |
| Camera views, HUD displays and screens | About 200 together in one run of OpenReliant, counting the ones registered before a reload (Camera views) |
| Post effects | 64 at once, all mods together (Post effects) |
| Surface and lighting functions | 64 at once, all mods together (Surface and lighting functions) |
| A mod's options | One page of up to 64 options, a choice of up to 32 choices, and a text of up to 24 characters (Options) |
| A game mode's missions | 64 (Game modes) |
| An object's parts and attachment points | object:parts() lists up to 256 parts, and object:attachments() up to 512 attachment points. The rest are left out (A ship's parts) |
| In the log | What to check |
|---|---|
skipping the mod ...: it needs OpenReliant 0.7.0 |
OpenReliant is older than the mod's OpenReliant= |
No started line for the script |
mod.ini lists it under [Scripts], with the file's exact name |
there's no hook named '...' |
The hook's name (the reference) |
... has no field '...' or expected a number, got string |
The field's name and type |
e can only be used while its handler runs |
Keep values from e in variables, not e itself |
... is an event, whose fields can't be changed |
Events can only be read |
... concerns no object to filter by |
That hook only takes a function as its filter |
the object is no longer in the mission |
object:is_valid() before using a handle kept from earlier |
script timed out |
A loop that doesn't end, or does too much at once. A handler that runs out of time three times in a row is removed |
storage: the mod's ... sections would take more than |
The mod keeps too much: take fields out of its sections |
too much data to keep |
A value that holds the same tables over and over, or too much data at once |
... is not available to load scripts |
Load scripts can't use that package |
object scripts can't change this object's ... |
An object's scripts can only change their own object |
only plain data can be passed on |
An event's or a timer's data holds a function or something else that isn't plain data |
only global scripts can add scripts |
Start object scripts from a global script, or list them in mod.ini |
hud is only drawn while it's shown |
Check hud.shown or ui.shown before drawing or reading its size, and draw in on_frame |
only plain data can be kept |
What on_save returns, or a value stored in a section, holds a function or something else that isn't plain data |
a timer names ..., which no script registered |
async.register_timer runs as the script starts, before a timer can fire |
player scripts can only read a game section |
Change game sections from a global or object script |
orders.register requires a global script |
Register orders from a global or mission script |
From OpenReliant 0.8 on, what scripts see stays stable, so that a mod keeps working as OpenReliant is updated. That is everything the reference lists, and what this page describes of the mods' shaders:
- the packages, with their functions, fields and types;
- the engine handlers, and what they're passed;
- the hooks, their fields and their results;
- the fields and methods of objects, missiles, turrets and the records;
- the names of values, such as
"cockpit"for a view or"laser_cannon"for a gun; - the manifest's keys and the kinds of scripts;
- what post effects and surface and lighting functions read and write.
A replacement for one of OpenReliant's own shaders isn't covered (Replacing OpenReliant's shaders).
New versions add to all of this: packages, functions, fields, hooks, handlers and values. A mod
that uses something new sets OpenReliant= in its manifest to the version that added it, so that
an older OpenReliant skips the mod and says so, instead of running scripts that fail. A script can
also read core.version and use a feature only where it's there.
A script that reads a value from a list, such as an object's type, can meet one it doesn't know: one a mod adds, or one a later version adds. A value without a name comes as its number, and a later version can give it a name.
When something has to change, it's deprecated first: the old name keeps working next to the new one until at least the next minor version, and the reference marks it and names what replaces it. The release notes list each deprecation and each removal.