On this page
  1. Related
  2. Mechanics
  3. References
  4. Sources

The atlas reads Sonic Advance 2 through SAT-R’s decompilation, C that builds the US release by default and shares one player file with Sonic Advance. Names quoted here are the decompilers’. Nothing has been measured in the ROM yet.

The jump is Sonic 1’s scaled down. The take-off of 4.875 px/frame1,1, the gravity2, and the release cap3 are each three quarters of Sonic 1’s value. The top speed isn’t scaled: holding forward builds to 6 px/frame, as in Sonic 14. The acceleration toward it rises with the rings held, from 8 subpixels a frame with 10 or fewer to 18 with 150, and letting go slows the run by a fixed 85.

Boost mode works like a hidden P-meter on the running speed. Holding forward at the top speed adds the acceleration to a counter, in the branch where Sonic 2 keeps a speed above the top. A full counter starts boost mode, after 256 frames with 10 rings or fewer and 60 with 1506. Boosting raises the top speed to 12 px/frame. Among the things that end it are a hit, the start of Tails’s or Cream’s flight, and a ground speed below 4.57.

The spin dash is fully charged from the first press. Every press raises the charge to at least the full mark, so the press that starts it already gives the top release speed, 9 px/frame. Sonic 2’s starting press only clears the counter, and its full charge, 12 px/frame, takes five presses in all8.

Mechanics

