feat: add player-scoped assignment recovery

This commit is contained in:
Josh Creek
2026-08-31 22:54:16 +01:00
parent 99680dbf6f
commit 69f1ef6be1
6 changed files with 181 additions and 1 deletions
+46
View File
@@ -40,6 +40,19 @@ type SessionIssuer interface {
Issue(context.Context, string, time.Duration, time.Time) (domain.Session, string, error)
}
type AssignmentView struct {
MatchID string `json:"match_id"`
ServerID string `json:"server_id"`
PlayerID string `json:"player_id"`
Slot int `json:"slot"`
ExpiresAt time.Time `json:"expires_at"`
ProtocolVersion int `json:"protocol_version"`
Transport string `json:"transport"`
JoinAuthorisation string `json:"join_authorisation"`
}
type AssignmentProvider func(context.Context, string, string, time.Time) (AssignmentView, error)
type Service struct {
Sessions *domain.SessionStore
SessionBackend SessionBackend
@@ -50,6 +63,7 @@ type Service struct {
CandidateV2 CandidateProviderV2
QueueBackend QueueBackend
Probe ProbeProvider
Assignment AssignmentProvider
Now func() time.Time
Proposals map[string]*domain.Proposal
RankedProfiles map[string]domain.RankedProfile
@@ -64,6 +78,7 @@ func (s *Service) Handler() http.Handler {
mux.HandleFunc("/v1/queue", s.queueCreate)
mux.HandleFunc("/v1/queue/", s.queueMutation)
mux.HandleFunc("/v1/proposals/", s.proposalMutation)
mux.HandleFunc("/v1/assignments/", s.assignment)
mux.HandleFunc("/v1/profile/ranked", s.rankedProfile)
mux.HandleFunc("/v1/probes/", s.probe)
return mux
@@ -331,6 +346,37 @@ func (s *Service) proposalMutation(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, toProposalResponse(updated))
}
func (s *Service) assignment(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed")
return
}
playerID, ok := s.authenticate(w, r)
if !ok {
return
}
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1/assignments/"), "/")
if len(parts) != 1 || parts[0] == "" {
writeError(w, http.StatusNotFound, "not_found")
return
}
if s.Assignment == nil {
writeError(w, http.StatusServiceUnavailable, "assignment_unavailable")
return
}
now := s.now()
view, err := s.Assignment(r.Context(), playerID, parts[0], now)
if err != nil || view.MatchID != parts[0] || view.PlayerID != playerID {
writeError(w, http.StatusNotFound, "not_found")
return
}
if view.ServerID == "" || view.Slot < 0 || view.Slot > 5 || view.ProtocolVersion < 1 || (view.Transport != "enet" && view.Transport != "steam_sdr") || view.JoinAuthorisation == "" || view.ExpiresAt.IsZero() || !now.Before(view.ExpiresAt) {
writeError(w, http.StatusServiceUnavailable, "assignment_unavailable")
return
}
writeJSON(w, http.StatusOK, view)
}
type rankedProfileResponse struct {
Rating float64 `json:"rating"`
RD float64 `json:"rd"`