Live objects¶
The ships, stations, gates, missiles and markers of a running mission. Each is a 0xB98-byte object
from gameobj.cpp, and each embeds the root of a hierarchy of nodes standing for the parts of its
model. The layouts are defined in gameobj.zig,
objects.zig, create.zig
and srapiext.zig, and make
ghidra-annotate applies them to the Ghidra project with the names used here.
The object array¶
game_objects (0x587CE0) holds 400 object pointers, which the engine calls the GO array. No
slot is ever empty. As a mission starts, objects_reset (0x00466630) gives every slot a
stand-in of type 1001, flagged stand_in and not created, sets game_object_count (0x539AA0) to
0, and forgets every ship type's objects and model. create_object (0x00466C10) fills a slot:
the one it is given, such as a mission ship's index among the mission's ship records, or for -1 the
next, which counts game_object_count up. It stops the game with a fatal error past the last slot
or for a slot filled already. object_reset (0x004688B0) pops a slot's orders and puts a new
stand-in in it, flagged 0x3C.
player_slots (0x58832C) counts the slots from the first that belong to players, one in a
single-player game, and player_index (0x5883FA) is the player's own, the first in a
single-player game.
Every loop over the objects walks the slots from the first up to game_object_count, then the
cutaway slot (0x57E04E), which the mission's start sets to 399, the last, reading the count afresh
at every slot. The loops pass over objects by their flags: the simulation's updates and
objects_update skip stand_in and disabled ones, and mission_frame's framing and drawing
jumping ones as well. Improvement: with every slot handed out, the cutaway slot comes round
again and again, and the game's loops never end; OpenReliant walks it once.
| Offset | Size | Field |
|---|---|---|
0x000 |
4 | Type: the ship's record in shipstats.bin. Types above 255, markers and nav points among them, have no stats, but for the ship types mods add in OpenReliant, from 256 (Ship types) |
0x004 |
4 | Slot in game_objects |
0x008 |
4 | Flags |
0x010 |
4 | The type's entry in ship_combat_stats |
0x014 |
4 | The type's entry in ship_flight_stats, or a missile's in missile_flight_stats |
0x018 |
4 | The type's model, as loaded |
0x01C |
4 | Data kept for the type and shared by its objects |
0x020 |
4 | The renderer's object for it, or null |
0x024 |
2 | The shape the wing status window shows it by; 0 for none, as created |
0x028 |
0x104 |
The root node of its model hierarchy |
0x150 |
2 | Missile racks fitted |
0x152 |
2 | Components listed |
0x158 |
0xF0 |
Up to 20 missile racks, 12 bytes each |
0x248 |
0x2D0 |
Up to 60 components, 12 bytes each |
0x5D0 |
4 | Engines in its model: parts of subsystem class 5 |
0x5D4 |
4 | The share of its engines left: 1.0 when created, less 1 / engines for each one destroyed |
0x5E8 |
4 | Afterburner fuel: 100 * afterburner_fuel from its stats when created, or zero in one of the game's modes; 5000 more for each fuel pod |
0x5EC |
2 | Countermeasures left: 29 when created |
0x5F0 |
16 | Shields: four values, each 6 * shield_power - 1 when created |
0x600 |
16 | Armor: four values, each 6 * armor_class - 1 when created |
0x634 |
2 | Where its lights stand in their blinks, in ticks added to the mission's clock: rand() over its largest value, times 100 and truncated, when allocated (object_alloc, 0x00475DD0) |
0x618 |
8 | The slots of two objects it passes through: the collision sweep tests no pair where either names the other. -1 when created |
0x644 |
4 | Its side: 0 friendly, 1 hostile, 2 neutral. Its type's when created; SetHostile makes it hostile or friendly |
0x648 |
4 | The tier a re-arm fits its racks by; nothing writes it |
0x64C |
4 | Set while a missile homes on it: mission_frame clears it on every object, and missiles_update sets it |
0x658, 0x65C |
8 | Its smoke's template and emitter, or null for none |
0x660 |
1 | Its smoke's level, 0 to 3, by its damage |
0x664 |
4 | The shields' condition, how well they recharge as the armor wears: 1.0 when created |
0x668 |
4 | The cruise speed's condition, which object_cruise_speed scales the speed by: 1.0 when created |
0x66C |
4 | The guns' condition: 1.0 when created. The guns recharge by it, and below 0.9 each shot goes off only as often as it plus a tenth (guns_step, 0x004770E0) |
0x680 to 0x697 |
Its orders: the stack and what the current order keeps, the damage it has taken lately and its last attacker | |
0x70C |
4 | Its pilot's eject roll, 0 to 99 from object_random15 when created: where a mission lets the pilot eject, it does below 40 (Destruction). The WillsBlag command sets it to 100 |
0x728 |
12 | The power distribution's point on the power ball: (1, 1, 1) when created |
0x734 |
4 | The guns' share of the power as a factor on how fast they recharge: 1.0 when created |
0x73C |
4 | The shields' share of the power as a factor on how fast they recharge: 1.0 when created |
0x740 |
4 | Its pilot, a record of pilotstats.bin (object_set_pilot, 0x0049CCE0) |
0x748 |
4 | The pilot's entry in pilot_stats |
0x74C |
2 | The wing a mission lists it in: 0 the player's, 1 and 2 two more, 0xFFFF none, as created (The wings) |
0x754 |
4 | The deathmatch power-up it holds (gameobj.PowerUp), -1 for none: a record of the table at 0x0050C510 |
0x75C |
4 | The frame the power-up runs out at, or -1 for never |
0x760 |
4 | The frame the power-up was handed out at |
0xB8C, 0xB90 |
8 | Orders from other players waiting for their frame, in a multiplayer game |
0xB94 |
1 | Set once create_object has filled the slot |
0xB95 |
1 | Nonzero while invulnerable: SetInvulnerability |
0xB96 |
2 | The 3D voice it holds, 0xFFFF for none |
Creating an object¶
create_object(slot, type, tier, x, y, z) fills the slot's object and returns the slot. It clears
the flags and sets the object up at rest at the place given, facing along the world's Z axis, with
no orders and no attacker, motion_forward as its motion, its armor whole and its own seed drawn
from rand(). A type above 255 stops there, as a stand-in for a marker or a nav point: flagged
0x3C, with a radius of 4000 and no shields or armor.
A few types are another ship under a number of their own: the Krasnaya (0x35, 0xDB and 0xDC,
for 0x78), the Kiev (0x36, 0x40, 0xDD and 0xDE, for 0xC2), the Mitchell (0xA0, for
0x13), the Zakov (0xA1, 0xA2 and 0xE2, for 0xB0), the Kestrel (0xDA, for 0x0F) and the
Mammoth (0xE3 to 0xEF, for 0x21). Such a type takes the other's flight model and combat stats
into its own entries, keeping its gun groups and its name, and its object takes the other's number
once it is made.
The object points at its type's stats and takes the type's side from them. The type's model is
loaded with its first object (ship_type_load, 0x00466740), with the type's schematic as its
data. OpenReliant's create/library.zig reads each
type's model and the models its attachment points mount once, and lets a type's go once no object is
of it. Each part of the model gets a node that plays its startup track from the start at 4 a step;
a part of class 6 gives the object shield_generator, one of class 5 counts as an engine, and an
attachment of kind 6 sets flag 0x2000000. The parts are then linked, each posed as its track has
it at the start, and the object's origin moves to their centre of mass (object_link_parts). A
piece of debris takes a tenth of its mass, and a mine a radius of 2000.
Then come the pilot, record 66 of pilotstats.bin for the Coalition's types and 0 for the rest;
each quadrant's shields and armor full, 6 * shield_power - 1 and 6 * armor_class - 1, and the
conditions that armor gives (Shields); for a model that lists no components the
shield's effect and, in every slot past the players', the ecm flag, and for one that does, the
components and attached flags; the afterburner's fuel, 100 * afterburner_fuel, and 29
countermeasures; the power shared evenly. The gun and component counts are cleared, then the
components are listed and the guns fitted from the model's muzzles, which sets the gun
count again; then come the guns' charge and rounds, their groups and the gun mode, the loadout, and
targetable, where the type allows it. Capital ships, planets, gates,
asteroids and a few other types get more set up for their kind.
The planets, types 0x5F to 0x69 and 0xC9 to 0xD3, first make their atmosphere where they
have one (Backdrop); then each planet is set up alike
(0x00467BAB). The object's flags gain components, no_collisions and frozen. Each node hanging
from the root has its scene object's flags set to 0x101100, lit, always drawn and bit 20, and
its light mask to 0x37, so that only the second key light, mask 0x08, reaches it, and its
offset from the root (+0x3C) cleared, so that it stands at the object's origin. Each of its
levels is moved to stand on the middle of its vertices, their sum over their count, and its
polygons' planes, its vertices' normals and its bounds are worked out again; the node takes the
radius of its last level. A planet's part stands off its model's origin, Neptune's some 218000 back
along Z, and none has a mass to move the origin to, so it is this that brings each sphere to its
object's place. Titan's parts, 0x63 and 0xCD, are also given the routine of the environment
effect Planet Bombard (0x0046A300). Unknown: what bit 20 of the flags does.
The words of each ship_combat_stats entry from +0x1C on come from the executable rather than
from shipstats.bin: the gun groups, which the gun code fills in at run time (gun_groups_build,
0x004667F0), then whether the type can be targeted, the string that names it, its class and its
side. make combat-tables transcribes them into
create/combat.zig.
| Class | What it is |
|---|---|
| 1 | Fighters: the player's ships, their twins, and the Coalition's fighters |
| 2 | Capital ships and their wrecks, and other large bodies such as asteroids |
| 3 | Bombers, transports, tugs, escape pods and some stations |
| 4 | Gates, containers, satellites, beacons, pods, rock chunks and the like |
| 5 | Torpedoes |
| 6 | Debris |
| 7 | The proximity mine |
| 8 | Planets |
A type's side is 0 for the Alliance's, which start friendly, 1 for the Coalition's, which start hostile, and 2 for the rest, which are neutral. Two objects on different sides are enemies.
create.zig ports create_object as createObject, and
Objects is OpenReliant's GO array: each slot the object's record, and what OpenReliant keeps
beside it where the record holds the original's pointers. It fits the guns and their groups
(Guns), and the racks from the player's loadout or by the loadout tier
(Missiles). Not ported yet: the special types but the wrecks and the
planets, which planetMade sets up once the mission has made the planet and its atmosphere, and
Titan's Planet Bombard (#233); and the
multiplayer cases.
Flags¶
The word at 0x008 is a GameObject.Flags. Many of its bits are what the script's Disable
commands and their like set; the names in quotes are the developers' labels for their arguments.
| Bit | Name | Meaning |
|---|---|---|
0x2 |
components |
Its components are listed, as its model's header asks. The collision code treats such objects apart. |
0x4 |
no_collisions |
The collision sweep of objects_update leaves it out. |
0x8 |
unpowered |
object_move runs no motion function for it, so it drifts; knocks still move it. Set while it is disrupted and once it is wrecked, and with 0x10 during gate jumps and warps and by object_reset. |
0x10 |
frozen |
object_move isn't run for it. |
0x20 |
stand_in |
Set on objects of types above 255, such as the type-1001 stand-in an empty slot holds. The per-object loops skip them. |
0x40 |
exploding |
Set as it starts to explode (object_destroyed). It takes no more orders. |
0x80 |
can_reverse |
Reverse thrust works only while it is set. Set for a ship with an engine glow that burns forward (node_mount_glow). |
0x100 |
cloaked |
Set by object_cloak (0x00463640), which posts the Cloaked event (The cloak). |
0x200 |
targetable |
SetTargetable for the whole object, which sets it only when the word at +0x24 of its combat stats is nonzero. |
0x400 |
disabled |
Not processed: DisableObject, "Stops entities from being processed", and DisableObjectAtNextJump at the next jump. |
0x800 |
ejected |
Set once its pilot ejects. It takes no more orders, and destroying it now makes it explode. |
0x1000 |
tractored |
Set while a ship's Scoop Up claims it, so no other ship takes it in (Ejection). |
0x2000 |
lights_disabled |
DisableLights. |
0x4000 |
shield_generator |
It has a shield generator, which destroying the part clears. |
0x8000 |
guns_disabled |
DisableGuns. orders_update skips 0x0047C950 for it. |
0x10000 |
missiles_disabled |
DisableMissiles. |
0x20000 |
engines_disabled |
DisableEngines. object_orders holds its throttle at zero and stops both burns. |
0x40000 |
eject_disabled |
DisableEject. The player cannot eject. |
0x80000 |
do_not_disturb |
DoNotDisturb, "dont disturb", which the command describes as keeping comms from disturbing it. It does not retaliate either. |
0x100000 |
no_avoidance |
SetShipAvoidance with "Disable Avoidance code": the avoidance code passes it over. |
0x200000 |
jumping |
Set during the jump orders, and on what lies in the way of the player's formation as it jumps out until the player's jump ends (Jumps). It cannot fire, the avoidance code passes it over, and so do the frame's passes over the objects. |
0x400000 |
attached |
Set while the Dock and Ripper orders hold it to another object; their ends clear it. |
0x1000000 |
abandoned |
Set on the ship a pilot has left (Ejection): its smoke goes, and its spin-out trails every other bit. |
0x10000000 |
sent_off |
Set as the player's ship is sent home for its friendly fire, into Friendly Fire, and again as that order ends and lands it (Friendly fire); a multiplayer game sends the other players' ships off into Jump Out. It takes no orders while it is set. |
0x20000000 |
listing_disabled |
DisableListing, "stop listing": a torpedo does not make the capital ship lurch (Collisions). |
Unknown: the other bits.
The model hierarchy¶
A node (objects.cpp, node_alloc at 0x004991D0) is 0x104 bytes:
| Offset | Size | Field |
|---|---|---|
0x00 |
4 | Kind: 1 for a model part's node |
0x04 |
4 | Flags, a Node.Flags: 0x1 a next place pending; 0x2 a new place committed this step, 0x4 one the frame hasn't taken up yet, and 0x8 a next place worked out from a pose, which drawing between steps reads; 0x800 animating, or carrying a node that is; 0x20 hidden; 0x40 a component's holder once component_damage (0x004645C0) takes the component's armor below zero; 0x100 listed among the components; 0x400 the base of a turret, whose gun stops for good when the node is destroyed (node_forget, 0x00499BB0); 0x2000 targetable, for the parts whose part flag 0x1000 says so, and changed by SetTargetable. Cycling subtargets (0x00414F90) stops only at components that are targetable and have neither 0x10 nor 0x20 |
0x08 |
4 | Its frame, the transform the renderer uses |
0x14 |
12 | Position, relative to the node it hangs from |
0x20 |
36 | Orientation, a 3x3 matrix, relative likewise |
0x44 |
24 | The pose the committed place came from: angles, then an offset |
0x5C |
12 | The next position, which the next simulation step commits |
0x68 |
36 | The next orientation |
0x8C |
24 | The pose the next place came from |
0xA4 |
4 | The model part it stands for: the part's record as loaded, which starts with the .SHP part record |
0xA8 |
4 | The object that owns it, set in the root |
0xB4 |
4 | How it plays its animation track: 0 not at all, 1 once, 2 looping, 3 back and forth |
0xB8 |
4 | Which of its part's tracks it plays |
0xBC |
4 | Where it is in the track |
0xC0 |
4 | How far it moves on through the track each simulation step |
0xC4 |
12 | The angles the track has it at |
0xD0 |
12 | The offset the track has it at |
0xDC |
12 | Angles a turret turns it by (node_turn), added to the track's |
0xE8 |
4 | A component's counterpart of the object's armor |
0xEC |
4 | The node it hangs from; null for a root |
0xF4 |
4 | Capacity of the child list: 100 once created |
0xF8 |
4 | Children |
0x100 |
4 | The child list |
node_owner (0x00499F20) finds a node's object by climbing to its root. node_world_place
(0x004AD960) and node_next_place (0x004AD8D0) find where a node stands in the world, at its
committed place or at its next: its own place in the node it hangs from, turned and moved by each
parent's in turn, up through a mounted object's root to the ship's. They skip a parent's turn where
its diagonal reads 1, 1 and anything but 1, which no turn does.
A part's node holds no turn: node_add_part (0x00499430) copies the part's position and leaves
the node's orientation the identity it was allocated with. It hides a part whose part flag 0x04
marks it damaged. create_object hangs every part's node from the root (object_add_part,
0x004760C0, which takes the object's centre at 0x524 off the position), then
object_link_parts (0x00476130) hangs each from its parent part's node, keeping it where it is,
and moves the object's origin to its parts' centre of mass (object_recentre, 0x004769F0).
A part keeps its origin in the model whatever it hangs from, so hanging it somewhere else has to
work that origin out again in the new frame, which node_place (0x0049A140) does (see
Animation). object_link_part (0x00476180) runs it through node_animate
(0x00499F40) at time zero, which poses it as the part's first track has it at its start, and
copies the place, but not the pose, into the node and its frame. With no pose the sums cancel and
the part stands where it stood; a part whose first track starts it posed, such as a gun barrel
drawn back, stands so from the start.
The centre of mass:
node_mass_add(0x004764A0) sums over the shown part nodes, a node's children first, the density times the part's first moment about the root: its origin there times its volume, plus its own first moment. Over the sum of density times volume, that is the centre.- The centre is added to the object's centre at
0x524and, turned, to its position, andobject_bounds(0x00476680) takes it off the position of each part hung from the root. object_boundsthen finds the object's bounding box and radius, its farthest vertex from the origin, over the vertices of every part node's current level, and sums its moment of inertia.
An object's root holds the object's place in the world: object_set_position (0x0049B600) and
object_set_orientation (0x0049B650) set it, together with the root's frame and further copies
at 0x768 and 0x798, and mission_ships_sync (0x0045A5F0) copies it into the mission ship's
runtime position.
A frame is Surrender's 0xB4-byte transform, which frame_create (0x004C51C0) allocates with a
name, such as GOroot object for an object's root. It holds a parent frame at +0x10, an
orientation at +0x18 and a position at +0x3C. A part's frame hangs from its parent part's, and
the root frame of an object mounted on an attachment point from the part's.
Animation¶
A part can carry animation tracks, which move it about its place (.SHP
clips): each has a length, a mode it plays in unless
told otherwise, a name, keyframes and events. The loader (model_load, 0x004A44D0) files the last
track named startup, fire and deploy, whatever the case, in three slots on the loaded part
(+0x234 to +0x23C), and a node starts one by its slot (node_play, 0x0049A2D0) or by its name
(node_play_named, 0x0049A340): from a time, unless that is below zero, in a mode, the track's
own for -1, at a speed. Any mode but 0 marks the node and every node it hangs from as animating
(flag 0x800, 0x0049A2A0). create_object plays each part's startup track from its start in
its own mode at a speed of 4, which is how the radar dishes of some capital ships and stations turn
from the start.
node_tree_update, once a simulation step, walks from the object's root into the children that are
animating, not hidden and not flagged 0x80, as the nodes of lights, engine glows and muzzle
flashes are. It keeps them on a stack of 500: a node pushes those of its children, in order, and the
last pushed is visited next. The root's children are every part of the model, whatever part each is
linked to, so a part is visited while its parent part is hidden; a part's own children are the roots
of the models it carries. A node it visits commits its pending place. One that plays no track, or
plays at no speed, loses its mark, which it keeps while it pushes a child that has one. One that
plays moves its time on by its speed and, for a track of some length:
- Once (1): stopping at the end, time and speed then set to the length and zero, or at the start, going backwards.
- Looping (2): past the end, starting again.
- Back and forth (3): out over the length and back over the next, round and round.
It then poses the node for the time (node_animate) and sets off the track's events whose time it
passed: from the whole number the old time rounds to up to, but not including, the new one's, and
for a looping track that went round, from the old time to the end and from zero to the new time.
Going back and forth sets off none. An event of kind 0 fires a shot from each of the part's
muzzles, its nodes of kind 4 (clip_event_muzzles, 0x0047C7B0), which is how an aimed
turret fires; one of kind 2 puffs particles from its attachments of kind 7
(0x0047C800); the update knows no others. A track of no length, or a mode past 3, sets off the
events of whatever span the last node visited left.
node_animate takes the pose between the keyframes either side of the time, in a straight line,
from no pose at time zero before the first, and holds the last past them all. node_place then
takes off the pose's angles about any axis whose flag the part has at +0xC8, adds the turret's
(+0xDC), and works out the next place: the part's origin plus the offset, less the origin of its
parent's part or the object's centre, turned about the part's mount point by
Oᵀ · R · O, where O is the part's orientation and R the turn mat3_from_angles makes of the
angles. The part turns in its own frame, and with no angles stays unturned.
node_turn (0x0049B520) turns a node by angles of its own, as a turret turns:
it adds them to +0xDC and marks the node animating. About each axis whose limits the part holds
(+0xD8 to +0xEC, in degrees) the angle then stays within them; about one whose two limits are
equal it comes round to within a half turn either way. node_place places the node by them.
Improvement: the game turns the degrees to radians by a rounded 0.0174533 and a half turn by
3.14159; OpenReliant by the exact values.
Drawing between steps¶
The simulation steps 25 times a second, but mission_frame draws each object where it stands that
far into the step (0x0049A880, for every object before the camera is placed): each node that a
step committed a new place for is drawn simulation_counter / 4 of the way from that place to the
next (node_frame_update, 0x0049A460). At the start of a step it is drawn at the committed
place. A node posed by node_place is drawn between its two poses, the angles turning the short
way round, so a dish that loops a full turn doesn't spin back at the end of its track. Any other,
an object's root among them, moves along the straight line between its places and turns by that
share of the angles that turn one into the other (mat3_angles). The camera follows the root's
frame, so it moves with the object as drawn. A node no step has moved keeps its frame. The walk
(node_tree_frames) goes into each child that is neither hidden nor flagged 0x80: the root's
children are every part of the model, so a part is drawn between steps while the part it is linked
to is hidden, and a hidden part's own children, the models it carries, keep their frames.
In a multiplayer game another player's ship is drawn between the places its last two messages gave
it (+0x768, +0x798) instead.
What an attachment point holds¶
node_mount (0x00499A10) mounts what a part's attachment points carry, by the attachment's kind:
an engine glow for kind 2, a muzzle's flash for kind 3 (Guns) and a light
for kind 4, which are nodes of the part's own, and for a gun
or a pod an object of its own, whose model attachment_models names by the attachment's kind and
id, twenty ids to a kind.
Each node has a kind, which node_draw (0x0049A8C0) draws it by: 1 a model part
(node_add_part), 2 an engine glow (node_mount_glow, 0x00499540), 3 a light's two sprites and
5 the point light a blinking light casts (node_mount_light), 4 a muzzle's flash
(node_mount_muzzle, 0x00499680), and 6 what a hit leaves where it struck (node_add_effect,
0x004992D0; Effects).
A mounted object is built the way any other is: a node for each part of its model, then
object_link_parts, which also moves its origin to its own centre of mass. Its root then hangs
from the node of the part that carries the attachment, and stands where the attachment does: the
attachment's position, plus that centre turned by the attachment's orientation, so its geometry
stands where its own model puts it. Its orientation is the attachment's.
A capital ship carries its turrets this way, where a fighter carries its own as a model part of subsystem class 3 with its own yaw and pitch limits.
Motion¶
object_move (0x00473FF0) moves an object for one update. It calls the object's motion function,
the code at 0x640, then sets the root's next orientation to its orientation times the object's
rotation (0x56C), and its next position to its position plus the object's velocity (0x590),
and records the length of the velocity as the speed (0x5D8). The root keeps that next place at
+0x5C and +0x68.
Before that, it checks the flags:
- A
frozenobject isn't moved at all. - If the object has taken knocks since its last move, they are applied in place of the motion
function (see Knocks). An
unpoweredobject has no motion function run either. - A
jumpingobject that isn't knocked orunpoweredstays where it is.
After the move, it sets bit 0 of the network flags (0x00C) if the object is moving and bit 1 if
any of its angular rates is nonzero. The multiplayer code (0x004BB2D0) then sends the object's
position and orientation. Bit 2 is set while the Scoop Up order runs, and the multiplayer code
skips such objects.
For the player's ship, object_move raises the camera's shake (hit_shake) to at least
0.2 * (speed / cruise speed - 1), so the view shakes when the ship flies faster than its cruise
speed, as it does under afterburner. It also stores the change in the player's speed at
0x00562CE4, which nothing reads.
object_move also sets bit 0 of the root's node flags, marking the next place as pending. At the
start of the next simulation step, before the objects move, simulation_step runs
node_tree_update (0x00476C90) for every live object. It commits the pending next place by
copying the 0x48 bytes from +0x5C over those from +0x14 (the next position and orientation
over the current ones), clears bit 0 and sets bits 1 and 2. It does the same for each animating
part, whose animation it also advances. So an object moves on from the place the previous step
worked out, and between steps its position is one step behind next_position, which the rest
of the game reads as the object's place.
Each step, before the node updates, simulation_step also orthonormalizes the root's next
orientation (0x004C2690) of one object, the one simulation_turn (0x00562FFC) names. The turn
moves on by one each step and goes round the live objects, so rounding never builds up in any
object's orientation. The same object's orientation at 0x7A4, which a multiplayer game draws
other players' ships by, is orthonormalized too.
create_object gives every object motion_forward (0x004744C0), which runs the flight model,
object_fly (0x004742E0), with a thrust of 1; motion_backward runs it with -1. The orders
select the others (see The orders' motion functions).
| Offset | Size | Field |
|---|---|---|
0x00C |
4 | Network flags |
0x51C |
4 | Knocks since the last move |
0x520 |
4 | Mass: the sum of the parts' masses (object_recentre) |
0x524 |
12 | How far object_recentre moved the origin to the centre of mass |
0x530 |
12 | Impulse: the sum of the knocks' forces |
0x53C |
12 | Angular impulse: the sum of the knocks' force × lever |
0x548 |
36 | The inverse of the inertia tensor |
0x56C |
36 | Rotation: the turn applied each update, a 3x3 matrix |
0x590 |
12 | Velocity, added to the position each update |
0x5B8 |
4 | Throttle |
0x5BC, 0x5C0, 0x5C4 |
4 each | Roll, pitch and yaw inputs, between -1 and 1 |
0x5C8 |
4 | Lateral input |
0x5CC |
1 | Afterburner |
0x5CD |
1 | Reverse thrust |
0x59C |
4 | Collision radius |
0x5D8 |
4 | Speed |
0x5DC, 0x5E0, 0x5E4 |
4 each | Roll, pitch and yaw rates |
0x640 |
4 | Motion function |
0x650 |
4 | The last update's throttle |
0x668 |
4 | armor_speed_factor: scales the cruise speed as the armor falls |
0x738 |
4 | speed_factor: scales the cruise speed. The engines' share of the power (Controls); 1.0 when created |
For the player's ship, the controls set the inputs, the throttle and the two burns; for the others, the routines of their orders.
The flight model works in the ship's own frame, the
model frame: X lateral, Y down, Z forward. Each quantity
moves toward a target through an inertia from the ship's flight stats:
new = old * inertia + target * (1 - inertia).
- Throttle. It stays between 0 and 1, but is 2 while the afterburner burns and -1 under reverse thrust. Either burns 4 units of afterburner fuel an update, and fuel stops at zero.
- Turning (
object_steer,0x00474150). Each input is clamped to between -1 and 1, and each angular rate moves toward the ship's rate for that axis times the input, through that axis's inertia. For callers that ask, the target is divided by3 - 2 * |throttle|where that exceeds 1, so the ship turns slower at low throttle. The three rates then make the rotation. - Speed. The velocity is turned into the ship's frame. Along Z,
v * |v|moves towardu * |u| * target * target, whereuis the thrust times the throttle andtargetthe cruise speed, ormax_speedunder afterburner or reverse thrust; the square root, with its sign, is the new forward speed. Along X, the speed moves toward a quarter of the target times the lateral input. Along Y it only decays. All three use the ship'sinertia, and the velocity is turned back.
The cruise speed (object_cruise_speed, 0x00403060) is max_speed times speed_factor
(0x738), 1.0 when created, times the share of engines left, and, unless the camera is in view 13
or the object is invulnerable, times armor_speed_factor (0x668), which falls as the armor does.
So losing engines or armor slows a ship.
Knocks¶
Collisions and explosions push objects with object_knock (0x004763C0), which takes a force and
the world point it acts at. It adds the force to the impulse (0x530), adds force × lever to the
angular impulse (0x53C), where the lever runs from the object's position to the point, and counts
the knock (0x51C). The engine takes the cross product in that order, which gives the opposite of
the torque. object_knock_local (0x00476430) does the same with the force in the object's own
frame and the lever given directly; the Disrupted order pushes a ship with it, with no lever.
The next object_move applies the knocks with object_apply_knocks (0x00476270) instead of
running the motion function:
- The impulse times
1 / massis added to the velocity. - If the angular impulse isn't zero, it is converted to the object's frame and multiplied by the
inverse inertia tensor (
0x548), whichobject_recentrebuilds from the parts (object_bounds) and inverts (0x004AD9F0). The rotation is turned by the negative of the result, to first order, and orthonormalized (0x004C2690), and the angular rates are set to its angles (0x004C2740). So the object keeps spinning until its steering takes over again. - The count and both impulses are cleared.
0x004C2740 takes its angles from sr_atan2 (0x004C3200), which looks them up in a table of the
arctangents of 0 to 1 in steps of 1/4096 (0x005DE344), by the smaller of y / x and x / y
rounded to the nearest step.
Improvement: OpenReliant computes the angles instead, which is more precise by up to half a step.
The orders' motion functions¶
The orders select eight more motion functions, which read the order's state (0x68C): the two the
ejection selects, motion_brake (0x00474610) and motion_drift (0x00474B00),
the two the launches select, motion_downward (0x004744E0) and motion_plain
(0x00474570), the two of the jumps, motion_jump_out (0x00474640) and
motion_jump_in (0x004746D0), and the two of the paths ships follow,
motion_follow (0x00474770) and motion_follow_backwards (0x00474930). OpenReliant has all
eight.
| Address | Selected by | What it does |
|---|---|---|
0x004744E0 |
The Reliant's launch, the Ripper | The plain flight model along the object's Y axis, which points below it: steers, the throttle slowing no turn, then moves the velocity through the flight stats' inertia toward the throttle times max_speed, the last update's throttle nothing. A fighter (class 1) flies by the first ship type's flight stats, the Predator's (ship_flight_stats, 0x004F9E70), so that every fighter leaves its carrier alike. |
0x00474570 |
A torpedo's launch, the Ripper, landing orders | The same along the Z axis, keeping the throttle as the last update's, and the Ripper flies by its own stats whatever its class. A flight model without the throttle rules or the burns. |
0x00474610 |
Eject | Slows the velocity to 0.97 of itself each update. |
0x00474640 |
Jump Out | Moves the object to the point between the two of the order's state by the square of the share of 250 ticks since it went. No rotation, the last update's throttle nothing. |
0x004746D0 |
Jump In | Flies along the nose at 2400 for an object that lists components, or 600, less 0.003 of that for each tick since the jump placed it, but never slower than the cruise speed. No rotation, the last update's throttle nothing. |
0x00474770 |
Follow Curve, Dock | Turns toward the point the routine at the start of the order's state gives: its inputs are the angles between the ship's orientation and the look at the point (mat3_angles), less 6 times each turn rate, times 11.46; turned at the point within 0.01 radians, it rolls to stand as the way up the routine gives, where it gives one. Then object_steer, and it moves straight to the point, no faster than its top speed times the share at +0x04 of the state, its throttle the share of its top speed it moves at. |
0x00474930 |
Follow Curve | The same, its turns the other way round, for flying tail first. |
0x00474B00 |
The ship a pilot has left (eject_separate) |
Slows the velocity to 0.99 of itself each update. |
Porting¶
motion.zig holds the model, steer, fly and move, whose
file the binary doesn't name: it lies after explode.cpp's code and before gameflow.cpp's.
cruiseSpeed is in ai.zig, since object_cruise_speed lies after
Ai.cpp's code, and gameobj.zig has knock, knockLocal
and applyKnocks. OpenReliant passes the flight stats and the camera
view in, where the game reaches them through the object's own pointer and a global, because
GameObject keeps the binary's 32-bit pointers for its layout. For the same reason move takes
the camera's shake as a pointer, set only for the player's ship, where the game compares the slot
with the player's and writes the global. Motion is an enum of the two routines
create_object installs, in place of the function pointer at 0x640, and the rule each quantity
settles by is one settle helper rather than the six copies the binary holds.
gameobj.updateTree ports node_tree_update, which gameobj.simulationStep runs for every live
object at the start of each step, after gameobj.orthonormalizeTurn on the object whose turn it is
(gameobj.nextTurn). create.objectsUpdate then moves them.
object_recentre builds the inertia tensor from the parts (object_bounds) and inverts it into
0x548, which create_object gives the record as angular_response: applyKnocks turns the
angular impulse of collisions and explosions by it, so a knock off an object's centre turns it.
Shields¶
Each simulation step, simulation_step runs object_recharge_shields (0x00476FC0) for every
object, after its node update. Each of the four shields gains its full charge,
6 * shield_power - 1, times the shields' power factor (0x73C) and their condition (0x664),
over the type's shield_recharge seconds of steps, and stops at the full charge. For the player's
ship, the full charge of the fore shield, the third, is lower by however far the aft shield and its
reserve together go beyond it, and the aft shield's likewise.
An object whose components are listed recharges no shields here, and neither does one holding
the no_shield_recharge power-up (8). One whose 0xB95 is 5 has its shields emptied instead. In a
multiplayer game, the player's shields don't recharge while 0x5D76F0 is 4 and 0x5DB538 names
the player. Unknown: what those values mean.
object_armor_conditions (0x00492370) works out three conditions from each quadrant's armor over
its full armor, 6 * armor_class - 1, the fore quadrant being the third and the aft the fourth:
the shields' (0x664), a quarter of each quadrant's; the guns' (0x66C), half the fore one's and
a quarter of each side's; and the cruise speed's (0x668), a quarter plus three quarters of the
aft one's. create_object runs it once the armor is full. For the player's ship it also sounds a
warning, sound 1 of betty.fat, at most every 500 ticks (0x00588334), while a quadrant has lost
its shield and half its armor.
gameobj.zig ports the recharge as rechargeShields, which
simulationStep runs, and main.zig the conditions as
armorConditions and the warning as armorWarning. Not ported: the multiplayer case.
Destruction¶
What gets through a shield wears the quadrant's armour (object_armor_damage, 0x004641F0). An
invulnerable object takes it only while it leaves armour to spare: one fully invulnerable from
anything, one that only a player can hit from anyone else. One in the last state, 4, takes none.
Armour below zero destroys the object (object_destroyed, 0x00401F30), telling it that it may
spin out, and that a player's pilot has no time to eject where the blow was over 1000.
Outside multiplayer the game's difficulty (0x00562F14: 0 easy, 1 medium, 2 hard, 0 as the game
starts, and medium as SET GAME DIFFICULTY starts, Front end)
scales damage (damage_by_difficulty, 0x00463D70):
| Difficulty | A shot on a hostile object | Anything on the player's ship |
|---|---|---|
| Easy | ×1.5 | ×0.375 |
| Medium | ×1 | ×0.5 |
| Hard | ×0.75 | ×0.75 |
object_damage (0x00463EE0) scales what the shield takes, but reckons what passes through from
the damage before the scaling. object_armor_damage scales its damage twice, once for
recent_damage and that again for the armour, and component_damage (0x004645C0) once. So at
medium a hit on the player's ship takes half off its shield and a quarter of what gets through off
its armour. The game tells a shot by comparing the damage's kind with the player's slot, which is
0, a shot's kind, in a single-player game. OpenReliant flies a mission the front end starts at the
difficulty the pilot roster set, and one --mission names at medium; --difficulty sets either.
- An AI ship's pilot ejects where the ship is in the player's wing,
0x74C0, and its roll at0x70Cis below 40, or where the ship was told to eject before exploding. The ship spins on under Eject Spin (108). - The player's pilot ejects, unless it has already, the blow was too heavy, or the ship is the
Kamov: Eject Player (118) marks the ship ejected and unpowered and
mission_ending(0x00588394) - The ship drifts, the player's controls still running, for 400 to 599 ticks, and is then destroyed again, now without spinning out.
- Otherwise the object explodes: its stack becomes the one order Explode (11), whatever it was
doing, and it is flagged
exploding.
Explode's init (order_explode_init, 0x00408610) picks a mode by what the object is, with an
init and an update for each in explode_modes (0x004E1798): a ship, a ship that lists
components going as a whole, one of its components, an asteroid, and the limpet car. A ship
(explode_ship_init, 0x004086F0) is heard at once within 20000 of the camera, sound 11 on a sure
voice, and goes in one of three styles, by object_random15 over 3; the torpedoes always halt:
| Style | Init | What it does |
|---|---|---|
| 0, spin out | explode_spin_out_init (0x00408BC0) |
Unpowered, it drifts on, turning by a random spin a step, up to ±0.025 about its first two axes and ±0.15 about its third, which shrinks to nothing as its end comes: 200 to 399 ticks on. Near its end it trails burning bits. A torpedo, or a ship that may not spin, stops dead instead and blows up at once. |
| 1, burst | explode_burst_init (0x004090F0) |
Unpowered, no longer turning, it bursts at once. |
| 2, halt | explode_halt_init (0x00408D20) |
It stops dead and blows up at once. A torpedo sets off a chain of fireballs about where it is drawn, and a shockwave that harms the player from where its next step takes it (root.next_position). |
Having picked its style, a ship credits its end (explode_kill_credit, 0x00408500): where the
player's ship struck it last (last_attacker) and it is hostile, and a fighter by its type's class,
a Kamov, a Kurgan or a Gurevich, the pilot has another kill (kills_add, which the display's skull
readout shows), and a wingman remarks on it. It does this only while kill_credit_on
(0x00529C6C) is set, as a mission's start leaves it and the radio's menu can clear it
(The menu).
Past its end, a burst blows up in its own way (explode_burst, 0x00471DB0), a torpedo not at all,
having gone up as it stopped, and anything else in a blast (explode_blast, 0x0046C980); both
play sound 11 again. object_retire (0x004688E0) then leaves a stand-in, of type 1001, flagged as
one and exploding, not targetable and with no orders, which nothing moves, draws or collides with.
The other modes:
- A ship that lists components, going as a whole (
explode_hull_init,0x00409170): each part of its hull in the root's child list, whatever part it hangs from, but a part of a damaged model, is left with -1 armour, and the root is flagged for the component losses to take it away (Components). Its update (0x004091E0) ends a disabled ship as its hull holding it together does (object_hull_lost) and pops the order of any other. - One of its components, the one the order is aimed at (
explode_component_init,0x00409200): where it is shown, it is left with -1 armour, and the root of the model holding it is flagged. Its update (explode_component,0x00409260) pops the order. - An asteroid, types
0x79to0x7F(explode_asteroid_init,0x00409270): unpowered and frozen, it goes the frame after (explode_asteroid,0x004092A0) in a fireball as wide as 1.5 times its radius, lighting what is round it, and is retired. Where itsvisibilitytimes 0.4 is at least 0.16, three asteroids of types0x7Bto0x7E, at random, take its place, that much of its size:visibility, and the scale of their first part's frame (+0x48). The first is turned three times 1.88496 about the X axis, the next twice and the last once, and each stands 3 of its own radii along its nose from where the rock was, still and colliding with nothing. A whole rock so leaves three of 0.4, and each of those three of 0.16. Improvement: 1.88496 is three fifths of a half turn, rounded; OpenReliant computes it. - The limpet car, type
0x1D(explode_limpet_car_init,0x004094D0): it stops dead, unpowered, with a random turn up to ±0.025 about its first two axes and ±0.15 about its third, and goes up in a fireball as wide as its radius. Its update (explode_limpet_car,0x004095F0), the same step, hides its first part, blows it up (explode_blast), and replaces it in its slot with a limpet pod, type0xBC, where that part was going; a car whose first part is already hidden blows up and is retired.
The player's ship has the camera watch its end, locked: a spin-out slower than 100 from behind,
pulling away (view 8), a faster one from where the camera was (view 0x1A), a burst from there
watching where it burst (view 0x1B), and a halt from behind. mission_ending becomes 1.
A ship's end, and the limpet car's, posts its Destroyed event for the mission's triggers (Script VM).
ai.zig ports object_destroyed as objectDestroyed,
collision.zig the armour damage as armorDamage,
aiexplode.zig Explode,
aieject.zig the ejection's orders,
explode.zig the blasts, and
create.zig object_retire as retire.
The blasts' break-up, particles, fireballs, burning bits and shockwaves are in Effects. Not ported: what a few types set off first (#238); the Ulysses' own end (#232).
The wing's pilots¶
The player's wingmen in the campaign are pilots of the 45th, who change as they die. The game keeps
them in pilots.cpp's globals (pilots.zig, Wingmen):
alpha_pilots(0x0058A958): the pilots of Alpha 1 to 6, by the pilot stats' number: -1 for the player, then the five wingmen's. A new campaign's are Frenchy (0x55), Worm (0x6C), Silky (0x56), Bandit (0xAC) and Viper (7).pilot_pool(0x005047D0): the 65 pilots who replace those who die, each with a status: 0 dead, 1 in the wing, 2 free. All are free as the game starts (pilots/pool.zig, generated from the payload).
campaign_pilots_reset (0x0049CD20) frees every pilot of the pool and gives the wing a new
campaign's pilots. The front end calls it on the way into SINGLE PLAYER (0x0042910A) and as START
GAME starts a campaign (0x00430AB4).
As the campaign moves on from a mission, before the autosave (mission_end_record,
0x00475BE8), and again as each mission starts, once its script has started, update_pilots
(0x0049CD70) gives Alpha 5 and 6 the pilots of the campaign's stretch: Bandit and Viper to
mission 5, Diceman and Bandit to mission 22 (two different Bandit records), and Hawkeye and Diceman
to mission 28. Each other place left empty takes the first free pilot of the pool, which is then in
the wing; with none free, the game stops (Fix: OpenReliant logs it and leaves the place empty).
Then, in missions 1 to 28 out of the simulator, the start gives each of the player's wingmen its
place's pilot (object_set_pilot, 0x00493DD1 to 0x00493E0A), in place of the one the mission's
records name, such as 45TH VOLUNTEERS. The radio's window and the target display then name it.
Fix: for a place of the wing no ship fills, the game gives the pilot to the object before the
first, writing through the pointer in front of the objects' table; OpenReliant gives none.
As a ship is destroyed (explode_ship_init, from 0x004088D5), a pilot of the wing leaves an empty
place for the next mission to fill, and is dead in the pool.
A saved game keeps the wing (ALPH) and the pool's first record alone (PILO)
(Saved games).
Components¶
The parts whose .SHP flags have bit 0x02 are the object's
components, such as a capital ship's engines, shield generators and turrets. create_object
gives the object a node for each part of its model, all hanging from the root in part order, and a
part's node holds the roots of the objects mounted on the part's gun and pod
attachment points. When the model's header asks for
components, object_collect_components (0x00468760) lists them from the root down: for each node,
first its children that are components, then, child by child, theirs. So the model's own components
come first, in part order, and then, part by part and mount by mount, those of the mounted models.
A component's entry holds its node, the slot of the parent's child list that holds it, and at +8
the invulnerability SetInvulnerability gives the component, a halfword that component_damage
reads as it reads an object's: 2 keeps off every hit, and 1 every hit but a player's ship's.
OpenReliant checks it on the armor-bearing part after resolving a linked assembly. Value 2
prevents lethal damage from every attacker; value 1 permits lethal damage only from player
slots. Nonlethal damage still reduces armor, and ShotAt events still fire. Hit scoring remains
in #538.
A component's armour comes from its part's record (0x104), and component_damage
(0x004645C0) wears it down: the damage goes to the first part of the component's assembly that
still has armour, a part with more than 2499 takes only a hit of 500 or more, an object with a
shield generator keeps three quarters of a hit below 1000, and a component whose armour runs out
marks its root as destroyed (node flag 0x40). A collision does none of this. Every part node
stays in its root's child list, whatever part object_link_part links it to, so the node holding
a component (node_holder, 0x00499EE0) is always its model's root: the object's, or that of a
model mounted on it.
A component's destruction¶
node_draw (0x0049A8C0) acts on a root flagged destroyed as mission_frame's pass reaches it,
whether the object is in sight or not. For each of the root's parts whose armour is below zero and
that has no flag 0x10, in part order:
- It gets flag
0x10, which neither targeting nor cycling subtargets accepts. - An engine takes
1 / enginesoff the object's share of its engines (+0x5D4). - A shield generator, while the object has one, plays
SHLDDOWN(sound0x39) at the part, facing its way, and clears the object'sshield_generator, so hits are no longer cut to a quarter. For any other part, the routine at the object's+0x614runs, and where it answers false the pass over this root ends there, its flag left set. explode_component_lost(0x0046D090) sets the assembly off: after a few types' own extras, each part of the assembly goes up with what is mounted on it (explode_part_burst, see Effects), and the root sends out 200 of the flame and sound0xB.- Each part of the assembly (the same link id) that is shown queues Destroyed for its component
index (
event_destroyed) and is destroyed (node_destroy,0x00499E30), with every part linked to it and every model mounted on them. Where it is of class hull, the ship ends first: the player gets the kill of a Kurgan, an Antanov or a Gurevich, andobject_hull_lost(0x00401F00) gives its order way as to Explode, empties its stack and marks it exploding. Each hidden part of the assembly, its damaged model, is shown.
Where the object's parts are the player's subtarget picked out in red, they are put back before the
pass and picked out again after it; since putting them back leaves none picked out, the second never
runs, and mission_frame picks them out on its next frame (The subtarget in
red). Then the flag is cleared.
node_forget (0x00499BB0), as a node is destroyed, stops for good each of the owner's turrets
whose base the node is (turret kind -1) and takes the node out of the owner's components and the
table at +0x518.
create_object gives most capital ships, bases and stations explode_capship_component
(0x0046F820) at +0x614, by the type whose stats they take, and type 0x16
explode_ulysses_component (0x0046EA50). The first answers true for any part but one of class
hull; for that one, it marks the ship unpowered and exploding, hides its force fields, splits it in
two (Effects), credits the kill as above and ends it with object_hull_lost.
The second answers false for every part.
Each part the pass takes out that the object lists as a component posts the component's Destroyed event first (Script VM).
objects.zig ports the pass as loseComponents and
node_destroy as destroyPart. Not ported: the types' own extras
(#238) and the Ulysses' routine
(#232).
OpenReliant lists them in create.zig as the parts themselves,
since a mounted turret's parts are not the hull's, and marks each one as a component and, where the
model asks, as targetable. A model with more components than the object holds leaves the
rest unlisted, where the game stops with a fatal error.
sltool shp components lists a model's components in that order, with their armour, finding the
mounted models beside it, and sltool dte triggers and sltool dte script name the components
missions refer to. Every component a trigger names in the shipped missions is on its ship's list,
and nearly every one a squad member or push_component names. The rest point past the end of the
list, mostly by one; the missions do not always agree among themselves, as when one squad of the
Kiev Morzov in mission19 holds its turrets as components 3 to 9 and others hold them one by one as
4 to 10.
Mission data names a component by its index in that list: a trigger's qualifier, a squad member's
component, the operand of push_component. Events on a component carry its index, and destroying
component n clears bit n & 31 of the mission ship's word at 0x30.
Commands act on a component through its assembly: the nodes beside it whose parts share its part's
link id, such as a turret and its barrels. The assembly can hold a damaged model too, parts whose
part flag 0x04 is set, which stay hidden (node flag 0x20) while the component is intact.
DisableObject hides the intact parts and shows the damaged ones, and enabling does the reverse; on
a whole ship it sets the object's disabled flag instead. DestroySubObject destroys the assembly,
keeping and showing its damaged parts when its second argument asks for them. Destroying an engine
lowers the owner's share of engines left, and destroying a shield generator clears its
shield_generator flag.
ship_damage_value (0x00452CB0), the value ShotAt events carry, is the lowest of the object's
four armor values, or a component's own.
The hit tests¶
object_hit_test (0x0049BEF0) walks an object's nodes with a test and a query. For a root node,
the object's or that of an object mounted on it, node_hit_test (0x0049BD30) hands the test the
object's bounding box, at the root's next place, and only where the test passes does the walk go on
into the root's children. For a part node it hands the test each box of the part's collision tree,
at the node's next place (node_next_place), from the root box down, going on into a box's children
only where the test passes, and then walks on into the node's own children, the roots of what the
part mounts, whatever the test said. A child that is hidden, or of flag 0x80, the nodes the
attachment points make, is passed over with all it holds. The shots' candidates
(bullet_candidate_test), the shots' hits and the missiles' (missile_hull_test, 0x004959A0)
and the collisions go through it.
For an object that lists components, create_object numbers its part nodes, its model's and those
of the models mounted on it, from the root down, depth first (object_number_parts, 0x00466BA0):
each node's number goes at +0xFC, the count at +0x154, and a table of the nodes by number at
+0x518. A shot's candidates name its parts by these numbers; OpenReliant names a part by its model
and its index there (objects.PartRef), and walks the models mounted on a part after it.