MechanicKindHow
RunningP-meterholding forward at the 6 px/frame top speed fills a hidden counter, faster with more rings, and boost mode then raises the top speed to 12
JumpOne speed4.875 px/frame along the ground's normal, three quarters of Sonic 1's
GravityOne gravity42/256 px/frame² in a jump, held or not, three quarters of Sonic 1's
Variable jump heightRelease capa release cuts a rise faster than 3 px/frame to 3, three quarters of Sonic 1's cap
Spin dashOne pressone press is a full charge, 9 px/frame on release, which leaks by a 32nd each frame
Every claim about this game, by concept (8)
GravityEach frame of a jump, button held or not, Player_Jumping adds PLAYER_GRAVITY, 42/256 px/frame² (12/256 in water), to the vertical speed through PLAYERFN_UPDATE_AIR_FALL_SPEED, except on a frame where Sonic's forward thrust or a mid-air move starts (Player_Sonic_TryForwardThrust, Player_TryMidAirAction): Player_Jumping returns before the gravity then. That is three quarters of the gravity Sonic 1's airborne-rolling mode, Sonic_MdJump2, adds through ObjectFall, $38 (56/256) a frame (computed).Code
JumpPlayer_InitJump, which doesn't test the character, adds PLAYER_JUMP_HEIGHT, 4.875 px/frame (2.625 in water), to the speed along the ground's normal (rotation - 64). Sonic 1's Sonic_Jump adds son_jumpspeed, $680 (6.5 px/frame), along the normal the same way, so Sonic Advance 2's take-off is three quarters of it (computed).Code
RunningIn Sonic Advance 2 the ground acceleration rises with the ring count. At the start of each frame Player_HandleBoostThreshold picks a bracket from the rings and Player_ApplyBoostPhysics sets the acceleration from playerBoostPhysicsTable: 8 subpixels (1/256 px/frame) a frame with 10 rings or fewer, 12 with 11 to 49, 14 with 50 to 99, 16 with 100 to 149, and 18 with 150 or more. The deceleration, taken when the d-pad is held against the motion, is 64 in every bracket, and the speed-up item doubles both. With neither Left nor Right held, or while unk2A counts down (a 30-frame timer set when the player slips off a steep slope, in the atlas's reading), Player_HandleGroundMovement takes a fixed 8 a frame off the speed toward 0. Sonic 1's rates are $C (12) to accelerate, the same $C for the friction, and $80 (128) against the motion.Code
Outside boost mode Player_ApplyBoostPhysics sets topSpeed, the speed holding forward builds to in Player_HandleGroundMovement, to 6 px/frame (Q(6.0)), Sonic 1's son_maxspeed ($600). A faster speed, from a slope, is cut only at maxSpeed, by sub_8023260, which the ground state (Player_Idle) calls after adding the slope term: 9 px/frame, or 15 while MOVESTATE_SPIN_ATTACK is set, as a spin dash sets it.Code
Boost mode starts from a run held at the top speed. Holding forward on the ground at 6 px/frame or faster, Player_HandleGroundMovement leaves the speed as it is and adds the frame's acceleration to boostSpeed, in the branch where Sonic 2's Sonic_MoveRight keeps a speed above the top and Sonic 1's cuts it back to the top. At the start of each frame Player_HandleBoostState turns boost mode on once boostSpeed reaches the ring bracket's entry in playerBoostThresholdTable: Q(8) (2048) with 10 rings or fewer, 2040 with 11 to 49, 1680 with 50 to 99, 1440 with 100 to 149, and 1080 with 150 or more. In the air or below the top speed it sets boostSpeed back to 0. With each bracket's acceleration that is 256, 170, 120, 90, and 60 frames of holding forward at the top speed, and half as many with the speed-up item (computed).Code
In boost mode Player_ApplyBoostPhysics raises topSpeed, the speed holding forward builds to, from 6 to 12 px/frame, twice Sonic 1's single top speed (son_maxspeed, $600; computed), and maxSpeed, the ground cap, to 15; the acceleration still follows the ring count. Player_HandleBoostState looks for the end of boost mode only on the ground, where a ground speed below 4.5 px/frame ends it. Among the other ends are a hit (Player_InitHurt), the start of Tails's or Cream's flight, a multiplayer finish, the special stage transition, and InitializePlayer.Code
Spin dashWhile the spin dash charges, each jump press adds Q(2.0) (512) to qSpindashAccel and then raises it to at least Q(8.0) (2048) with MAX, so one press is a full charge, the starting press included: Player_TryInitSpindash runs Player_InitSpindash and then Player_Spindash in the same frame. Each frame Down stays held first takes 1/32 off the charge. Releasing Down sets the ground speed from sSpinDashSpeeds at the charge's whole part, capped at 8: 6 px/frame plus 3/8 a step, up to 9, so a release on the frame after a press always gives 9 px/frame (computed). In Sonic 2 the starting press sets spindash_counter to 0, and each later press adds $200 up to a cap of $800, so a full charge takes five presses in all (computed); its release speeds run from 8 to 12 px/frame.Code
Variable jump heightIn a jump (MOVESTATE_100, which Player_InitJump sets), on any frame the jump button isn't held, Player_Jumping cuts a rise faster than 3 px/frame to exactly −3 (PLAYER_MAX_NOT_HELD_JUMP_FORCE; −1.5 in water). Outside the Collect Rings multiplayer mode it first tries Sonic's forward thrust and the mid-air moves (Player_Sonic_TryForwardThrust, Player_TryMidAirAction), and a frame on which one starts skips the cap. Sonic 1's Sonic_JumpHeight cuts to 4 px/frame (-$400), so this cap too is three quarters of Sonic 1's (computed).Code

References

  1. ^ abPlayer_InitJump, which doesn't test the character, adds PLAYER_JUMP_HEIGHT, 4.875 px/frame (2.625 in water), to the speed along the ground's normal (rotation - 64). Sonic 1's Sonic_Jump adds son_jumpspeed, $680 (6.5 px/frame), along the normal the same way, so Sonic Advance 2's take-off is three quarters of it (computed).CodeSAT-R/sa2 · sonicretro/s1disasm
    How we know
    23#define PLAYER_JUMP_HEIGHT             (4.875)
    24#define PLAYER_JUMP_HEIGHT_UNDER_WATER (2.625)
    
    include/game/shared/parameters/characters.h lines 23–24 at 393357f · upstream ↗
    6939void Player_InitJump(Player *p)
    ⋮
    6980    jumpHeight = (p->moveState & MOVESTATE_IN_WATER) ? Q(PLAYER_JUMP_HEIGHT_UNDER_WATER) : Q(PLAYER_JUMP_HEIGHT);
    ⋮
    6985    rot = p->rotation - 64;
    ⋮
    6987    accelX = I(COS_24_8(rot * 4) * jumpHeight);
    6988    p->qSpeedAirX += accelX;
    ⋮
    6990    accelY = I(SIN_24_8(rot * 4) * jumpHeight);
    6991    p->qSpeedAirY += accelY;
    
    src/game/shared/stage/player.c lines 6939–7004 at 393357f · upstream ↗
    8son_jumpspeed:		equ	$680				; Sonic's jump speed
    
    _incObj/01 Sonic.asm line 8 at 15dfa3a · upstream ↗
    1218		move.w	#son_jumpspeed,d2			; set initial jump force
    
    _incObj/01 Sonic.asm line 1218 at 15dfa3a · upstream ↗
    1226		subi.b	#$40,d0					; rotate it perpendicularly for jump trajectory
    1227		jsr	(CalcSine).l				; find the direction Sonic should jump
    
    _incObj/01 Sonic.asm lines 1224–1233 at 15dfa3a · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  2. ^Each frame of a jump, button held or not, Player_Jumping adds PLAYER_GRAVITY, 42/256 px/frame² (12/256 in water), to the vertical speed through PLAYERFN_UPDATE_AIR_FALL_SPEED, except on a frame where Sonic's forward thrust or a mid-air move starts (Player_Sonic_TryForwardThrust, Player_TryMidAirAction): Player_Jumping returns before the gravity then. That is three quarters of the gravity Sonic 1's airborne-rolling mode, Sonic_MdJump2, adds through ObjectFall, $38 (56/256) a frame (computed).CodeSAT-R/sa2 · sonicretro/s1disasmrevised
    How we know
    18#define PLAYER_GRAVITY             (42.0 / 256.0)
    19#define PLAYER_GRAVITY_UNDER_WATER (12.0 / 256.0)
    
    include/game/shared/parameters/characters.h lines 18–19 at 393357f · upstream ↗
    200#elif (GAME == GAME_SA2)
    201#ifndef COLLECT_RINGS_ROM
    202#define PLAYERFN_UPDATE_AIR_FALL_SPEED(player)                                                                                             \
    203    if (player->moveState & MOVESTATE_IN_WATER) {                                                                                          \
    204        player->qSpeedAirY += Q(PLAYER_GRAVITY_UNDER_WATER);                                                                               \
    205    } else {                                                                                                                               \
    206        player->qSpeedAirY += Q(PLAYER_GRAVITY);                                                                                           \
    
    src/game/shared/stage/player.c lines 200–207 at 393357f · upstream ↗
    7017    if (p->moveState & MOVESTATE_100) {
    7018#ifndef COLLECT_RINGS_ROM
    7019        if (gGameMode != GAME_MODE_MULTI_PLAYER_COLLECT_RINGS)
    7020            if (Player_Sonic_TryForwardThrust(p) || Player_TryMidAirAction(p))
    7021                return;
    
    src/game/shared/stage/player.c lines 7017–7021 at 393357f · upstream ↗
    7039    SA2_LABEL(sub_80232D0)(p);
    ⋮
    7041    PLAYERFN_UPDATE_AIR_FALL_SPEED(p);
    7042    PLAYERFN_UPDATE_POSITION(p);
    
    src/game/shared/stage/player.c lines 7039–7042 at 393357f · upstream ↗
    5gravity:	equ	$38				; gravity constant used by many objects
    
    _incObj/sub ObjectFall & SpeedToPos.asm line 5 at 15dfa3a · upstream ↗
    359Sonic_MdJump2:	; While Sonic is in the air and rolling (usually, but not limited to, jumping)
    ⋮
    363		jsr	(ObjectFall).l				; apply gravity and update Sonic's position based on his current velocities
    
    _incObj/01 Sonic.asm lines 359–363 at 15dfa3a · 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.

    In the claim ledger

  3. ^In a jump (MOVESTATE_100, which Player_InitJump sets), on any frame the jump button isn't held, Player_Jumping cuts a rise faster than 3 px/frame to exactly −3 (PLAYER_MAX_NOT_HELD_JUMP_FORCE; −1.5 in water). Outside the Collect Rings multiplayer mode it first tries Sonic's forward thrust and the mid-air moves (Player_Sonic_TryForwardThrust, Player_TryMidAirAction), and a frame on which one starts skips the cap. Sonic 1's Sonic_JumpHeight cuts to 4 px/frame (-$400), so this cap too is three quarters of Sonic 1's (computed).CodeSAT-R/sa2 · sonicretro/s1disasm
    How we know
    28#define PLAYER_MAX_NOT_HELD_JUMP_FORCE             (3.0)
    29#define PLAYER_MAX_NOT_HELD_JUMP_FORCE_UNDER_WATER (1.5)
    
    include/game/shared/parameters/characters.h lines 28–29 at 393357f · upstream ↗
    6965    p->moveState |= (MOVESTATE_100 | MOVESTATE_IN_AIR);
    
    src/game/shared/stage/player.c line 6965 at 393357f · upstream ↗
    7006void Player_Jumping(Player *p)
    ⋮
    7009    s16 maxJumpSpeed = -Q(PLAYER_MAX_NOT_HELD_JUMP_FORCE);
    ⋮
    7012    if (p->moveState & MOVESTATE_IN_WATER) {
    7013        maxJumpSpeed = -Q(PLAYER_MAX_NOT_HELD_JUMP_FORCE_UNDER_WATER);
    ⋮
    7017    if (p->moveState & MOVESTATE_100) {
    ⋮
    7019        if (gGameMode != GAME_MODE_MULTI_PLAYER_COLLECT_RINGS)
    7020            if (Player_Sonic_TryForwardThrust(p) || Player_TryMidAirAction(p))
    7021                return;
    ⋮
    7024        // Caps the jump force if the player lets go of the jump button
    7025        if (p->qSpeedAirY < maxJumpSpeed && !(p->heldInput & gPlayerControls.jump)) {
    7026            p->qSpeedAirY = maxJumpSpeed;
    
    src/game/shared/stage/player.c lines 7006–7027 at 393357f · upstream ↗
    1273Sonic_JumpHeight:
    ⋮
    1276		move.w	#-$400,d1				; set max jump height
    ⋮
    1283		cmp.w	obVelY(a0),d1				; get current y speed
    1284		ble.s	.return					; is Sonic moving up slower than the cap speed? if yes, branch
    1285		move.b	(v_jpadhold2).w,d0			; get currently held buttons
    1286		andi.b	#btnABC,d0				; is A, B or C still held after jumping?
    1287		bne.s	.return					; if yes, branch
    1288		move.w	d1,obVelY(a0)				; otherwise, cap Sonic's maximum jump height
    
    _incObj/01 Sonic.asm lines 1273–1288 at 15dfa3a · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  4. ^Outside boost mode Player_ApplyBoostPhysics sets topSpeed, the speed holding forward builds to in Player_HandleGroundMovement, to 6 px/frame (Q(6.0)), Sonic 1's son_maxspeed ($600). A faster speed, from a slope, is cut only at maxSpeed, by sub_8023260, which the ground state (Player_Idle) calls after adding the slope term: 9 px/frame, or 15 while MOVESTATE_SPIN_ATTACK is set, as a spin dash sets it.CodeSAT-R/sa2 · sonicretro/s1disasm
    How we know
    9912void Player_ApplyBoostPhysics(Player *p)
    ⋮
    9914    u32 boostThreshold = p->boostThreshold;
    9915    if (p->isBoosting) {
    9916        p->topSpeed = Q(12.0);
    9917        p->maxSpeed = Q(15.0);
    9918    } else if (p->moveState & MOVESTATE_SPIN_ATTACK) {
    9919        p->topSpeed = Q(6.0);
    9920        p->maxSpeed = Q(15.0);
    9921    } else {
    9922        p->topSpeed = Q(6.0);
    9923        p->maxSpeed = Q(9.0);
    
    src/game/shared/stage/player.c lines 9912–9924 at 393357f · upstream ↗
    8974                if (p->qSpeedGround < p->topSpeed) {
    8975                    p->qSpeedGround += acc;
    ⋮
    8977                    if (p->qSpeedGround > p->topSpeed) {
    8978                        p->qSpeedGround = p->topSpeed;
    
    src/game/shared/stage/player.c lines 8974–8979 at 393357f · upstream ↗
    6658        Player_HandleGroundMovement(p);
    ⋮
    6660        if (((p->rotation + Q(0.375)) & 0xFF) < 0xC0) {
    6661            s32 acceleration = GET_ROTATED_ACCEL(p->rotation);
    ⋮
    6664                p->qSpeedGround += acceleration;
    ⋮
    6669        SA2_LABEL(sub_8023260)(p);
    
    src/game/shared/stage/player.c lines 6658–6669 at 393357f · upstream ↗
    3383void SA2_LABEL(sub_8023260)(Player *p)
    ⋮
    3385    s32 maxSpeed = p->maxSpeed;
    ⋮
    3387    if (p->qSpeedGround > (s16)maxSpeed) {
    3388        p->qSpeedGround = +maxSpeed;
    
    src/game/shared/stage/player.c lines 3383–3388 at 393357f · upstream ↗
    7190    p->moveState |= (MOVESTATE_SPINDASH | MOVESTATE_SPIN_ATTACK);
    
    src/game/shared/stage/player.c line 7190 at 393357f · upstream ↗
    5son_maxspeed:		equ	$600				; Sonic's max speed
    
    _incObj/01 Sonic.asm line 5 at 15dfa3a · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  5. ^In Sonic Advance 2 the ground acceleration rises with the ring count. At the start of each frame Player_HandleBoostThreshold picks a bracket from the rings and Player_ApplyBoostPhysics sets the acceleration from playerBoostPhysicsTable: 8 subpixels (1/256 px/frame) a frame with 10 rings or fewer, 12 with 11 to 49, 14 with 50 to 99, 16 with 100 to 149, and 18 with 150 or more. The deceleration, taken when the d-pad is held against the motion, is 64 in every bracket, and the speed-up item doubles both. With neither Left nor Right held, or while unk2A counts down (a 30-frame timer set when the player slips off a steep slope, in the atlas's reading), Player_HandleGroundMovement takes a fixed 8 a frame off the speed toward 0. Sonic 1's rates are $C (12) to accelerate, the same $C for the friction, and $80 (128) against the motion.CodeSAT-R/sa2 · sonicretro/s1disasm
    How we know
    4899#if (GAME == GAME_SA2)
    4900    Player_HandleBoostThreshold(p);
    4901    Player_HandleBoostState(p);
    4902    Player_ApplyBoostPhysics(p);
    
    src/game/shared/stage/player.c lines 4899–4902 at 393357f · upstream ↗
    9107void Player_HandleBoostThreshold(Player *p)
    ⋮
    9109    if (gRingCount >= 150) {
    9110        p->boostThreshold = 4;
    9111    } else if (gRingCount >= 100) {
    9112        p->boostThreshold = 3;
    9113    } else if (gRingCount >= 50) {
    9114        p->boostThreshold = 2;
    9115    } else if (gRingCount > 10) {
    9116        p->boostThreshold = 1;
    9117    } else {
    9118        p->boostThreshold = 0;
    
    src/game/shared/stage/player.c lines 9107–9120 at 393357f · upstream ↗
    552static const s16 playerBoostPhysicsTable[5][2] = {
    553    { 8, 64 }, { 12, 64 }, { 14, 64 }, { 16, 64 }, { 18, 64 },
    
    src/game/shared/stage/player.c lines 552–553 at 393357f · upstream ↗
    9926    p->acceleration = playerBoostPhysicsTable[boostThreshold][0];
    9927    p->deceleration = playerBoostPhysicsTable[boostThreshold][1];
    
    src/game/shared/stage/player.c lines 9926–9927 at 393357f · upstream ↗
    4907    Player_HandleInputs(p);
    ⋮
    4910    Player_HandleWater(p);
    
    src/game/shared/stage/player.c lines 4907–4910 at 393357f · upstream ↗
    4429    if (p->itemEffect & PLAYER_ITEM_EFFECT__SPEED_UP) {
    ⋮
    4432        p->acceleration = p->acceleration * 2;
    4433        p->deceleration = p->deceleration * 2;
    4434#else
    4435        p->acceleration = p->acceleration * 2;
    4436        p->deceleration = p->deceleration * 2;
    
    src/game/shared/stage/player.c lines 4429–4437 at 393357f · upstream ↗
    6652    if (!Player_TryTaunt(p) && !Player_TryCrouchOrSpinAttack(p) && !Player_TryJump(p)
    ⋮
    6658        Player_HandleGroundMovement(p);
    
    src/game/shared/stage/player.c lines 6652–6658 at 393357f · upstream ↗
    8965void Player_HandleGroundMovement(Player *p)
    ⋮
    8967    s8 charState = CHARSTATE_INVALID;
    8968    s32 acc = p->acceleration;
    8969    s32 dec = p->deceleration;
    ⋮
    8971    if ((p->SA2_LABEL(unk2A) == 0) && p->heldInput & (DPAD_LEFT | DPAD_RIGHT)) {
    8972        if (p->qSpeedGround > 0) {
    8973            if (p->heldInput & DPAD_RIGHT) {
    8974                if (p->qSpeedGround < p->topSpeed) {
    8975                    p->qSpeedGround += acc;
    
    src/game/shared/stage/player.c lines 8965–8975 at 393357f · upstream ↗
    8984            } else if (p->qSpeedGround >= Q(2.0)) {
    ⋮
    8994                p->qSpeedGround -= dec;
    
    src/game/shared/stage/player.c lines 8984–8994 at 393357f · upstream ↗
    9063    } else {
    9064        s32 grndSpeed = p->qSpeedGround;
    9065        if (grndSpeed > 0) {
    9066            s16 val = grndSpeed - Q(8.0 / 256.0);
    9067            if (val <= 0) {
    9068                val = 0;
    ⋮
    9070            } else {
    
    src/game/shared/stage/player.c lines 9063–9074 at 393357f · upstream ↗
    118#define PLAYERFN_UPDATE_UNK2A(player)                                                                                                      \
    119    {                                                                                                                                      \
    120        if (player->SA2_LABEL(unk2A) != 0) {                                                                                               \
    121            player->SA2_LABEL(unk2A) -= 1;                                                                                                 \
    122        } else if ((player->rotation + Q(0.125)) & 0xC0) {                                                                                 \
    123            if (ABS(player->qSpeedGround) < Q(1.875)) {                                                                                    \
    ⋮
    127                player->SA2_LABEL(unk2A) = GBA_FRAMES_PER_SECOND / 2;                                                                      \
    
    src/game/shared/stage/player.c lines 118–127 at 393357f · upstream ↗
    5son_maxspeed:		equ	$600				; Sonic's max speed
    6son_acceleration:	equ	$C				; Sonic's acceleration
    7son_deceleration:	equ	$80				; Sonic's deceleration
    
    _incObj/01 Sonic.asm lines 5–7 at 15dfa3a · upstream ↗
    499Sonic_CheckDpadLetGo:
    ⋮
    501		andi.b	#btnL+btnR,d0				; is left or right held?
    ⋮
    506		sub.w	d5,d0					; reduce current rightward speed by acceleration
    
    _incObj/01 Sonic.asm lines 499–506 at 15dfa3a · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  6. ^Boost mode starts from a run held at the top speed. Holding forward on the ground at 6 px/frame or faster, Player_HandleGroundMovement leaves the speed as it is and adds the frame's acceleration to boostSpeed, in the branch where Sonic 2's Sonic_MoveRight keeps a speed above the top and Sonic 1's cuts it back to the top. At the start of each frame Player_HandleBoostState turns boost mode on once boostSpeed reaches the ring bracket's entry in playerBoostThresholdTable: Q(8) (2048) with 10 rings or fewer, 2040 with 11 to 49, 1680 with 50 to 99, 1440 with 100 to 149, and 1080 with 150 or more. In the air or below the top speed it sets boostSpeed back to 0. With each bracket's acceleration that is 256, 170, 120, 90, and 60 frames of holding forward at the top speed, and half as many with the speed-up item (computed).CodeSAT-R/sa2 · sonicretro/s2disasm · sonicretro/s1disasm
    How we know
    8972        if (p->qSpeedGround > 0) {
    8973            if (p->heldInput & DPAD_RIGHT) {
    8974                if (p->qSpeedGround < p->topSpeed) {
    ⋮
    8980                } else {
    8981                    p->boostSpeed += acc;
    
    src/game/shared/stage/player.c lines 8972–8982 at 393357f · upstream ↗
    9009        } else if (p->qSpeedGround < 0) {
    9010            if (p->heldInput & DPAD_LEFT) {
    9011                if (p->qSpeedGround > -p->topSpeed) {
    ⋮
    9017                } else {
    9018                    p->boostSpeed += acc;
    
    src/game/shared/stage/player.c lines 9009–9019 at 393357f · upstream ↗
    556static const s16 playerBoostThresholdTable[5] = { Q(8), Q(7.96875), Q(6.5625), Q(5.625), Q(4.21875) };
    
    src/game/shared/stage/player.c line 556 at 393357f · upstream ↗
    4899#if (GAME == GAME_SA2)
    4900    Player_HandleBoostThreshold(p);
    4901    Player_HandleBoostState(p);
    4902    Player_ApplyBoostPhysics(p);
    
    src/game/shared/stage/player.c lines 4899–4902 at 393357f · upstream ↗
    9122void Player_HandleBoostState(Player *p)
    ⋮
    9124    bool32 isBoostActive = p->isBoosting;
    9125    if (isBoostActive) {
    ⋮
    9134    } else {
    9135        if ((!(p->moveState & MOVESTATE_IN_AIR)) && ((ABS(p->qSpeedGround) >= p->topSpeed))) {
    9136            if (p->boostSpeed >= playerBoostThresholdTable[p->boostThreshold]) {
    9137                p->isBoosting = TRUE;
    ⋮
    9144        } else {
    9145#ifndef NON_MATCHING
    9146            p->boostSpeed = isBoostActive;
    
    src/game/shared/stage/player.c lines 9122–9150 at 393357f · upstream ↗
    552static const s16 playerBoostPhysicsTable[5][2] = {
    553    { 8, 64 }, { 12, 64 }, { 14, 64 }, { 16, 64 }, { 18, 64 },
    
    src/game/shared/stage/player.c lines 552–553 at 393357f · upstream ↗
    4429    if (p->itemEffect & PLAYER_ITEM_EFFECT__SPEED_UP) {
    ⋮
    4432        p->acceleration = p->acceleration * 2;
    ⋮
    4435        p->acceleration = p->acceleration * 2;
    
    src/game/shared/stage/player.c lines 4429–4437 at 393357f · upstream ↗
    36998	add.w	d5,d0	; add acceleration to the right
    36999	cmp.w	d6,d0	; compare new speed with top speed
    37000	blt.s	+		; if new speed is less than the maximum, branch
    37001	sub.w	d5,d0	; remove this frame's acceleration change
    37002	cmp.w	d6,d0	; compare speed with top speed
    37003	bge.s	+		; if speed was already greater than the maximum, branch
    37004	move.w	d6,d0	; limit speed on ground going right
    
    s2.asm lines 36998–37004 at 4ef457c · upstream ↗
    716		add.w	d5,d0					; add acceleration to current ground speed
    717		cmp.w	d6,d0					; is new speed above max speed?
    718		blt.s	.nocap					; if not, branch
    719		move.w	d6,d0					; cap Sonic's ground speed
    
    _incObj/01 Sonic.asm lines 716–719 at 15dfa3a · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  7. ^In boost mode Player_ApplyBoostPhysics raises topSpeed, the speed holding forward builds to, from 6 to 12 px/frame, twice Sonic 1's single top speed (son_maxspeed, $600; computed), and maxSpeed, the ground cap, to 15; the acceleration still follows the ring count. Player_HandleBoostState looks for the end of boost mode only on the ground, where a ground speed below 4.5 px/frame ends it. Among the other ends are a hit (Player_InitHurt), the start of Tails's or Cream's flight, a multiplayer finish, the special stage transition, and InitializePlayer.CodeSAT-R/sa2 · sonicretro/s1disasmrevised
    How we know
    9912void Player_ApplyBoostPhysics(Player *p)
    ⋮
    9914    u32 boostThreshold = p->boostThreshold;
    9915    if (p->isBoosting) {
    9916        p->topSpeed = Q(12.0);
    9917        p->maxSpeed = Q(15.0);
    ⋮
    9920        p->maxSpeed = Q(15.0);
    ⋮
    9926    p->acceleration = playerBoostPhysicsTable[boostThreshold][0];
    
    src/game/shared/stage/player.c lines 9912–9927 at 393357f · upstream ↗
    9122void Player_HandleBoostState(Player *p)
    ⋮
    9124    bool32 isBoostActive = p->isBoosting;
    9125    if (isBoostActive) {
    9126        if (!(p->moveState & MOVESTATE_IN_AIR)) {
    9127            p->boostSpeed = playerBoostThresholdTable[p->boostThreshold];
    ⋮
    9129            if (ABS(p->qSpeedGround) < Q(4.5)) {
    9130                p->isBoosting = FALSE;
    9131                p->boostSpeed = 0;
    
    src/game/shared/stage/player.c lines 9122–9133 at 393357f · upstream ↗
    7856void Player_InitHurt(Player *p)
    ⋮
    7858    p->timerInvulnerability = 0x78;
    7859    p->isBoosting = FALSE;
    
    src/game/shared/stage/player.c lines 7856–7859 at 393357f · upstream ↗
    893void Player_Tails_InitFlying(Player *p)
    ⋮
    905    p->isBoosting = FALSE;
    
    src/game/sa2/stage/player_callbacks.c lines 893–905 at 393357f · upstream ↗
    632void Player_Cream_InitFlying(Player *p)
    ⋮
    644    p->isBoosting = FALSE;
    
    src/game/sa2/stage/player_callbacks.c lines 632–644 at 393357f · upstream ↗
    8193void Player_HandleMultiplayerFinish(Player *p)
    ⋮
    8230        p->isBoosting = FALSE;
    
    src/game/shared/stage/player.c lines 8193–8230 at 393357f · upstream ↗
    10050void Player_InitSpecialStageTransition(Player *p)
    ⋮
    10057    p->isBoosting = FALSE;
    
    src/game/shared/stage/player.c lines 10050–10057 at 393357f · upstream ↗
    976void InitializePlayer(Player *p)
    ⋮
    987    if (
    ⋮
    999#if (GAME == GAME_SA2)
    ⋮
    1059#if (GAME == GAME_SA2)
    1060    p->transition = 0;
    1061    p->unk6E = 0;
    1062    p->prevTransition = 0;
    1063    p->isBoosting = FALSE;
    
    src/game/shared/stage/player.c lines 976–1063 at 393357f · upstream ↗
    5son_maxspeed:		equ	$600				; Sonic's max speed
    
    _incObj/01 Sonic.asm line 5 at 15dfa3a · 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.

    In the claim ledger

  8. ^While the spin dash charges, each jump press adds Q(2.0) (512) to qSpindashAccel and then raises it to at least Q(8.0) (2048) with MAX, so one press is a full charge, the starting press included: Player_TryInitSpindash runs Player_InitSpindash and then Player_Spindash in the same frame. Each frame Down stays held first takes 1/32 off the charge. Releasing Down sets the ground speed from sSpinDashSpeeds at the charge's whole part, capped at 8: 6 px/frame plus 3/8 a step, up to 9, so a release on the frame after a press always gives 9 px/frame (computed). In Sonic 2 the starting press sets spindash_counter to 0, and each later press adds $200 up to a cap of $800, so a full charge takes five presses in all (computed); its release speeds run from 8 to 12 px/frame.CodeSAT-R/sa2 · sonicretro/s2disasmrevised
    How we know
    9898bool32 Player_TryInitSpindash(Player *p)
    ⋮
    9900    u32 r6 = 0;
    9901    if (p->charState == CHARSTATE_CROUCH) {
    9902        if (p->frameInput & gPlayerControls.jump) {
    9903            PLAYERFN_SET_AND_CALL(Player_InitSpindash, p);
    
    src/game/shared/stage/player.c lines 9898–9904 at 393357f · upstream ↗
    7186void Player_InitSpindash(Player *p)
    ⋮
    7195    p->qSpindashAccel = 0;
    ⋮
    7203    PLAYERFN_SET_AND_CALL(Player_Spindash, p);
    
    src/game/shared/stage/player.c lines 7186–7203 at 393357f · upstream ↗
    7212    if (!(p->heldInput & DPAD_DOWN)) {
    ⋮
    7217        index = I(p->qSpindashAccel);
    7218        if (index > 8)
    7219            index = 8;
    ⋮
    7221        speed = sSpinDashSpeeds[index];
    ⋮
    7225        p->qSpeedGround = speed;
    
    src/game/shared/stage/player.c lines 7212–7225 at 393357f · upstream ↗
    7230    } else {
    7231        s16 pitch = p->qSpindashAccel;
    ⋮
    7233        s16 pitch2 = pitch;
    7234        if (pitch2 != 0) {
    7235            pitch = pitch2 - (I(pitch << 3));
    ⋮
    7240        if (p->frameInput & gPlayerControls.jump) {
    ⋮
    7248            pitch += Q(2.0);
    7249            pitch = MAX(pitch, Q(8.0));
    
    src/game/shared/stage/player.c lines 7230–7249 at 393357f · upstream ↗
    558static const s16 sSpinDashSpeeds[9] = {
    559    Q_8_8(6.000 + 0 * (3. / 8.)), //
    ⋮
    567    Q_8_8(6.000 + 8 * (3. / 8.)), //
    
    src/game/shared/stage/player.c lines 558–567 at 393357f · upstream ↗
    37605	move.b	(Ctrl_1_Press_Logical).w,d0
    37606	andi.b	#button_B_mask|button_C_mask|button_A_mask,d0
    37607	beq.w	return_1AC8C
    ⋮
    37612	move.b	#1,spindash_flag(a0)
    37613	move.w	#0,spindash_counter(a0)
    
    s2.asm lines 37605–37613 at 4ef457c · upstream ↗
    37726	addi.w	#$200,spindash_counter(a0)
    37727	cmpi.w	#$800,spindash_counter(a0)
    37728	blo.s	Obj01_Spindash_ResetScr
    37729	move.w	#$800,spindash_counter(a0)
    
    s2.asm lines 37726–37729 at 4ef457c · upstream ↗
    37688SpindashSpeeds:
    37689	dc.w  $800	; 0
    ⋮
    37697	dc.w  $C00	; 8
    
    s2.asm lines 37688–37697 at 4ef457c · 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.

    In the claim ledger

Sources

Code