Star Fox 64
Nintendo's 1997 rail shooter for the Nintendo 64, read through the sonicdcer/sf64 decompilation.
On this page
The atlas reads Star Fox 64 through sonicdcer and contributors’ decompilation, C that builds the US 1.1 ROM. Names and comments quoted here are theirs. Nothing has been measured in the game yet.
Two presses of Z or R start a barrel roll; the first sets a counter of 10 updates1, and while the roll’s deflect value is above 0, enemy shots that reach the Arwing turn away and do no damage1. Boost and brake share one meter of 90 units2, and once both buttons are let go it must refill completely before either works again2. Holding A charges a lock-on shot, and a press fires it once the charge counter has reached 10 counts3 or an enemy is locked, unless an earlier charged shot still holds its slot3. Fox starts with 255 units4 of shield, and each wing has its own health; a broken or missing wing sets the lasers back to single4.
Mechanics
| Mechanic | Kind | How |
|---|---|---|
| Barrel roll | Deflect | a second press of Z or R while the first press's 10-update count runs rolls the Arwing; while the roll's deflect value is above 0, enemy shots turn away and do no damage |
| Boost meter | Shared, with cooldown | boost and brake fill one 90-unit meter by 3 an update on rails; once let go or full it must empty to 0 before either works again |
| Lock-on shot | Search and home | holding A charges to 21 and sends search shots; a press at 10 or more, or with an enemy locked, fires a shot that steers toward the lock |
| Charge shot | Press to fire | holding A charges; with A up the charge falls 1 an update, and a new press at 10 or more fires |
| Locational damage | Wing loss | each wing starts at 60 (10 in Expert); side hits take 20, others 15, and a broken or missing wing sets the lasers to single |
Every claim about this game, by concept (4)
References
- ^ abIn the US 1.1 version of Star Fox 64, the Arwing rolls when Z or R is pressed twice. The first press sets that button's counter to 10, which
Player_UpdateArwingRollcounts down once per player update, and a second press while it is above 0 starts the roll, turning the ship 30 degrees an update for 10 updates and then slowing by 5 degrees an update until it stops.barrelRollAlphafalls by 30 every update while it is above 0, and in each update the roll's count is still above 0 after counting down (9 of its 10) it then gains 100, capped at 180, a net rise of 70 an update. While that value is above 0, the enemy-shot update (Effect_EnemyShot_Update) widens the shot's box around the player by 100 units on each axis, sends a shot inside it off in a new direction, and applies no damage.Codesonicdcer/sf64revisedHow we know
3397 if (gInputPress->button & Z_TRIG) { 3398 player->sfx.bank = 1; 3399 if (player->rollInputTimerL != 0) { 3400 player->rollState = 1; 3401 player->rollTimer = 10; 3402 player->rollRate = player->baseRollRate = 30; ⋮ 3405 player->rollInputTimerL = 10; ⋮ 3409 if (gInputPress->button & R_TRIG) { 3410 player->sfx.bank = 1; 3411 if (player->rollInputTimerR != 0) { 3412 player->rollState = 1; 3413 player->rollTimer = 10; ⋮
src/engine/fox_play.c lines 3397–3419 at 4235c59 · upstream ↗ 4823 if (player->barrelRollAlpha > 0) { 4824 player->barrelRollAlpha -= 30; 4825 if (player->barrelRollAlpha <= 0) { 4826 player->barrelRollAlpha = 0; ⋮ 4834 if (player->rollInputTimerL != 0) { 4835 player->rollInputTimerL--; ⋮ 4837 if (player->rollInputTimerR != 0) { 4838 player->rollInputTimerR--;
src/engine/fox_play.c lines 4823–4839 at 4235c59 · upstream ↗ 4846 if (player->rollTimer != 0) { 4847 player->rollTimer--; ⋮ 4850 if (player->rollState != 0) { 4851 player->rollInputTimerR = 0; 4852 player->rollInputTimerL = 0; ⋮ 4854 player->zRotBarrelRoll += player->rollRate; 4855 if (player->rollTimer == 0) { 4856 if (player->rollRate > 0) { 4857 player->rollRate -= 5; ⋮ 4859 if (player->rollRate < 0) { 4860 player->rollRate += 5; ⋮ 4862 if (player->rollRate == 0) { ⋮
src/engine/fox_play.c lines 4846–4870 at 4235c59 · upstream ↗ 5534void Player_UpdateOnRails(Player* player) { ⋮ 5545 Player_ArwingBank(player); 5546 Player_ArwingBoost(player); 5547 Player_ArwingBrake(player); 5548 Play_dummy_800B41E0(player); 5549 Player_UpdateArwingRoll(player);
src/engine/fox_play.c lines 5534–5549 at 4235c59 · upstream ↗ 2902 if (gPlayer[0].barrelRollAlpha != 0) { 2903 var_fa0 = 100.0f; ⋮ 2906 if (fabsf(gPlayer[0].trueZpos - this->obj.pos.z) < (50.0f + var_fa0)) { 2907 if ((fabsf(gPlayer[0].pos.x - this->obj.pos.x) < (30.0f + var_fa0)) && 2908 (fabsf(gPlayer[0].pos.y - this->obj.pos.y) < (30.0f + var_fa0))) { 2909 if ((gPlayer[0].barrelRollAlpha != 0) || (gPlayer[0].meteoWarpTimer != 0)) { 2910 this->obj.rot.y = 90.0f; 2911 this->obj.rot.x = RAND_FLOAT(360.0f); ⋮ 2921 gPlayer[0].deflectCount++; 2922 AUDIO_PLAY_SFX(NA_SE_ROLLING_REFLECT, this->sfxSource, 0); ⋮ 2925 if ((gPlayer[0].barrelRollAlpha == 0) && (gPlayer[0].mercyTimer == 0)) { 2926 Player_ApplyDamage(&gPlayer[0], 0, this->info.damage);
src/engine/fox_effect.c lines 2902–2926 at 4235c59 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^ abIn the US 1.1 version of Star Fox 64, the Arwing's boost and brake share one meter,
boostMeter, which counts up from 0 as it is used and stops at 90. Holding the boost button adds 3 a player update on rails and 1.5 in all-range mode (Player_ArwingBoost), and holding brake adds the same amounts (Player_ArwingBrake). Each of the two functions takes back 0.5 a update on rails (0.35 in all-range mode) whenever its own action isn't running, so the meter falls by 1 an update on rails when neither button is held. Reaching 90, holding both buttons with the meter above 0, or letting go of both with the meter above 0 setsboostCooldown, which blocks both actions until the meter is back to 0.Codesonicdcer/sf64How we know
4951void Player_ArwingBoost(Player* player) { ⋮ 4956 if ((player->boostMeter != 0.0f) && (gInputHold->button & gBrakeButton[player->num]) && 4957 (gInputHold->button & gBoostButton[player->num])) { 4958 player->boostCooldown = true; ⋮ 4961 if (gLevelMode == LEVELMODE_ON_RAILS) { 4962 boostDepleteRate = 3.0f; 4963 boostRecoverRate = 0.5f; ⋮ 4966 boostDepleteRate = 1.5f; 4967 boostRecoverRate = 0.35f;
src/engine/fox_play.c lines 4951–4968 at 4235c59 · upstream ↗ 5024 if (!(gInputHold->button & gBrakeButton[player->num]) && !(gInputHold->button & gBoostButton[player->num])) { 5025 player->boostCooldown = true; 5026 if (player->boostMeter == 0.0f) { 5027 player->boostCooldown = false; ⋮ 5030 if ((gInputHold->button & gBoostButton[player->num]) && !(gInputHold->button & gBrakeButton[player->num]) && 5031 (player->state != PLAYERSTATE_U_TURN) && !player->boostCooldown) {
src/engine/fox_play.c lines 5024–5031 at 4235c59 · upstream ↗ 5047 player->boostMeter += boostDepleteRate; 5048 if (player->boostMeter > 90.0f) { 5049 player->boostMeter = 90.0f; 5050 player->boostCooldown = true;
src/engine/fox_play.c lines 5047–5051 at 4235c59 · upstream ↗ 5064 } else { 5065 if (player->boostMeter > 0.0f) { 5066 player->boostMeter -= boostRecoverRate; 5067 if (player->boostMeter <= 0.0f) { 5068 player->boostMeter = 0.0f; 5069 player->boostCooldown = false;
src/engine/fox_play.c lines 5064–5069 at 4235c59 · upstream ↗ 5092 if (gLevelMode == LEVELMODE_ON_RAILS) { 5093 brakeDepleteRate = 3.0f; 5094 brakeRecoverRate = 0.5f; 5095 } else { 5096 brakeDepleteRate = 1.5f; 5097 brakeRecoverRate = 0.35f;
src/engine/fox_play.c lines 5092–5098 at 4235c59 · upstream ↗ 5130 if ((gInputHold->button & gBrakeButton[player->num]) && !(gInputHold->button & gBoostButton[player->num]) && 5131 (player->state != PLAYERSTATE_U_TURN) && !player->boostCooldown) { ⋮ 5146 player->boostMeter += brakeDepleteRate; 5147 if (player->boostMeter > 90.0f) { 5148 player->boostCooldown = true; 5149 player->boostMeter = 90.0f; ⋮ 5161 } else if (player->boostMeter > 0.0f) { 5162 player->boostMeter -= brakeRecoverRate; 5163 if (player->boostMeter <= 0.0f) { 5164 player->boostMeter = 0.0f; 5165 player->boostCooldown = false;
src/engine/fox_play.c lines 5130–5165 at 4235c59 · upstream ↗ 5601void Player_Update360(Player* player) { 5602 switch (player->form) { 5603 case FORM_ARWING: 5604 Player_ArwingBank(player); 5605 Player_ArwingBoost(player); 5606 Player_ArwingBrake(player);
src/engine/fox_play.c lines 5601–5606 at 4235c59 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abIn the US 1.1 version of Star Fox 64, holding A counts a charge counter up by 1 a player update to a cap of 21, and an update without A held counts it down by 1 (
Player_UpdateLockOn). A fresh press of A, whose own update first adds 1 to the counter, fires the charged shot (PLAYERSHOT_LOCK_ON) if an enemy is locked on or the counter is then above 10 (so 10 or more before the press). Either path fires only if the player's charged-shot slot (gPlayerShots[14 - player->num]) is free, holds a shot of another type, or holds a charged shot that has lost its target (the locked path) or grown past scale 1 (the counter path); a press that fires nothing sets the counter to 0. Once the counter is above 20, while A is held, nothing is locked, and Z and R aren't both held, it launches a search shot every fourth game frame, and the first targetable enemy within 300 units of that shot on each axis (200 in all-range mode) is locked on for a count of 20, which counts down only while A is up. Each update while its target stays locked, the charged shot turns toward it (PlayerShot_UpdateLockOnShot).Codesonicdcer/sf64revisedHow we know
3186bool Player_UpdateLockOn(Player* player) { ⋮ 3190 if (gInputHold->button & A_BUTTON) { 3191 gChargeTimers[player->num]++; 3192 if (gChargeTimers[player->num] > 21) { 3193 gChargeTimers[player->num] = 21; ⋮ 3201 ((gGameFrameCount % 4) == 0) && Player_CanLockOn(player->num)) { 3202 if (gChargeTimers[player->num] > 20) { ⋮ 3206 Player_SetupArwingShot(player, &gPlayerShots[i], 0.0f, 0.0f, PLAYERSHOT_LOCK_SEARCH, ⋮ 3217 if (gChargeTimers[player->num] != 0) { 3218 gChargeTimers[player->num]--;
src/engine/fox_play.c lines 3186–3220 at 4235c59 · upstream ↗ 3164bool Player_CanLockOn(s32 playerNum) { ⋮ 3169 if ((actor->obj.status == OBJ_ACTIVE) && (actor->lockOnTimers[playerNum] != 0)) { 3170 return false;
src/engine/fox_play.c lines 3164–3172 at 4235c59 · upstream ↗ 3222 if (gInputPress->button & A_BUTTON) { 3223 for (i = 0; i < ARRAY_COUNT(gActors); i++) { 3224 if ((gActors[i].obj.status == OBJ_ACTIVE) && (gActors[i].lockOnTimers[player->num] != 0)) { 3225 if ((gPlayerShots[14 - player->num].obj.status == SHOT_FREE) || 3226 (gPlayerShots[14 - player->num].obj.id != PLAYERSHOT_LOCK_ON) || 3227 ((gPlayerShots[14 - player->num].obj.id == PLAYERSHOT_LOCK_ON) && 3228 (gPlayerShots[14 - player->num].unk_60 != 0))) { ⋮ 3230 Player_SetupArwingShot(player, &gPlayerShots[14 - player->num], 0.0f, 0.0f, PLAYERSHOT_LOCK_ON, ⋮ 3239 break; ⋮ 3243 if (gChargeTimers[player->num] > 10) { 3244 if ((gPlayerShots[14 - player->num].obj.status == SHOT_FREE) || 3245 (gPlayerShots[14 - player->num].obj.id != PLAYERSHOT_LOCK_ON) || ⋮
src/engine/fox_play.c lines 3222–3260 at 4235c59 · upstream ↗ 69 gControllerPress[i].button = 70 (gControllerHold[i].button ^ sPrevController[i].button) & gControllerHold[i].button;
src/sys/sys_joybus.c lines 69–70 at 4235c59 · upstream ↗ 1758 if (gLevelMode == LEVELMODE_ON_RAILS) { 1759 lockRange = 300.0f; 1760 } else { 1761 lockRange = 200.0f; ⋮ 1765 if ((actor->obj.status == OBJ_ACTIVE) && (actor->info.targetOffset != 0.0f) && 1766 ((actor->lockOnTimers[shot->sourceId] == 0) && (fabsf(shot->obj.pos.x - actor->obj.pos.x) <= lockRange) && 1767 (fabsf(shot->obj.pos.y - (actor->obj.pos.y + actor->info.targetOffset)) <= lockRange) && 1768 (fabsf(shot->obj.pos.z - actor->obj.pos.z) <= lockRange))) { 1769 actor->lockOnTimers[shot->sourceId] = 20;
src/engine/fox_beam.c lines 1758–1769 at 4235c59 · upstream ↗ 2848 } else if (this->lockOnTimers[TEAM_ID_FOX] != 0) { 2849 if (!(gControllerHold[gMainController].button & A_BUTTON)) { 2850 this->lockOnTimers[TEAM_ID_FOX]--; ⋮ 2852 gChargeTimers[0] = 0;
src/engine/fox_enmy.c lines 2848–2852 at 4235c59 · upstream ↗ 2174 if (shot->unk_60 == 0) { ⋮ 2177 (actor->lockOnTimers[shot->sourceId] != 0)) { 2178 var_a3 = 1; 2179 actor->lockOnTimers[shot->sourceId] = 2; 2180 sLockOnPos = actor->obj.pos; ⋮ 2188 var_a3 = 1; ⋮ 2193 } else { ⋮ 2200 if (var_a3 != 0) { ⋮ 2218 Math_SmoothStepToAngle(&shot->vec_2C.y, sp58, 1.0f, shot->unk_50, 0.0f); 2219 Math_SmoothStepToAngle(&shot->vec_2C.x, sp5C, 1.0f, shot->unk_50, 0.0f); ⋮ 2221 } else { 2222 shot->unk_60 = 1;
src/engine/fox_beam.c lines 2174–2222 at 4235c59 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^ abIn the US 1.1 version of Star Fox 64,
Player_Setupgives the player 255 shield units (less with a Versus handicap), and healing stops atPlay_GetMaxShields: 383 with three or more gold rings, otherwise 255. A hit's damage, doubled in Expert mode, drains from the shield at 2 units an update (Player_UpdateShields), and an enemy laser's damage is 20; a hit whilegHasShieldis set does none. Each Arwing wing starts at 60 health (10 in Expert mode). A hit the code gives direction 1 or 2 takes 20 from the right or left wing, directions 3 and 4 take 15 from one wing at random, and direction 0, which enemy shots pass, takes none; a wing at 0 breaks. While either wing is broken or absent (WINGSTATE_NONE; the test is<= WINGSTATE_BROKEN),Player_Shootsets the lasers back to single every update.Codesonicdcer/sf64revisedHow we know
4405void Player_Setup(Player* playerx) { ⋮ 4414 player->shields = 255; ⋮ 4416 if (gVersusMode) { 4417 switch (gHandicap[gPlayerNum]) { 4418 case 1: 4419 player->shields = 191; ⋮ 4422 player->shields = 127; ⋮ 4430 if (gExpertMode) { 4431 gLeftWingHealth[gPlayerNum] = 10; 4432 gRightWingHealth[gPlayerNum] = 10; 4433 } else { 4434 gLeftWingHealth[gPlayerNum] = 60; ⋮
src/engine/fox_play.c lines 4405–4436 at 4235c59 · upstream ↗ 64s32 Play_GetMaxShields(void) { 65 if (gGoldRingCount[0] >= 3) { 66 return 383; ⋮ 68 return 255;
src/engine/fox_play.c lines 64–69 at 4235c59 · upstream ↗ 5463void Player_UpdateShields(Player* player) { 5464 if (player->damage > 0) { 5465 player->damage -= 2; ⋮ 5469 player->shields -= 2; ⋮ 5476 if (player->heal > 0) { ⋮ 5482 player->shields += 2; 5483 if (player->shields >= Play_GetMaxShields()) { 5484 player->shields = Play_GetMaxShields();
src/engine/fox_play.c lines 5463–5484 at 4235c59 · upstream ↗ 867 if (gExpertMode) { 868 player->damage = damage * 2; 869 } else { 870 player->damage = damage; ⋮ 887 } else { ⋮ 891 if (gHasShield[player->num]) { ⋮ 895 player->damage = 0;
src/engine/fox_play.c lines 867–895 at 4235c59 · upstream ↗ 916 switch (player->hitDirection) { 917 case 0: ⋮ 924 case 1: ⋮ 932 Player_DamageWings(player, 1, 20); ⋮ 935 case 2: ⋮ 943 Player_DamageWings(player, 2, 20); ⋮ 946 case 3: ⋮ 958 if (Rand_ZeroOne() < 0.5f) { 959 Player_DamageWings(player, 1, 15); 960 } else { 961 Player_DamageWings(player, 2, 15); ⋮ 965 case 4: ⋮
src/engine/fox_play.c lines 916–978 at 4235c59 · upstream ↗ 819void Player_DamageWings(Player* player, s32 side, s32 damage) { 820 if ((player->form == FORM_ARWING) && (gShieldAlpha[player->num] < 1.0f)) { 821 if (side == 1) { ⋮ 823 if (player->arwing.rightWingState == WINGSTATE_INTACT) { 824 gRightWingHealth[player->num] -= damage; 825 if (gRightWingHealth[player->num] <= 0) { ⋮ 827 player->arwing.rightWingState = WINGSTATE_BROKEN;
src/engine/fox_play.c lines 819–827 at 4235c59 · upstream ↗ 3299void Player_Shoot(Player* player) { 3300 switch (player->form) { 3301 case FORM_ARWING: 3302 if ((player->arwing.rightWingState <= WINGSTATE_BROKEN) || 3303 (player->arwing.leftWingState <= WINGSTATE_BROKEN)) { 3304 gLaserStrength[player->num] = LASERS_SINGLE;
src/engine/fox_play.c lines 3299–3305 at 4235c59 · upstream ↗ 27 /* 0 */ WINGSTATE_NONE, 28 /* 1 */ WINGSTATE_BROKEN, 29 /* 2 */ WINGSTATE_INTACT
include/sf64player.h lines 27–29 at 4235c59 · upstream ↗ 2925 if ((gPlayer[0].barrelRollAlpha == 0) && (gPlayer[0].mercyTimer == 0)) { 2926 Player_ApplyDamage(&gPlayer[0], 0, this->info.damage);
src/engine/fox_effect.c lines 2925–2926 at 4235c59 · upstream ↗ 472 /* OBJ_EFFECT_ENEMY_LASER */ {(void*) Effect_EnemyLaser_Draw, 1, (ObjectFunc) Effect_EnemyShot_Update, NULL, 100.0f, 0, 0, 20, 0, 0.0f, 0 },src/engine/fox_edata_info.c line 472 at 4235c59 · upstream ↗ 153typedef struct ObjectInfo { ⋮ 161 /* 0x10 */ f32 cullDistance; // z coordinate of something 162 /* 0x14 */ s16 unk_14; // can be -1, 0, 1. governs camera-related behavior in effects (billboarding?) 163 /* 0x16 */ s16 unk_16; // can be 0, 1, 2. affects death behavior? 164 /* 0x18 */ u8 damage;
include/sf64object.h lines 153–168 at 4235c59 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
Sources
Code
- sonicdcer and sf64 contributors. 2026. “sf64: a decompilation of Star Fox 64 (sonicdcer/sf64).” Commit 4235c59. sonicdcer/sf64 @ 4235c59 · licence, all uses