Files
Josh Creek 5190cded56 fix(multiplayer): stop a doomed formation from wedging the matcher
Closes the 'innocent-ticket restoration' gap noted in §8.20 and found
by re-examining §8.16's matcher worker. domain.FormFromQueue's anchor
is always the single oldest candidate, deterministically. If
domain.PrepareProposal then rejected that exact formation for a
reason specific to those particular players — mismatched protocol,
incomplete ranked identity metadata, a duplicate-SteamID pair, ranked
admission generally — RunOnce returned immediately and the next
matcher interval reproduced the identical formation and failed again.
Forever: nothing in the queue ever changes, so the same doomed anchor
group would be retried every single pass, permanently head-of-line-
blocking every other waiting player behind it too, not just the
players actually at fault. This is worse than the already-fixed
no-common-region crash-loop (§8.16) — that one killed the process;
this one fails silently and just never matches anyone again.

Two changes, both required together:

1. RunOnce now excludes a failed formation's players and retries
   with the remaining candidate pool, bounded to 8 attempts per pass.
   A batch with no viable formation at all (the pre-existing
   no-common-region case) still returns immediately, since retrying
   that can't help.

2. That fix was inert without a second one: RunOnce was asking
   Source for exactly w.Size candidates, so after excluding one
   failed formation's players there was nothing left to retry
   against. domain.SelectCandidates was always designed to search a
   larger pool (anchor plus an arbitrary remainder, widening through
   it) — the call site just never gave it one. RunOnce now requests
   up to 10x w.Size, capped at 200.

Verified: go build/vet/test -race clean across every server package.
Three new matcher tests cover the exclusion retry (an 8-candidate
batch whose permanently-doomed oldest 4 still lets the remaining 4
form and claim, correctly excluding the doomed players from the
claimed ticket set), that exhausting every attempt surfaces the last
real error rather than a silent false/nil, and that Source is
actually asked for more than w.Size candidates — a regression guard
for exactly the companion bug above. All six pre-existing worker
tests still pass unmodified, confirming the fix preserves every prior
guarantee (mixed-playlist/duplicate-identity rejection, incomplete
batch handling, durable claim failure propagation, Run's existing
per-pass-error survival).
2026-09-04 17:33:00 +01:00
..