feat(multiplayer): persist live reconnect abandonments

This commit is contained in:
Josh Creek
2026-09-03 20:53:28 +01:00
parent aac81c89b6
commit 2463713cde
10 changed files with 360 additions and 6 deletions
+32
View File
@@ -156,6 +156,38 @@ type Abandonment struct {
AbandonedAt time.Time
}
// ReconnectParticipant is the durable subset needed to evaluate an expired
// live reconnect lease. Connected players are deliberately absent: only a
// persisted disconnect can start a player-caused abandon clock.
type ReconnectParticipant struct {
PlayerID string
DisconnectedAt time.Time
}
// PlanRankedAbandonments turns expired durable reconnect leases into the
// same rolling cooldown ladder used by pre-live ranked no-shows. Future
// disconnect timestamps are ignored rather than penalised: they can only be
// an infrastructure clock anomaly, not a player abandonment.
func PlanRankedAbandonments(now time.Time, participants []ReconnectParticipant, priorAbandons map[string][]time.Time) ([]Abandonment, error) {
if now.IsZero() {
return nil, fmt.Errorf("invalid reconnect-abandonment time")
}
seen := make(map[string]bool, len(participants))
result := make([]Abandonment, 0, len(participants))
for _, participant := range participants {
if participant.PlayerID == "" || participant.DisconnectedAt.IsZero() || seen[participant.PlayerID] {
return nil, fmt.Errorf("invalid reconnect participant")
}
seen[participant.PlayerID] = true
if now.Before(participant.DisconnectedAt) || now.Sub(participant.DisconnectedAt) <= RankedReconnectGrace {
continue
}
result = append(result, Abandonment{PlayerID: participant.PlayerID, Cooldown: abandonCooldown(priorAbandons[participant.PlayerID], now), AbandonedAt: now})
}
sort.Slice(result, func(i, j int) bool { return result[i].PlayerID < result[j].PlayerID })
return result, nil
}
// ExpireGrace marks every disconnected player whose 60-second reclaim window
// has elapsed. The returned list is lexical for stable audit/event ordering.
func (r *RankedConnections) ExpireGrace(now time.Time, priorAbandons map[string][]time.Time) []Abandonment {