Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Swoop & Turret Minigame Deep Dive

The MiniGame struct nested inside an ARE’s top-level GFF configures the optional swoop-racing or turret minigame an area can host, gated by a Type field (1 = Swoop, 2 = Turret). A corpus scan of vanilla .are files turned up 53 GFF labels inside this struct that no typed view modelled, concentrated in the handful of area files that actually ship a minigame. This page documents what the loader does with all of them, and the shape they nest in.

(Documented from Ghidra decompilation of swkotor.exe. Entry point: CSWMiniGame::Load (0x006723d0).)

Shape: Player, Enemies, and Obstacles Are Not Siblings

CSWMiniGame::Load reads three things off the top-level MiniGame struct: a single Player struct, an Enemies list, and an Obstacles list. The three diverge sharply in what they carry:

  • Obstacles are the lightest of the three. Each entry is matched by its Name resref to an already-placed object (CSWMiniGameObjectArray::GetMiniGameObjectByName, 0x0066bfb0), and CSWMGObstacle::Load (0x0066d0b0) reads only a nested Scripts struct – no weapon, lifecycle, or geometry data at all.
  • Player and each Enemies entry are both backed by the same underlying object, CSWTrackFollower. CSWMiniPlayer::Load (0x006702f0) and CSWMiniEnemy::Load (0x006705f0) both delegate first to CSWTrackFollower::Load (0x0066fff0) on an embedded follower sub-object, which is where the bulk of the 53 fields actually live, then each reads its own extra fields on top.

So this isn’t one flat struct with 53 siblings – it’s three distinct object shapes sharing a common base, plus movement fields that only make sense on the vehicle the player actually drives.

The Shared Vehicle Base (CSWTrackFollower)

CSWTrackFollower::Load reads the following directly onto the Player or Enemy entry, before any weapon or script data:

FieldTypeAbsent-field behaviour
Hit_Points, Max_HPsDWORDDefault 0; only applied if the read value is greater than 0 – otherwise the object’s already-constructed value is left untouched (carried over, not reset).
Sphere_RadiusFLOATDefault sentinel -1.0; applied only if the read value is >= 0.0.
Invince_PeriodFLOATDefault 0.0, applied whenever the read value is >= 0.0 – trivially true, so this one effectively always writes.
Bump_DamageINTDefault 0, written unconditionally with no gate.
Num_LoopsINTDefault sentinel -10, passed unconditionally into a virtual setter whose own absence handling wasn’t traced further.

Then it reads a Gun_Banks list (covered below), a nested Scripts struct via CSWTrackFollower::LoadScripts (0x0066c740), and a nested Sounds struct via CSWTrackFollower::LoadSounds (0x0066f7e0).

CSWTrackFollower::LoadScripts overrides the base CSWMiniGameObject::LoadScripts (0x0066c420, the same one Obstacles use for their own, smaller Scripts struct) and adds five fields on top of the base set, listed in the override row below. An earlier draft of this sentence said three, which contradicted the page’s own table. Every script field, base and override alike, follows the same pattern: default empty CResRef, and always written into an indexed script-slot array via a virtual setter regardless of whether the file supplied a value – an absent field overwrites with empty rather than leaving a prior value in place.

FieldOwnerNotes
OnCreate, OnHitBullet, OnHitFollower, OnAnimEvent, OnHeartbeatBase (CSWMiniGameObject::LoadScripts)Shared by Obstacles’ own Scripts struct too. Confirmed by direct decompilation: the function reads exactly these five fields, in this order, OnHeartbeat trailing OnAnimEvent as the fifth and last read.
OnDamage, OnDeath, OnFire, OnHitObstacle, OnTrackLoopOverride (CSWTrackFollower::LoadScripts)Player/Enemy only.

OnHeartbeat was absent from an earlier draft of this page, and the reason is worth keeping. The corpus scan that produced the field inventory keyed on bare labels rather than paths, and the ARE root has an OnHeartbeat of its own that a typed view already models, so the nested one was counted as covered and never surfaced. Every vanilla player carries a real script name in it. Path-keyed scans do not have this blind spot, which is why the tooling moved to them. It was initially re-added to the base set by inference (five labels in every Obstacles Scripts struct, four documented reads, obstacles reaching only the base loader) rather than by audit; that inference has since been confirmed directly against the decompiled function, so the base set stands at five fields, audited rather than inferred.

CSWTrackFollower::LoadSounds reads Engine and Death, both defaulting to empty CResRef and always written (same overwrite-on-absence pattern as scripts). A non-empty Engine sound is additionally forced into looping playback.

The Weapon Subsystem (Gun_Banks)

Gun_Banks is a list nested on the Player or an Enemy entry, not a flat field. Each entry is read by a class-specific virtual: CSWMiniPlayer::LoadGun (0x0066f890) or CSWMiniEnemy::LoadGun (0x0066fb20). Both read BankID and Gun_Model directly on the bank entry, then a nested Bullet struct, then Fire_Sound back on the bank entry itself – Fire_Sound is a sibling of Bullet, not a field inside it.

