fix(multiplayer): harden reconnect lifecycle fencing

This commit is contained in:
Josh Creek
2026-09-03 13:24:52 +01:00
parent cee0163eac
commit 8c28374eb4
5 changed files with 62 additions and 8 deletions
+15
View File
@@ -91,6 +91,9 @@ func (r *RankedConnections) validate(auth JoinAuthorisation, now time.Time) erro
// slot with the next server-owned generation. A newer generation fences every
// older connection, even if the backend is temporarily unavailable.
func (r *RankedConnections) Admit(auth JoinAuthorisation, now time.Time) (uint64, error) {
if now.IsZero() {
return 0, ErrJoinAuthorisation
}
if err := r.validate(auth, now); err != nil {
return 0, err
}
@@ -107,7 +110,13 @@ func (r *RankedConnections) Admit(auth JoinAuthorisation, now time.Time) (uint64
if player.Abandoned {
return 0, ErrReconnectExpired
}
if !player.ConnectedAt.IsZero() && player.LostAt.IsZero() {
return 0, ErrConnectionFenced
}
if !player.LostAt.IsZero() {
if now.Before(player.LostAt) {
return 0, ErrJoinAuthorisation
}
if now.Sub(player.LostAt) > RankedReconnectGrace {
return 0, ErrReconnectExpired
}
@@ -120,6 +129,9 @@ func (r *RankedConnections) Admit(auth JoinAuthorisation, now time.Time) (uint64
}
func (r *RankedConnections) Disconnect(playerID string, generation uint64, now time.Time) error {
if now.IsZero() {
return ErrJoinAuthorisation
}
player, ok := r.players[playerID]
if !ok {
return ErrJoinAuthorisation
@@ -130,6 +142,9 @@ func (r *RankedConnections) Disconnect(playerID string, generation uint64, now t
if player.Abandoned {
return ErrReconnectExpired
}
if player.ConnectedAt.IsZero() || !player.LostAt.IsZero() || now.Before(player.ConnectedAt) {
return ErrConnectionFenced
}
player.LostAt = now
r.players[playerID] = player
return nil