3306a016a0
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 28s
CI / ui (pull_request) Successful in 1m11s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m46s
Carry the caller's per-tier active-game caps and each game's kind on the wire (Profile.game_limits + GameView.kind, additive FBS + gateway transcode + client codec, committed regen). The New Game screen counts the player's active games per kind from the lobby and locks a capped start: an outline button with a lock that opens a funnel modal instead of a game -- a sign-in prompt for a guest, a "finish a current game first" notice for a signed-in account (native Telegram popup, in-app modal elsewhere). The lock lifts via the existing profile refetch after a guest->durable upgrade. Remove the lobby's old at_game_limit New-Game tab disable + notice: the flag (now the random-kind cap) conflicted with the per-kind lock -- it hid the screen where the lock lives and wrongly blocked an unfulfilled kind. The New Game tab is always enabled; the per-kind start lock is the only gate. The at_game_limit wire field stays (unused by the client) for a later cleanup. Tests: client lock logic + codec kind/game_limits roundtrip + gateway transcode encode + native popup builders (unit); a mock e2e for the lock badge and the modal.
76 lines
2.5 KiB
Go
76 lines
2.5 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"scrabble/backend/internal/adminconsole"
|
|
"scrabble/backend/internal/gamelimits"
|
|
)
|
|
|
|
// gameLimitsDTOFor resolves the caller's tier active-game caps into the profile DTO, so the client
|
|
// can lock a capped New-Game start per kind. It returns nil when the limits config is not wired.
|
|
func (s *Server) gameLimitsDTOFor(isGuest bool) *gameLimitsDTO {
|
|
if s.gamelimits == nil {
|
|
return nil
|
|
}
|
|
l := s.gamelimits.LimitsFor(isGuest)
|
|
return &gameLimitsDTO{VsAI: l.VsAI, Random: l.Random, Friends: l.Friends}
|
|
}
|
|
|
|
// consoleLimits renders the per-tier, per-kind active-game limit form (backend.config), read from
|
|
// the in-memory cache.
|
|
func (s *Server) consoleLimits(c *gin.Context) {
|
|
cfg := s.gamelimits.Get()
|
|
s.renderConsole(c, "limits", "limits", "Game limits", adminconsole.GameLimitsView{
|
|
GuestVsAI: cfg.Guest.VsAI,
|
|
GuestRandom: cfg.Guest.Random,
|
|
GuestFriends: cfg.Guest.Friends,
|
|
DurableVsAI: cfg.Durable.VsAI,
|
|
DurableRandom: cfg.Durable.Random,
|
|
DurableFriends: cfg.Durable.Friends,
|
|
})
|
|
}
|
|
|
|
// consoleUpdateLimits saves the active-game limits and refreshes the hot cache in place. Each
|
|
// field is a per-kind cap: -1 is unlimited, 0 blocks the kind, a positive value caps concurrent games.
|
|
func (s *Server) consoleUpdateLimits(c *gin.Context) {
|
|
cfg := gamelimits.Config{
|
|
Guest: gamelimits.Limits{
|
|
VsAI: atoiForm(c, "guest_vs_ai"),
|
|
Random: atoiForm(c, "guest_random"),
|
|
Friends: atoiForm(c, "guest_friends"),
|
|
},
|
|
Durable: gamelimits.Limits{
|
|
VsAI: atoiForm(c, "durable_vs_ai"),
|
|
Random: atoiForm(c, "durable_random"),
|
|
Friends: atoiForm(c, "durable_friends"),
|
|
},
|
|
}
|
|
if err := validateGameLimits(cfg); err != nil {
|
|
s.renderConsoleMessage(c, "Invalid", err.Error(), "/_gm/limits")
|
|
return
|
|
}
|
|
if err := s.gamelimits.Update(c.Request.Context(), cfg); err != nil {
|
|
s.consoleError(c, err)
|
|
return
|
|
}
|
|
s.renderConsoleMessage(c, "Saved", "game limits updated", "/_gm/limits")
|
|
}
|
|
|
|
// validateGameLimits rejects a limit below -1 (the unlimited sentinel); -1, 0 and any positive count
|
|
// are valid. It mirrors the backend.config CHECK so a bad value is refused with a clean message
|
|
// rather than a raw database error.
|
|
func validateGameLimits(cfg gamelimits.Config) error {
|
|
for _, v := range []int{
|
|
cfg.Guest.VsAI, cfg.Guest.Random, cfg.Guest.Friends,
|
|
cfg.Durable.VsAI, cfg.Durable.Random, cfg.Durable.Friends,
|
|
} {
|
|
if v < gamelimits.Unlimited {
|
|
return fmt.Errorf("a limit must be -1 (unlimited), 0, or a positive count")
|
|
}
|
|
}
|
|
return nil
|
|
}
|