FieldOwnerAbsent-field behaviour
BankIDBank entryRead with a default of 0xffffffff if absent. The gate that decides whether to build the bank at all checks the resolved value against that same literal, not whether the field was present in the file – confirmed identical in both CSWMiniPlayer::LoadGun and CSWMiniEnemy::LoadGun. So an absent BankID and one explicitly written as 0xffffffff are indistinguishable to the loader: both skip bank creation the same way. There is no separate “field was present” check anywhere in this gate.
Gun_ModelBank entryDefault empty, gated by resref validity; invalid or absent aborts the whole bank.
Damage, Lifespan, Rate_Of_Fire, Speed, Target_TypeBullet structEach defaults to 0/0.0, but each read also reports a presence flag that gates whether the next field in this chain is even attempted. If any one of these five is genuinely absent, the chain truncates silently and the bank is never created – there’s no partial bank built from defaults.
Bullet_Model, Collision_SoundBullet structDefault empty; read unconditionally once Target_Type has succeeded, no further gating.
Fire_SoundBank entry (sibling of Bullet)Default empty, read unconditionally after the Bullet struct completes.

CSWMiniEnemy::LoadGun additionally reads four AI-targeting fields directly on the bank entry (also siblings of Bullet, bundled into a CSWMGTargettingParameters value): Sensing_Radius, Horiz_Spread, Vert_Spread, Inaccuracy. These are enemy-only – the player’s own guns don’t carry them. Each defaults to 0.0 and follows the same presence-gate chain as the Bullet fields: absence of any one aborts the read before Bullet is even fetched.

Enemy-Only: Trigger

CSWMiniEnemy::Load reads one more field directly on the Enemy entry itself, not on a gun bank: Trigger (BYTE), stored on the shared CSWTrackFollower base. It defaults to 0 when absent, and that default is applied unconditionally – the read’s own presence flag is never inspected, so a missing Trigger stamps 0 on load exactly as if the file had written it explicitly, rather than leaving the object’s already-constructed value in place.

Despite carrying a nonzero value in most vanilla enemy entries, Trigger looks write-only in this build: an exhaustive check of CSWTrackFollower’s and CSWMiniGame’s own behavior methods (Update, Go, Hit, OnDamage, OnDeath, OnHitObstacle, hit-check dispatch) and the script-facing per-follower accessor (which switches over hitpoints, max hitpoints, loop count, gun-bank count, and invulnerability) turned up no read of this value anywhere. Nothing observed gates on it, animates from it, or exposes it to scripts. This closes out the last previously-unmodeled field in this struct: all 53 are now traced.

Movement and Track Geometry (Player-Only)

A block of fields sit flat on the Player struct itself, read directly by CSWMiniPlayer::Load, and never appear on Enemies or Obstacles at all – they describe the track boundaries and the player vehicle’s own acceleration curve, not anything an enemy or a static obstacle needs.

FieldTypeAbsent-field behaviour
Minimum_SpeedFLOATDefault sentinel -1.0; applied only if >= 0.0.
Maximum_SpeedFLOATDefault 100.0; applied only if >= 0.0 (effectively always).
Accel_SecsFLOATDefault sentinel -1.0. If the read value is exactly 0.0, acceleration derives as (max_speed - min_speed). If it’s negative (and not the sentinel path), the whole acceleration derivation is skipped. Otherwise, (max_speed - min_speed) is divided by the read value. The raw field is never stored; only the derived acceleration is kept.
TunnelXPos / TunnelXNeg, TunnelZPos / TunnelZNeg (a Vector pair)FLOATDefault 0.0 each, written unconditionally – no carry-over gate.
TunnelInfiniteVectorRead via the vector default path, {0, 0, 0}, unconditional.
Start_Offset_X / Start_Offset_Y / Start_Offset_ZFLOAT (assembled into one Vector)Default 0.0 each, fed to SetOrigin unconditionally.
Target_Offset_X / Target_Offset_Y / Target_Offset_ZFLOAT (three independent floats, not assembled into a Vector)Default 0.0 each, unconditional.

The “Present But Never Live” 13 Are Genuinely Read

Unlike the unmodelled DLG field set, where a binary-wide string search settled the question outright (none of those five labels exist in the executable at all), that shortcut does not apply here: every one of the 53 labels in this subsystem, including the 13 that never carried a real value across the four scanned area files, exists in the binary and is read by real loader code with real default-handling, per the tables above (Bump_Damage, Engine, Maximum_Speed, Minimum_Speed, OnAnimEvent, OnHitBullet, OnTrackLoop, Start_Offset_Y, Start_Offset_Z, Target_Offset_X, Target_Offset_Y, TunnelYNeg, TunnelYPos). Nothing in the read path distinguishes them from their “live” siblings in the same functions – they simply never happened to diverge from the engine default in the four vanilla files that carry a minigame at all. None of the 53 are toolset-only scaffolding.

One Subsystem, Composed From Several Concerns

The field set is best understood as three genuinely separate concerns sharing a common vehicle base, not one flat bag of fields:

  • Weapon – a Gun_Banks list nested on the vehicle (Player or Enemy). Each bank is BankID / Gun_Model / Fire_Sound plus a nested Bullet struct for ballistics, and, enemy-only, sibling AI-targeting spread fields.
  • Lifecycle – flat fields directly on the shared CSWTrackFollower-backed vehicle (Hit_Points / Max_HPs / Invince_Period / Bump_Damage / Sphere_Radius / Num_Loops), plus the nested Scripts and Sounds structs.
  • Movement / track geometry – Player-struct-only flat fields (Tunnel*, *_Offset_*, the Accel_Secs/Minimum_Speed/Maximum_Speed derivation). Meaningless on an Enemy or Obstacle, and never read there.

Obstacles sit outside this hierarchy entirely, as a distinct, much lighter leaf with only a Scripts struct and nothing else.