e63748c344
Adds backend/internal/devsandbox: an idempotent boot-time hook that, when BACKEND_DEV_SANDBOX_EMAIL is set, ensures (1) the configured engine_version row, (2) the real dev user, (3) PlayerCount-1 deterministic dummy users, (4) a private "Dev Sandbox" game with a year-out turn schedule, (5) memberships for every participant via the new lobby.Service.InsertMembershipDirect helper, (6) a drive of the lifecycle to running. Re-running on a populated DB is a no-op; partial states from earlier crashes are recovered. tools/local-dev gains the matching env vars in .env, surfaces them in compose, and acquires a `make build-engine` target that builds galaxy-engine:local-dev from game/Dockerfile (a prerequisite of `up`/`rebuild`). The compose game-state mount is changed from a named volume to a host bind on /tmp/galaxy-game-state so backend's bind-mount source for spawned engine containers resolves on the docker daemon. After `make -C tools/local-dev up`, login as dev@local.test with the dev code 123456 and the Dev Sandbox already shows up in My Games. Per-user behaviour for the same email survives a backend restart. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
97 lines
3.0 KiB
Go
97 lines
3.0 KiB
Go
package lobby
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// InsertMembershipDirectInput is the parameter struct for
|
|
// Service.InsertMembershipDirect.
|
|
type InsertMembershipDirectInput struct {
|
|
GameID uuid.UUID
|
|
UserID uuid.UUID
|
|
RaceName string
|
|
}
|
|
|
|
// InsertMembershipDirect grants a membership to userID inside gameID
|
|
// bypassing the application/approval flow. It performs the same DB
|
|
// writes as ApproveApplication: the per-game race-name reservation
|
|
// row plus the membership row, and refreshes the in-memory caches.
|
|
//
|
|
// The method is intended for boot-time provisioning by
|
|
// `backend/internal/devsandbox` and similar trusted callers. It is
|
|
// not exposed through any HTTP handler. The caller must guarantee
|
|
// game.Status == GameStatusEnrollmentOpen — the function returns
|
|
// ErrConflict otherwise — and that the race-name policy and
|
|
// canonical-key invariants are honoured (the implementation reuses
|
|
// the lobby's own Policy and assertRaceNameAvailable so a duplicate
|
|
// or unsuitable name still fails).
|
|
//
|
|
// Idempotency: if a membership for (GameID, UserID) already exists
|
|
// the function returns the existing row without modifying state.
|
|
// This makes the helper safe to call on every backend boot from
|
|
// devsandbox.Bootstrap.
|
|
func (s *Service) InsertMembershipDirect(ctx context.Context, in InsertMembershipDirectInput) (Membership, error) {
|
|
displayName, err := ValidateDisplayName(in.RaceName)
|
|
if err != nil {
|
|
return Membership{}, err
|
|
}
|
|
game, err := s.GetGame(ctx, in.GameID)
|
|
if err != nil {
|
|
return Membership{}, err
|
|
}
|
|
if game.Status != GameStatusEnrollmentOpen {
|
|
return Membership{}, fmt.Errorf("%w: game status is %q, want enrollment_open", ErrConflict, game.Status)
|
|
}
|
|
canonical, err := s.deps.Policy.Canonical(displayName)
|
|
if err != nil {
|
|
return Membership{}, err
|
|
}
|
|
existing, err := s.deps.Store.ListMembershipsForGame(ctx, in.GameID)
|
|
if err != nil {
|
|
return Membership{}, err
|
|
}
|
|
for _, m := range existing {
|
|
if m.UserID == in.UserID && m.Status == MembershipStatusActive {
|
|
return m, nil
|
|
}
|
|
}
|
|
if err := s.assertRaceNameAvailable(ctx, canonical, in.UserID, in.GameID); err != nil {
|
|
return Membership{}, err
|
|
}
|
|
now := s.deps.Now().UTC()
|
|
if _, err := s.deps.Store.InsertRaceName(ctx, raceNameInsert{
|
|
Name: displayName,
|
|
Canonical: canonical,
|
|
Status: RaceNameStatusReservation,
|
|
OwnerUserID: in.UserID,
|
|
GameID: in.GameID,
|
|
ReservedAt: &now,
|
|
}); err != nil {
|
|
return Membership{}, err
|
|
}
|
|
membership, err := s.deps.Store.InsertMembership(ctx, membershipInsert{
|
|
MembershipID: uuid.New(),
|
|
GameID: in.GameID,
|
|
UserID: in.UserID,
|
|
RaceName: displayName,
|
|
CanonicalKey: canonical,
|
|
})
|
|
if err != nil {
|
|
_ = s.deps.Store.DeleteRaceName(ctx, canonical, in.GameID)
|
|
return Membership{}, err
|
|
}
|
|
s.deps.Cache.PutMembership(membership)
|
|
s.deps.Cache.PutRaceName(RaceNameEntry{
|
|
Name: displayName,
|
|
Canonical: canonical,
|
|
Status: RaceNameStatusReservation,
|
|
OwnerUserID: in.UserID,
|
|
GameID: in.GameID,
|
|
ReservedAt: &now,
|
|
})
|
|
return membership, nil
|
|
}
|