Skip to content

Fix: prevent a single fire command from spawning duplicate bullets with the same ID#89

Merged
pavelsavara merged 3 commits into
robo-code:mainfrom
pavelsavara:fix_bullet
Jun 3, 2026
Merged

Fix: prevent a single fire command from spawning duplicate bullets with the same ID#89
pavelsavara merged 3 commits into
robo-code:mainfrom
pavelsavara:fix_bullet

Conversation

@pavelsavara

Copy link
Copy Markdown
Member

Summary

A queued BulletCommand could be fired more than once by the battle engine,
producing two (or more) BulletPeers that share the same bulletId. This
change marks a BulletCommand as consumed the moment it produces a bullet and
skips already-consumed commands on subsequent turns, so each fire command yields
at most one bullet.

Changes

  • robocode.coreBulletCommand

    • Added a battle-side-only transient boolean consumed flag with
      isConsumed() / consume() accessors. It is transient because it is
      purely engine bookkeeping and must not become part of the serialized wire
      format exchanged with the robot host.
  • robocode.battleRobotPeer.fireBullets(...)

    • Skip any BulletCommand whose isConsumed() is true.
    • After a command successfully creates its BulletPeer, call consume() so
      the same command (and its bulletId) cannot fire again on a later turn.

When do two bullets get the same ID?

Bullet IDs are assigned once, on the host side, when the robot calls
fire() / setFire(). In BasicRobotProxy.setFireImpl(...) the proxy
increments nextBulletId, stores it inside a new BulletCommand, and appends
that command to the shared ExecCommands bullet list. The ID is therefore fixed
and unique per fire call — the duplication does not come from ID
generation, it comes from the same command being executed twice on the
battle side.

The battle side replays commands like this every turn:

  1. RobotPeer.performLoadCommands() reads the latest ExecCommands from the
    commands AtomicReference and calls
    fireBullets(currentCommands.getBullets()) every turn.
  2. fireBullets(...) walks the bullet-command list. If the gun is still hot
    (gunHeat > 0), it returns early without removing the command — the fire
    request stays pending in the list until the gun cools down.

The problem appears when the robot stops producing fresh commands while an
old command list is still installed, for example when the robot:

  • stops calling execute() / returns from run(), or
  • is skipped / disabled / crashes in its own code and no longer advances.

In that situation the commands reference keeps handing back the same
ExecCommands instance
turn after turn, so the same BulletCommand
objects
are re-processed repeatedly. A pending fire command then behaves like
this:

  • Turn n: gun cools to gunHeat == 0, the command fires and a BulletPeer is
    created with bulletCmd.getBulletId().
  • The command is not removed from the list and the robot never replaces the
    list.
  • A later turn: the gun has cooled again, the same command is encountered
    once more and fires a second BulletPeer — reusing the identical
    bulletId
    baked into that command.

The result is two live bullets sharing one ID, which corrupts any
ID-keyed bookkeeping (the host tracks bullets in a Map keyed by bulletId,
and downstream snapshot consumers assume IDs are unique).

Fix rationale

Marking the command consumed after it spawns its bullet makes the command
single-shot: it fires exactly once regardless of how many times the persisted
command list is replayed, guaranteeing one bulletId ↔ one bullet.

@pavelsavara pavelsavara self-assigned this Jun 2, 2026
@pavelsavara pavelsavara marked this pull request as ready for review June 3, 2026 19:21
@pavelsavara pavelsavara merged commit 16779cc into robo-code:main Jun 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants