when rotating the fleet, ships goes out of sync in term of speed letting the player spam a direction to build up speed infinitely #72

Closed
opened 2026-08-16 16:15:43 +00:00 by metaheavens · 2 comments
Owner
No description provided.
Author
Owner

Implemented and pushed in d68aac6.

A fast series of tiny taps on the pivot control could each individually queue the minimum pivot step, stacking up to a rotation rate faster than holding the control down - an exploit around the intended hold-to-rotate speed cap. Added a per-direction cooldown (PIVOT_TAP_COOLDOWN, derived from the existing PIVOT_MIN_STEP/PIVOT_SPEED) so a tap in the same direction can't be repeated faster than a held rotation would achieve. Tracked separately for CW/CCW so a legitimate quick direction-reversal (tap right, immediately tap left) still isn't throttled by the other direction's cooldown.

Tests: extended tests/test_pivot_controls.gd (bash tests/run_tests.sh pivot_controls) with a spam-immunity assertion - 5 rapid same-direction taps produce ~0 additional rotation beyond the first.

Implemented and pushed in `d68aac6`. A fast series of tiny taps on the pivot control could each individually queue the minimum pivot step, stacking up to a rotation rate faster than holding the control down - an exploit around the intended hold-to-rotate speed cap. Added a per-direction cooldown (`PIVOT_TAP_COOLDOWN`, derived from the existing `PIVOT_MIN_STEP`/`PIVOT_SPEED`) so a tap in the same direction can't be repeated faster than a held rotation would achieve. Tracked separately for CW/CCW so a legitimate quick direction-reversal (tap right, immediately tap left) still isn't throttled by the other direction's cooldown. Tests: extended `tests/test_pivot_controls.gd` (`bash tests/run_tests.sh pivot_controls`) with a spam-immunity assertion - 5 rapid same-direction taps produce ~0 additional rotation beyond the first.
Author
Owner

Reopening the record on this one: the fix I closed this with was not a fix. The pivot cooldown throttled how often the exploit could be triggered and never touched the cause. The reporter confirmed the behaviour was still there. Actually fixed in 67cb853.

The cause

Fleet.stop() did:

var motion: Vector2 = ship.velocity + ship.fleet_velocity
ship.velocity = motion
ship.fleet_velocity = Vector2.ZERO

_assign_slot_targets() refills fleet_velocity on the next steer, so every press-release cycle added a whole cruise velocity to the ship's own, decaying only at acceleration in between. Tap faster than that bleeds it off and they stack without limit. Ships fell out of sync because each bled off at a rate set by its own distance to its slot - which is exactly the "goes out of sync in term of speed" in the title.

Measured against the pre-fix code, spamming a direction while pivoting:

held steady for 3.0s: travelled  784.3, fastest vessel  0.0x
spammed with pivots:  travelled 32737.5, fastest vessel 48.0x

42x faster than flying honestly, with a vessel at 48x its own top speed. Note fastest vessel 0.0x on the honest leg - that is the bug's other half showing through.

Why it was there

ship.velocity was not the ship's real motion. Ship snapped to its slot every tick and zeroed it, so a vessel cruising at full speed reported zero, and the truth lived in a second field that callers had to add back in. stop() was compensating for that lie. Everything else - the frame-rate-dependent behaviour, the "pause then it moves again" stutter this code had already been patched for - came from the same place.

The fix

Fixed at the root rather than by clamping the symptom, along the lines the reporter suggested (simplify how the fleet and vessels move together):

  • fleet_velocity is now fed forward into the steering instead of carried alongside it. The ship aims to travel at its slot's velocity plus a correction closing the gap, so holding formation costs no error and velocity is always true world motion.
  • The snap applies only when the slot is standing still - the one case where converging asymptotically leaves a residue. While the slot moves, the ship steers continuously.
  • A hard bound on the integrated result every tick, not merely on the request, so no sequence of commands can leave a vessel above its own top speed.
  • Fleet.stop() injects nothing; it reports that the slot has halted.

Test

