Menu

#3403 Ghost Soldier Bug Report and Patch

Current
open
nobody
1
5 days ago
5 days ago
Larry Gray
No

Summary: Captured unit stays "alive" and stuck in the former owner's active-unit list ("ghost unit"), rejecting all orders, until forced to resync (e.g. that settlement is later captured)

Description

When a unit loses a combat and the result is that it gets captured (as
opposed to killed, losing its equipment, or being demoted), the losing
player's client is never told that the unit changed hands. The unit's
sprite correctly disappears from its old tile, but the unit itself remains
in the former owner's list of units and keeps being offered as "needing
orders" at every End Turn, indefinitely. Attempting to give the stuck unit
any order fails with "The server can not do that." (an illegal-move /
ownership rejection), because the server has already reassigned the unit
to the winning player.

The only way to clear the stuck unit that I've found is to eventually
capture the settlement involved, which forces a full state resync and
incidentally clears it.

This has been a long-standing, very frequently hit bug for me (and I
suspect many other players) -- because from the player's point of view a
captured unit just silently vanishes and starts misbehaving, it's easy to
describe/assume the unit "died" rather than "was captured", which may be
why this hasn't previously been tracked down: the death case
(SLAUGHTER_UNIT) is handled correctly and does not have this problem, so
bug reports framed around "unit death" don't reproduce.

Steps to reproduce

  1. Attack an enemy settlement (or otherwise engage in combat) with a
    soldier/dragoon unit repeatedly, such that eventually one of your
    units loses a fight with the CAPTURE_UNIT outcome (this is one of
    several possible loss outcomes alongside LOSE_EQUIP/DEMOTE_UNIT and
    SLAUGHTER_UNIT, so it does not happen on every loss -- only some).
  2. The captured unit's sprite disappears from the map, as expected.
  3. At the next End Turn, the "unit still waiting for orders" dialog
    lists the captured unit anyway, still shown as active and under your
    ownership, with full moves.
  4. Try to give it any order (move it). The server rejects it with
    "The server can not do that."
  5. The unit continues to reappear at every subsequent End Turn until the
    settlement involved in the capture is conquered, at which point it
    disappears (via unrelated bulk state resync, not because the
    underlying issue is fixed).

Root cause

ServerPlayer.csCaptureUnit() (src/net/sf/freecol/server/model/ServerPlayer.java)
transfers ownership of the losing unit via:

((ServerPlayer)loserPlayer).csChangeOwner(loser, winnerPlayer,
        UnitChangeType.CAPTURE, winner.getTile(), cs)

ServerPlayer.csChangeOwner() performs the ownership/type/location change
on the server-side model correctly, but the only ChangeSet update it adds
is:

cs.add(See.only(newOwner),
       ((ServerPlayer)newOwner).exploreForUnit(unit));

This notifies only the new owner. The former owner's client is never
told the unit is gone, so it keeps its stale copy: still owned by them,
still active, still with full moves, and thus keeps surfacing as
"needing orders" via the normal active-unit iteration
(Unit.isCandidateForNextActiveUnit() / Player.getNextActiveUnit()),
since none of the conditions that would exclude it (disposed, inactive,
no moves) have changed from the former owner's point of view.

Compare with the death case, ServerPlayer.csSlaughterUnit(), which
correctly notifies the loser via ServerUnit.csRemove() /
cs.addRemove(...), which the client's RemoveMessage handler applies by
calling owner.removeUnit(u) and disposeResources() -- this is exactly
the mechanism that's missing for the capture case.

Fix

Patch attached (against src/net/sf/freecol/server/model/ServerPlayer.java).
In csCaptureUnit(), after the ownership transfer succeeds, explicitly
notify the former owner to drop the unit via cs.addRemove(...), mirroring
the visibility handling csSlaughterUnit() already uses (See.only() when
the unit's old tile has a settlement, to avoid exposing a settlement that
should remain hidden; See.perhaps().always() otherwise).

I deliberately scoped the fix to csCaptureUnit() rather than the shared
csChangeOwner() utility, since csChangeOwner() has several other
callers (colony capture, native conversion, REF defection) that I have not
individually verified are safe to change the same way -- they may be worth
auditing separately, as they share the same missing-notification pattern
and could have similar (rarer) symptoms.

