252 lines
7.0 KiB
Go
252 lines
7.0 KiB
Go
package game
|
|
|
|
// Status identifies one platform-level lifecycle state of a game record.
|
|
type Status string
|
|
|
|
const (
|
|
// StatusDraft reports that the record was created but enrollment is
|
|
// not yet open.
|
|
StatusDraft Status = "draft"
|
|
|
|
// StatusEnrollmentOpen reports that applications (public game) or
|
|
// invite redemptions (private game) are being accepted.
|
|
StatusEnrollmentOpen Status = "enrollment_open"
|
|
|
|
// StatusReadyToStart reports that enrollment closed and the start
|
|
// command is accepted.
|
|
StatusReadyToStart Status = "ready_to_start"
|
|
|
|
// StatusStarting reports that a start job has been submitted to
|
|
// Runtime Manager and Lobby is waiting for the result.
|
|
StatusStarting Status = "starting"
|
|
|
|
// StatusStartFailed reports that the container start or metadata
|
|
// persistence step failed.
|
|
StatusStartFailed Status = "start_failed"
|
|
|
|
// StatusRunning reports that the engine container is live and normal
|
|
// gameplay has begun.
|
|
StatusRunning Status = "running"
|
|
|
|
// StatusPaused reports a platform-level pause; the engine container
|
|
// may still be alive.
|
|
StatusPaused Status = "paused"
|
|
|
|
// StatusFinished reports that the game ended; the record is terminal.
|
|
StatusFinished Status = "finished"
|
|
|
|
// StatusCancelled reports that the game was cancelled before start;
|
|
// the record is terminal.
|
|
StatusCancelled Status = "cancelled"
|
|
)
|
|
|
|
// IsKnown reports whether status belongs to the frozen Lobby status
|
|
// vocabulary.
|
|
func (status Status) IsKnown() bool {
|
|
switch status {
|
|
case StatusDraft,
|
|
StatusEnrollmentOpen,
|
|
StatusReadyToStart,
|
|
StatusStarting,
|
|
StatusStartFailed,
|
|
StatusRunning,
|
|
StatusPaused,
|
|
StatusFinished,
|
|
StatusCancelled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllStatuses returns the frozen list of every Lobby status value. The
|
|
// slice order is stable across calls and matches the README §Status
|
|
// vocabulary listing.
|
|
func AllStatuses() []Status {
|
|
return []Status{
|
|
StatusDraft,
|
|
StatusEnrollmentOpen,
|
|
StatusReadyToStart,
|
|
StatusStarting,
|
|
StatusStartFailed,
|
|
StatusRunning,
|
|
StatusPaused,
|
|
StatusFinished,
|
|
StatusCancelled,
|
|
}
|
|
}
|
|
|
|
// IsTerminal reports whether status can no longer accept lifecycle
|
|
// transitions.
|
|
func (status Status) IsTerminal() bool {
|
|
switch status {
|
|
case StatusFinished, StatusCancelled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Trigger identifies what caused a status transition. The value is used by
|
|
// the domain transition gate and by later observability layers.
|
|
type Trigger string
|
|
|
|
const (
|
|
// TriggerCommand reports an explicit owner or admin command such as
|
|
// start, pause, resume, cancel, open-enrollment, or retry-start.
|
|
TriggerCommand Trigger = "command"
|
|
|
|
// TriggerManual reports that an admin or owner manually closed
|
|
// enrollment while min_players was already satisfied.
|
|
TriggerManual Trigger = "manual"
|
|
|
|
// TriggerDeadline reports that the enrollment automation worker
|
|
// detected enrollment_ends_at had passed with min_players satisfied.
|
|
TriggerDeadline Trigger = "deadline"
|
|
|
|
// TriggerGap reports that the enrollment automation worker detected
|
|
// the gap window exhausted its time or player budget.
|
|
TriggerGap Trigger = "gap"
|
|
|
|
// TriggerRuntimeEvent reports that the transition was caused by a
|
|
// Runtime Manager job result or a Game Master runtime event.
|
|
TriggerRuntimeEvent Trigger = "runtime_event"
|
|
|
|
// TriggerExternalBlock reports that the transition was caused by the
|
|
// user-lifecycle cascade reacting to a permanent_block or DeleteUser
|
|
// event on the game owner. The trigger is the only path that
|
|
// cancels a game from in-flight statuses
|
|
// (`starting`, `running`, `paused`).
|
|
TriggerExternalBlock Trigger = "external_block"
|
|
)
|
|
|
|
// IsKnown reports whether trigger belongs to the frozen trigger
|
|
// vocabulary.
|
|
func (trigger Trigger) IsKnown() bool {
|
|
switch trigger {
|
|
case TriggerCommand,
|
|
TriggerManual,
|
|
TriggerDeadline,
|
|
TriggerGap,
|
|
TriggerRuntimeEvent,
|
|
TriggerExternalBlock:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// transitionKey stores one `(from, to)` pair in the allowed-transitions
|
|
// table.
|
|
type transitionKey struct {
|
|
from Status
|
|
to Status
|
|
}
|
|
|
|
// allowedTransitions maps each permitted `(from, to)` pair to the set of
|
|
// triggers that may drive it. The table mirrors the status transition
|
|
// table frozen in lobby/README.md.
|
|
var allowedTransitions = map[transitionKey]map[Trigger]struct{}{
|
|
{StatusDraft, StatusEnrollmentOpen}: {
|
|
TriggerCommand: {},
|
|
},
|
|
{StatusEnrollmentOpen, StatusReadyToStart}: {
|
|
TriggerManual: {},
|
|
TriggerDeadline: {},
|
|
TriggerGap: {},
|
|
},
|
|
{StatusReadyToStart, StatusStarting}: {
|
|
TriggerCommand: {},
|
|
},
|
|
{StatusStarting, StatusRunning}: {
|
|
TriggerRuntimeEvent: {},
|
|
},
|
|
{StatusStarting, StatusPaused}: {
|
|
TriggerRuntimeEvent: {},
|
|
},
|
|
{StatusStarting, StatusStartFailed}: {
|
|
TriggerRuntimeEvent: {},
|
|
},
|
|
{StatusStartFailed, StatusReadyToStart}: {
|
|
TriggerCommand: {},
|
|
},
|
|
{StatusRunning, StatusPaused}: {
|
|
TriggerCommand: {},
|
|
},
|
|
{StatusRunning, StatusFinished}: {
|
|
TriggerRuntimeEvent: {},
|
|
},
|
|
{StatusPaused, StatusRunning}: {
|
|
TriggerCommand: {},
|
|
},
|
|
{StatusPaused, StatusFinished}: {
|
|
TriggerRuntimeEvent: {},
|
|
},
|
|
{StatusDraft, StatusCancelled}: {
|
|
TriggerCommand: {},
|
|
TriggerExternalBlock: {},
|
|
},
|
|
{StatusEnrollmentOpen, StatusCancelled}: {
|
|
TriggerCommand: {},
|
|
TriggerExternalBlock: {},
|
|
},
|
|
{StatusReadyToStart, StatusCancelled}: {
|
|
TriggerCommand: {},
|
|
TriggerExternalBlock: {},
|
|
},
|
|
{StatusStartFailed, StatusCancelled}: {
|
|
TriggerCommand: {},
|
|
TriggerExternalBlock: {},
|
|
},
|
|
{StatusStarting, StatusCancelled}: {
|
|
TriggerExternalBlock: {},
|
|
},
|
|
{StatusRunning, StatusCancelled}: {
|
|
TriggerExternalBlock: {},
|
|
},
|
|
{StatusPaused, StatusCancelled}: {
|
|
TriggerExternalBlock: {},
|
|
},
|
|
}
|
|
|
|
// AllowedTransitions returns a copy of the `(from, to) -> {triggers}` table
|
|
// used by Transition. The returned map is safe to mutate; callers should
|
|
// not rely on iteration order.
|
|
func AllowedTransitions() map[Status]map[Status][]Trigger {
|
|
result := make(map[Status]map[Status][]Trigger, len(allowedTransitions))
|
|
for key, triggers := range allowedTransitions {
|
|
inner, ok := result[key.from]
|
|
if !ok {
|
|
inner = make(map[Status][]Trigger)
|
|
result[key.from] = inner
|
|
}
|
|
copied := make([]Trigger, 0, len(triggers))
|
|
for trigger := range triggers {
|
|
copied = append(copied, trigger)
|
|
}
|
|
inner[key.to] = copied
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Transition reports whether from may transition to next under trigger.
|
|
// The function returns nil when the triplet is permitted, and an
|
|
// *InvalidTransitionError wrapping ErrInvalidTransition otherwise. It does
|
|
// not touch any store and is safe to call from any layer.
|
|
func Transition(from Status, next Status, trigger Trigger) error {
|
|
if !from.IsKnown() || !next.IsKnown() || !trigger.IsKnown() {
|
|
return &InvalidTransitionError{From: from, To: next, Trigger: trigger}
|
|
}
|
|
|
|
triggers, ok := allowedTransitions[transitionKey{from: from, to: next}]
|
|
if !ok {
|
|
return &InvalidTransitionError{From: from, To: next, Trigger: trigger}
|
|
}
|
|
if _, ok := triggers[trigger]; !ok {
|
|
return &InvalidTransitionError{From: from, To: next, Trigger: trigger}
|
|
}
|
|
|
|
return nil
|
|
}
|