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)
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.
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.
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.
Combat attacker=... Attack LOSE CAPTURE_UNIT) and the subsequent[unit:8805 english veteranSoldier], server responded to a moveUnit not owned by player:19:
[unit:8805 russian freeColonist]).ant compile, no errors).
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.
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.
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 ?
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.
Yup same bug, I'm the one that reported it the first time.
http://softwaredeveloperzone.com
http://fb.com/softwaredeveloperzone
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:
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
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).
lists the captured unit anyway, still shown as active and under your
ownership, with full moves.
"The server can not do that."
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
(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]).
(ant compile, no errors).
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
checked out from the FreeCol/freecol GitHub repository)
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