Verification

  • Diagnosed from a live game log: found the exact combat event
    (Combat attacker=... Attack LOSE CAPTURE_UNIT) and the subsequent
    client-side desync (client held the unit as
    [unit:8805 english veteranSoldier], server responded to a move
    attempt with Unit not owned by player:19: [unit:8805 russian freeColonist]).
  • Patch compiles cleanly against the current source tree
    (ant compile, no errors).
  • Not yet verified with a fresh in-game repro of a new capture after
    the fix (the original stuck unit predates the fix and won't self-heal
    without a fresh capture event or settlement conquest) -- I'd appreciate
    someone with a build environment confirming a newly-captured unit no
    longer gets stuck.

Environment

  • FreeCol 1.1.0 (client showed "FreeCol 1.1.0" in-game; source tree
    checked out from the FreeCol/freecol GitHub repository)
  • Windows 11
1 Attachments

Related

Bugs: #3403

Discussion

  • Larry Gray

    Larry Gray - 5 days ago

    I hope you guys get this fix in new release soon, it's been 20 years I've been putting up with it lol. Thank Claude Code.

     
  • Larry Gray

    Larry Gray - 5 days ago

    Follow-up: verified with the attached patch

    Applied the patch and played on. Confirmed against a fresh capture event in-game:

    A veteran soldier of mine was captured by an enemy unit (Attack WIN CAPTURE_UNIT in the combat log).
    Previously this reliably produced the "ghost unit" — stuck in my active-unit list, rejecting all orders — every time a capture like this happened.
    With the patch applied, the captured unit cleanly disappeared from my unit list. No stuck/ghost unit, no repeated "still waiting for orders" prompt at End Turn, no illegal-move rejection.
    This confirms the root cause and fix (notifying the former owner when csChangeOwner() reassigns a captured unit) actually resolves the issue in real play, not just in theory.

     
  • Lone_Wolf

    Lone_Wolf - 5 days ago

    Is this the same bug as in https://sourceforge.net/p/freecol/bugs/3370/ ?

    The patch does apply cleanly to master rev a7d293.
    Could you create a merge request for it (here or on github) so it can be added easily to the source tree ?

     
    • Larry Gray

      Larry Gray - 5 days ago

      es, same bug — BR#3370 was my earlier report of this exact issue.

      Pull request is up: https://github.com/FreeCol/freecol/pull/167

      It applies cleanly to sf-auto-merge (verified against rev a7d293e, same as you mentioned). Let me know if you'd like anything changed.

       
    • Larry Gray

      Larry Gray - 5 days ago

      Yup same bug, I'm the one that reported it the first time.
      http://softwaredeveloperzone.com
      http://fb.com/softwaredeveloperzone

      On Tuesday, August 4, 2026 at 03:55:01 AM MDT, Lone_Wolf <lone_wolfsf@users.sourceforge.net> wrote:
      

      Is this the same bug as in https://sourceforge.net/p/freecol/bugs/3370/ ?

      The patch does apply cleanly to master rev a7d293.
      Could you create a merge request for it (here or on github) so it can be added easily to the source tree ?

      [bugs:#3403] Ghost Soldier Bug Report and Patch

      Status: open
      Group: Current
      Labels: ghost soldier unit
      Created: Tue Aug 04, 2026 07:05 AM UTC by Larry Gray
      Last Updated: Tue Aug 04, 2026 09:52 AM UTC
      Owner: nobody
      Attachments:

      • ghost-soldier-capture-fix.patch (sourceforge.net) (1.4 kB; application/octet-stream)

      Summary: Captured unit stays "alive" and stuck in the former owner's active-unit list ("ghost unit"), rejecting all orders, until forced to resync (e.g. that settlement is later captured)

      Description

      When a unit loses a combat and the result is that it gets captured (as
      opposed to killed, losing its equipment, or being demoted), the losing
      player's client is never told that the unit changed hands. The unit's
      sprite correctly disappears from its old tile, but the unit itself remains
      in the former owner's list of units and keeps being offered as "needing
      orders" at every End Turn, indefinitely. Attempting to give the stuck unit
      any order fails with "The server can not do that." (an illegal-move /
      ownership rejection), because the server has already reassigned the unit
      to the winning player.

      The only way to clear the stuck unit that I've found is to eventually
      capture the settlement involved, which forces a full state resync and
      incidentally clears it.

      This has been a long-standing, very frequently hit bug for me (and I
      suspect many other players) -- because from the player's point of view a
      captured unit just silently vanishes and starts misbehaving, it's easy to
      describe/assume the unit "died" rather than "was captured", which may be
      why this hasn't previously been tracked down: the death case
      (SLAUGHTER_UNIT) is handled correctly and does not have this problem, so
      bug reports framed around "unit death" don't reproduce.

      Steps to reproduce

      • Attack an enemy settlement (or otherwise engage in combat) with a
        soldier/dragoon unit repeatedly, such that eventually one of your
        units loses a fight with the CAPTURE_UNIT outcome (this is one of
        several possible loss outcomes alongside LOSE_EQUIP/DEMOTE_UNIT and
        SLAUGHTER_UNIT, so it does not happen on every loss -- only some).
      • The captured unit's sprite disappears from the map, as expected.
      • At the next End Turn, the "unit still waiting for orders" dialog
        lists the captured unit anyway, still shown as active and under your
        ownership, with full moves.
      • Try to give it any order (move it). The server rejects it with
        "The server can not do that."
      • The unit continues to reappear at every subsequent End Turn until the
        settlement involved in the capture is conquered, at which point it
        disappears (via unrelated bulk state resync, not because the
        underlying issue is fixed).

      Root cause

      ServerPlayer.csCaptureUnit() (src/net/sf/freecol/server/model/ServerPlayer.java)
      transfers ownership of the losing unit via:
      ((ServerPlayer)loserPlayer).csChangeOwner(loser, winnerPlayer,
      UnitChangeType.CAPTURE, winner.getTile(), cs)

      ServerPlayer.csChangeOwner() performs the ownership/type/location change
      on the server-side model correctly, but the only ChangeSet update it adds
      is:
      cs.add(See.only(newOwner),
      ((ServerPlayer)newOwner).exploreForUnit(unit));

      This notifies only the new owner. The former owner's client is never
      told the unit is gone, so it keeps its stale copy: still owned by them,
      still active, still with full moves, and thus keeps surfacing as
      "needing orders" via the normal active-unit iteration
      (Unit.isCandidateForNextActiveUnit() / Player.getNextActiveUnit()),
      since none of the conditions that would exclude it (disposed, inactive,
      no moves) have changed from the former owner's point of view.

      Compare with the death case, ServerPlayer.csSlaughterUnit(), which
      correctly notifies the loser via ServerUnit.csRemove() /
      cs.addRemove(...), which the client's RemoveMessage handler applies by
      calling owner.removeUnit(u) and disposeResources() -- this is exactly
      the mechanism that's missing for the capture case.

      Fix

      Patch attached (against src/net/sf/freecol/server/model/ServerPlayer.java).
      In csCaptureUnit(), after the ownership transfer succeeds, explicitly
      notify the former owner to drop the unit via cs.addRemove(...), mirroring
      the visibility handling csSlaughterUnit() already uses (See.only() when
      the unit's old tile has a settlement, to avoid exposing a settlement that
      should remain hidden; See.perhaps().always() otherwise).

      I deliberately scoped the fix to csCaptureUnit() rather than the shared
      csChangeOwner() utility, since csChangeOwner() has several other
      callers (colony capture, native conversion, REF defection) that I have not
      individually verified are safe to change the same way -- they may be worth
      auditing separately, as they share the same missing-notification pattern
      and could have similar (rarer) symptoms.

      Verification

      • Diagnosed from a live game log: found the exact combat event
        (Combat attacker=... Attack LOSE CAPTURE_UNIT) and the subsequent
        client-side desync (client held the unit as
        [unit:8805 english veteranSoldier], server responded to a move
        attempt with Unit not owned by player:19: [unit:8805 russian freeColonist]).
      • Patch compiles cleanly against the current source tree
        (ant compile, no errors).
      • Not yet verified with a fresh in-game repro of a new capture after
        the fix (the original stuck unit predates the fix and won't self-heal
        without a fresh capture event or settlement conquest) -- I'd appreciate
        someone with a build environment confirming a newly-captured unit no
        longer gets stuck.

      Environment

      • FreeCol 1.1.0 (client showed "FreeCol 1.1.0" in-game; source tree
        checked out from the FreeCol/freecol GitHub repository)
      • Windows 11

      Sent from sourceforge.net because you indicated interest in https://sourceforge.net/p/freecol/bugs/3403/

      To unsubscribe from further messages, please visit https://sourceforge.net/auth/subscriptions/

      | | Virus-free.www.avast.com |

       

      Related

      Bugs: #3403


Log in to post a comment.