The README lists this under known issues:
A district spawn zone must currently be selected quickly; delayed selections may be ignored.
Reading the selection path on main (84cbb97), the deadline is not a timer — it is first-receipt-wins, and the selectable actors are destroyed on that first receipt. Four separate mechanisms combine, so a slow (or corrected) pick has nothing left to act on.
1. Only the first ServerSelectSpawnZone is honoured
DistrictServer/DistrictServer.cpp:12410-12415:
if (!state.ServerSelectSpawnZoneSeen)
{
state.ServerSelectSpawnZoneSeen = true;
firstReceipt = true;
}
That flag is the only gate. Every later field-371 arrival is logged " (duplicate)" (:12467) and then does nothing.
2. The picked location is only recorded on first receipt
:12474-12505 stores the resolved coordinates inside if (firstReceipt):
if (zoneIndex != static_cast<std::size_t>(-1))
{
...
g_selectedSpawnLocations[account->GetId()] = { x, y, z + 100.0f };
}
So if the player clicks zone A and then zone B, B is discarded — the pawn opens at A. A "changed my mind" click is indistinguishable from a late click.
3. First receipt closes all twelve bridge channels
:12507-12528, action districts only:
CancelPendingReliablesByLabelPrefix(account->GetId(),
{ "SPAWN-ZONE-ACTOR-BRIDGE-OPEN-", "CLIENT-REPLICATE-HUD-MARKER-" });
for (std::uint16_t channel = kSpawnZoneActorChannel;
channel < kSpawnZoneActorChannel + 12u; ++channel)
{
... "SPAWN-ZONE-ACTOR-CLOSE"
}
Per-zone identity is carried only by these bridge channels (kSpawnZoneActorChannel = 5, :2774; decoded at :12476-12482). Once they are closed the client has no object to reference, so a subsequent pick cannot even be resolved to a zone index.
4. The marker/bridge scaffolding has a 2-send reliable budget
:1636-1642 gives spawn-zone traffic a much smaller budget than everything else:
const int spawnZoneMaxSends = ReadHandshakeInt(
"APB_SPAWN_ZONE_RELIABLE_MAX_SENDS", "SpawnZoneReliableMaxSends", 2, 1, 8);
versus ServerReliableMaxSends = 8 (:1623-1629). On exhaustion the pending entry is erased and logged at error level (:1700-1744): "No ACK after maximum sends … The startup sequence cannot be trusted past this point." The comment above the knob explains the intent (this client build accepts these without ACKing, so retrying 12 pairs 8× is a pointless burst) — which is reasonable, but it does mean the scaffolding's delivery guarantee lapses within ~2 retry intervals while the human is still reading the map.
Secondary: Sleep() on the receive thread
:12376-12384 sleeps APB_AUTO_POSSESS_DELAY_MS (default 500 ms) inline:
if (delayMilliseconds > 0)
Sleep(static_cast<DWORD>(delayMilliseconds));
This runs on the UDP receive loop, so nothing is drained from the socket for that period and a click landing in the window can be lost to the socket buffer. Currently reachable only via AutoPossessAfterClientLoaded (default false, non-action districts), so it is latent rather than active — but it is on the hot path for any future timed possession.
Suggested direction
- Make the selection idempotent and re-pickable: let field 371 update
g_selectedSpawnLocations on every receipt, and only latch when possession actually starts.
- Defer the
SPAWN-ZONE-ACTOR-CLOSE sweep until the pawn is committed, rather than firing it on first receipt.
- Keep the bridge/marker actors alive (or re-open on demand) for as long as the map-select screen is up.
- Replace the inline
Sleep() with a deadline checked on the existing receive-driven tick, so the loop never stops draining.
Verified by reading main at 84cbb97; no live-client session was run for this report, so the symptom is quoted from the README rather than independently reproduced. Line numbers are from that commit.
The README lists this under known issues:
Reading the selection path on
main(84cbb97), the deadline is not a timer — it is first-receipt-wins, and the selectable actors are destroyed on that first receipt. Four separate mechanisms combine, so a slow (or corrected) pick has nothing left to act on.1. Only the first
ServerSelectSpawnZoneis honouredDistrictServer/DistrictServer.cpp:12410-12415:That flag is the only gate. Every later field-371 arrival is logged
" (duplicate)"(:12467) and then does nothing.2. The picked location is only recorded on first receipt
:12474-12505stores the resolved coordinates insideif (firstReceipt):So if the player clicks zone A and then zone B, B is discarded — the pawn opens at A. A "changed my mind" click is indistinguishable from a late click.
3. First receipt closes all twelve bridge channels
:12507-12528, action districts only:Per-zone identity is carried only by these bridge channels (
kSpawnZoneActorChannel = 5,:2774; decoded at:12476-12482). Once they are closed the client has no object to reference, so a subsequent pick cannot even be resolved to a zone index.4. The marker/bridge scaffolding has a 2-send reliable budget
:1636-1642gives spawn-zone traffic a much smaller budget than everything else:versus
ServerReliableMaxSends= 8 (:1623-1629). On exhaustion the pending entry is erased and logged at error level (:1700-1744): "No ACK after maximum sends … The startup sequence cannot be trusted past this point." The comment above the knob explains the intent (this client build accepts these without ACKing, so retrying 12 pairs 8× is a pointless burst) — which is reasonable, but it does mean the scaffolding's delivery guarantee lapses within ~2 retry intervals while the human is still reading the map.Secondary:
Sleep()on the receive thread:12376-12384sleepsAPB_AUTO_POSSESS_DELAY_MS(default 500 ms) inline:This runs on the UDP receive loop, so nothing is drained from the socket for that period and a click landing in the window can be lost to the socket buffer. Currently reachable only via
AutoPossessAfterClientLoaded(defaultfalse, non-action districts), so it is latent rather than active — but it is on the hot path for any future timed possession.Suggested direction
g_selectedSpawnLocationson every receipt, and only latch when possession actually starts.SPAWN-ZONE-ACTOR-CLOSEsweep until the pawn is committed, rather than firing it on first receipt.Sleep()with a deadline checked on the existing receive-driven tick, so the loop never stops draining.Verified by reading
mainat84cbb97; no live-client session was run for this report, so the symptom is quoted from the README rather than independently reproduced. Line numbers are from that commit.