package lobby import ( "context" "github.com/google/uuid" "go.uber.org/zap" ) // EntitlementProvider is the read-only view the lobby needs over the // user-domain entitlement snapshot. The canonical implementation is // `*user.Service` exposing `GetEntitlement(ctx, userID)`; tests substitute // a fake. // // `MaxRegisteredRaceNames` is the only field consumed by when // the caller attempts to register a `pending_registration` row the lobby // counts already-`registered` rows for that user against this limit. type EntitlementProvider interface { GetMaxRegisteredRaceNames(ctx context.Context, userID uuid.UUID) (int32, error) } // RuntimeGateway is the outbound surface the lobby uses to ask the runtime // module to start, pause, resume, or stop an engine container. The real // implementation lives in `backend/internal/runtime` ; until // then `NewNoopRuntimeGateway` ships a logger-only stub that pretends the // request was accepted so the lobby state machine stays exercisable // end-to-end. type RuntimeGateway interface { StartGame(ctx context.Context, gameID uuid.UUID) error StopGame(ctx context.Context, gameID uuid.UUID) error PauseGame(ctx context.Context, gameID uuid.UUID) error ResumeGame(ctx context.Context, gameID uuid.UUID) error } // RuntimeJobResult is the inbound shape used by the runtime reconciler // when a labelled container that lobby believes is alive has // disappeared. The wiring connects `Service.OnRuntimeJobResult` against // this type; the no-op consumer logs the event at debug level. type RuntimeJobResult struct { Op string Status string Message string } // NotificationPublisher is the outbound surface the lobby uses to fan out // notification intents (invite received, application submitted, race name // promoted, etc.). The real implementation lives in // `backend/internal/notification` ; until then // `NewNoopNotificationPublisher` ships a logger-only stub. type NotificationPublisher interface { PublishLobbyEvent(ctx context.Context, intent LobbyNotification) error } // DiplomailPublisher is the outbound surface the lobby uses to drop a // durable system mail entry whenever a game-state or // membership-state transition needs to land in the affected players' // inboxes. The real implementation in `cmd/backend/main` adapts the // `*diplomail.Service.PublishLifecycle` call; tests and partial // wiring fall back to `NewNoopDiplomailPublisher`. type DiplomailPublisher interface { PublishLifecycle(ctx context.Context, event LifecycleEvent) error } // LifecycleEvent is the open shape carried by a system-mail intent. // `Kind` is one of the lobby-internal constants // (`LifecycleKindGamePaused`, etc.). `TargetUser` is populated only // for membership-scoped events; the publisher derives the game-scoped // recipient set itself. type LifecycleEvent struct { GameID uuid.UUID Kind string Actor string Reason string TargetUser *uuid.UUID } // Lifecycle-event kinds the lobby emits. const ( LifecycleKindGamePaused = "game.paused" LifecycleKindGameCancelled = "game.cancelled" LifecycleKindMembershipRemoved = "membership.removed" LifecycleKindMembershipBlocked = "membership.blocked" ) // LobbyNotification is the open shape carried by a notification intent. // The implementation emits a small set of `Kind` values matching the catalog in // `backend/README.md` §10. The `Payload` map is the kind-specific data // blob; recipients are the user_ids the intent should reach. // // The struct lives in the lobby package on purpose: it is the producer // vocabulary. The implementation will reuse it as the notification.Submit input // (or wrap it in a domain-side type, if more channels show up). type LobbyNotification struct { Kind string IdempotencyKey string Recipients []uuid.UUID Payload map[string]any } // NewNoopRuntimeGateway returns a RuntimeGateway that logs every call at // debug level and returns nil. The lobby state machine treats the no-op // as "request was accepted asynchronously" — the game stays in `starting` // until the canonical implementation wires real `runtime` / `OnRuntimeSnapshot` interactions. func NewNoopRuntimeGateway(logger *zap.Logger) RuntimeGateway { if logger == nil { logger = zap.NewNop() } return &noopRuntimeGateway{logger: logger.Named("lobby.runtime.noop")} } type noopRuntimeGateway struct { logger *zap.Logger } func (g *noopRuntimeGateway) StartGame(_ context.Context, gameID uuid.UUID) error { g.logger.Debug("noop start-game", zap.String("game_id", gameID.String())) return nil } func (g *noopRuntimeGateway) StopGame(_ context.Context, gameID uuid.UUID) error { g.logger.Debug("noop stop-game", zap.String("game_id", gameID.String())) return nil } func (g *noopRuntimeGateway) PauseGame(_ context.Context, gameID uuid.UUID) error { g.logger.Debug("noop pause-game", zap.String("game_id", gameID.String())) return nil } func (g *noopRuntimeGateway) ResumeGame(_ context.Context, gameID uuid.UUID) error { g.logger.Debug("noop resume-game", zap.String("game_id", gameID.String())) return nil } // NewNoopNotificationPublisher returns a NotificationPublisher that logs // every call at debug level and returns nil. The implementation will swap in a // real publisher backed by `notification.Submit`. func NewNoopNotificationPublisher(logger *zap.Logger) NotificationPublisher { if logger == nil { logger = zap.NewNop() } return &noopNotificationPublisher{logger: logger.Named("lobby.notify.noop")} } type noopNotificationPublisher struct { logger *zap.Logger } func (p *noopNotificationPublisher) PublishLobbyEvent(_ context.Context, intent LobbyNotification) error { p.logger.Debug("noop notification", zap.String("kind", intent.Kind), zap.String("idempotency_key", intent.IdempotencyKey), zap.Int("recipients", len(intent.Recipients)), ) return nil } // NewNoopDiplomailPublisher returns a DiplomailPublisher that logs // every call at debug level and returns nil. Used by tests and by // the lobby Service factory when the Deps.Diplomail field is left // nil. func NewNoopDiplomailPublisher(logger *zap.Logger) DiplomailPublisher { if logger == nil { logger = zap.NewNop() } return &noopDiplomailPublisher{logger: logger.Named("lobby.diplomail.noop")} } type noopDiplomailPublisher struct { logger *zap.Logger } func (p *noopDiplomailPublisher) PublishLifecycle(_ context.Context, event LifecycleEvent) error { p.logger.Debug("noop diplomail lifecycle", zap.String("kind", event.Kind), zap.String("game_id", event.GameID.String()), ) return nil }