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). Provenance: derived, not attested, so these rows sit on the reverse-engineering queue.)
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:
Obstaclesare the lightest of the three. Each entry is matched by itsNameresref to an already-placed object (CSWMiniGameObjectArray::GetMiniGameObjectByName,0x0066bfb0), andCSWMGObstacle::Load(0x0066d0b0) reads only a nestedScriptsstruct, with no weapon, lifecycle or geometry data at all.Playerand eachEnemiesentry are both backed by the same underlying object,CSWTrackFollower.CSWMiniPlayer::Load(0x006702f0) andCSWMiniEnemy::Load(0x006705f0) both delegate first toCSWTrackFollower::Load(0x0066fff0) on an embeddedfollowersub-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 is three distinct object shapes sharing a common base, plus movement fields that only make sense on the vehicle the player actually drives.
There is a fourth child, and the trace above does not cover it. Mouse sits on the same MiniGame struct alongside those three, carrying four axis settings: AxisX, AxisY, FlipAxisX and FlipAxisY. It turns up in a single vanilla minigame area. What reads it, if anything, has not been established: the three reads above are what CSWMiniGame::Load was traced doing, not an inventory of everything the struct holds.
Treat an absent Mouse as an open question rather than an empty one. A missing nested struct is a different fact from a missing scalar: a scalar’s absence resolves to whatever the engine substitutes, while a struct’s absence says the thing is not there at all. Filling one in with zeroes would invent structure the file never carried.
The Shared Vehicle Base (CSWTrackFollower)
CSWTrackFollower::Load reads the following directly onto the Player or Enemy entry, before any weapon or script data:
| Field | Type | Absent-field behaviour |
|---|---|---|
Hit_Points, Max_HPs | DWORD | Default 0; only applied if the read value is greater than 0. Otherwise the object’s already-constructed value is left untouched, carried over rather than reset. |
Sphere_Radius | FLOAT | Default sentinel -1.0; applied only if the read value is >= 0.0. |
Invince_Period | FLOAT | Default 0.0, applied whenever the read value is >= 0.0, which is trivially true, so this one effectively always writes. |
Bump_Damage | INT | Default 0, written unconditionally with no gate. |
Num_Loops | INT | Default 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. 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, so an absent field overwrites with empty rather than leaving a prior value in place.
| Field | Owner | Notes |
|---|---|---|
OnCreate, OnHitBullet, OnHitFollower, OnAnimEvent, OnHeartbeat | Base (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, OnTrackLoop | Override (CSWTrackFollower::LoadScripts) | Player/Enemy only. |
OnHeartbeat is the field an inventory of this struct is most likely to miss, and the reason transfers to any field survey. A scan keyed on bare labels rather than paths cannot tell it apart from the ARE root’s own OnHeartbeat, which a typed view already models, so the nested one reads as covered and never surfaces. Every vanilla player carries a real script name in it. Path-keyed scans do not have that blind spot. Its membership in the base set is confirmed against the decompiled function rather than inferred from label counts.
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.
| Field | Owner | Absent-field behaviour |
|---|---|---|
BankID | Bank entry | Read 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_Model | Bank entry | Default empty, gated by resref validity; invalid or absent aborts the whole bank. |
Damage, Lifespan, Rate_Of_Fire, Speed, Target_Type | Bullet struct | Each 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 is no partial bank built from defaults. |
Bullet_Model, Collision_Sound | Bullet struct | Default empty; read unconditionally once Target_Type has succeeded, no further gating. |
Fire_Sound | Bank 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 behaviour 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 it anywhere. All 53 fields in this struct are 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.
| Field | Type | Absent-field behaviour |
|---|---|---|
Minimum_Speed | FLOAT | Default sentinel -1.0; applied only if >= 0.0. |
Maximum_Speed | FLOAT | Default 100.0; applied only if >= 0.0 (effectively always). |
Accel_Secs | FLOAT | Default 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) | FLOAT | Default 0.0 each, written unconditionally, with no carry-over gate. |
TunnelInfinite | Vector | Read via the vector default path, {0, 0, 0}, unconditional. |
Start_Offset_X / Start_Offset_Y / Start_Offset_Z | FLOAT (assembled into one Vector) | Default 0.0 each, fed to SetOrigin unconditionally. |
Target_Offset_X / Target_Offset_Y / Target_Offset_Z | FLOAT (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 in any area file carrying a minigame, 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).
Note
“Four scanned area files” was the whole population, not a sample. Exactly four of the area files in the module archives carry a
MiniGamestruct at all, and no area in a save corpus carries one. So the thirteen were checked against every minigame that ships rather than against a handful of them, and there is no wider set left to widen to. 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.