tests/test_fleet_speed_bounded.gd, verified to fail against the previous code with the numbers above. It covers: no vessel exceeds its own top speed however hard a direction is spammed; spamming cannot cover more ground than honestly holding the stick; both hold while the formation is being pivoted, the case this ticket names; and the fleet stays together rather than fanning out. Speeds are tracked as a ratio of each vessel's own limit, so a mixed fleet is judged per vessel.

One knock-on worth recording: making velocity honest changed test_space_time_distorder.gd's baseline from "formation lag 0.0" to 51.6 - the old zero was the teleport-snap lying about a ship that was in fact moving. That test also now clears the asteroid and enemy groups at setup (the idiom test_ship_lab.gd already uses); silencing the spawners was never enough, and losing the distorder vessel to a rock resets the multiplier correctly, which it had been reporting three stages later as a failed scene swap.

Full suite green at 94/94.

Reopening the record on this one: **the fix I closed this with was not a fix.** The pivot cooldown throttled how often the exploit could be triggered and never touched the cause. The reporter confirmed the behaviour was still there. Actually fixed in 67cb853. ## The cause `Fleet.stop()` did: ```gdscript var motion: Vector2 = ship.velocity + ship.fleet_velocity ship.velocity = motion ship.fleet_velocity = Vector2.ZERO ``` `_assign_slot_targets()` refills `fleet_velocity` on the next steer, so **every press-release cycle added a whole cruise velocity to the ship's own**, decaying only at `acceleration` in between. Tap faster than that bleeds it off and they stack without limit. Ships fell out of sync because each bled off at a rate set by its own distance to its slot - which is exactly the "goes out of sync in term of speed" in the title. Measured against the pre-fix code, spamming a direction *while pivoting*: ``` held steady for 3.0s: travelled 784.3, fastest vessel 0.0x spammed with pivots: travelled 32737.5, fastest vessel 48.0x ``` **42x faster than flying honestly, with a vessel at 48x its own top speed.** Note `fastest vessel 0.0x` on the honest leg - that is the bug's other half showing through. ## Why it was there `ship.velocity` was not the ship's real motion. Ship snapped to its slot every tick and zeroed it, so a vessel cruising at full speed reported zero, and the truth lived in a second field that callers had to add back in. `stop()` was compensating for that lie. Everything else - the frame-rate-dependent behaviour, the "pause then it moves again" stutter this code had already been patched for - came from the same place. ## The fix Fixed at the root rather than by clamping the symptom, along the lines the reporter suggested (simplify how the fleet and vessels move together): - `fleet_velocity` is now fed **forward** into the steering instead of carried alongside it. The ship aims to travel at its slot's velocity plus a correction closing the gap, so holding formation costs no error and `velocity` is always true world motion. - The snap applies **only when the slot is standing still** - the one case where converging asymptotically leaves a residue. While the slot moves, the ship steers continuously. - A hard bound on the **integrated result** every tick, not merely on the request, so no sequence of commands can leave a vessel above its own top speed. - `Fleet.stop()` injects nothing; it reports that the slot has halted. ## Test `tests/test_fleet_speed_bounded.gd`, verified to fail against the previous code with the numbers above. It covers: no vessel exceeds its own top speed however hard a direction is spammed; spamming cannot cover more ground than honestly holding the stick; both hold while the formation is being pivoted, the case this ticket names; and the fleet stays together rather than fanning out. Speeds are tracked as a ratio of each vessel's own limit, so a mixed fleet is judged per vessel. One knock-on worth recording: making `velocity` honest changed `test_space_time_distorder.gd`'s baseline from "formation lag 0.0" to 51.6 - the old zero was the teleport-snap lying about a ship that was in fact moving. That test also now clears the asteroid and enemy groups at setup (the idiom `test_ship_lab.gd` already uses); silencing the spawners was never enough, and losing the distorder vessel to a rock resets the multiplier *correctly*, which it had been reporting three stages later as a failed scene swap. Full suite green at 94/94.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
metaheavens/spaceship#72
No description provided.