Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-guest-room-roster.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kit": patch
---

Fix casino games showing a 0 balance and refusing wagers. The guest runtime built a fresh `room` every callback but created the game's `Handler` (and its stored `Services`) only once at `OnStart`, so `svc.Credits` was permanently bound to the *start* callback's roster. `creditsSvc` resolves a player's host-side index against that roster's `ctx.members`, which is empty for a solo game (the player joins after start) — so every `Balance`/`Wager`/`Settle` was denied at the guest facade (`ErrCreditsDenied`) and never reached the host. The room is now a single persistent instance refreshed in place each callback, so the game's stored `Services` always resolve against the live roster. (The `kittest`/native/memsvc doubles resolve by account id, not roster index, which is why unit tests and conformance did not catch this — it only manifested on the real wasm ABI path.) Casino games must be rebuilt against this release to pick up the fix.
17 changes: 16 additions & 1 deletion internal/game/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ var (
theGame Game
handler Handler
rng *rand.Rand
// theRoom is the single, persistent Room, refreshed in place each callback
// (its ctx is reassigned in decodeCall). The game's Services are built ONCE
// from it at NewRoom and stored by the game, so any roster-dependent surface
// they expose must see the LIVE roster: Credits resolves a player's host
// index against ctx.members, so a fresh room per callback would bind the
// game's stored svc.Credits to the START roster forever — denying credits to
// everyone who joins after start (a solo player joins after OnStart, so its
// start roster is empty and every Balance/Wager is denied). One reused room
// keeps the stored Services pointed at the current ctx.
theRoom = &room{}
)

// Run installs the game. The instance's Handler is created lazily on start.
Expand Down Expand Up @@ -68,7 +78,12 @@ func decodeCall() (*room, *wire.Rd) {
if rosterChanged {
invalidateBaselines()
}
rm := &room{ctx: ctx, rng: rng}
// Refresh the single persistent room in place (NOT a fresh room each
// callback) so the game's once-built Services keep resolving against the
// live ctx — see theRoom.
theRoom.ctx = ctx
theRoom.rng = rng
rm := theRoom
if epochMismatch && !epochMismatchLogged {
// Host fault: an unchanged-form ctx carried an epoch we don't hold.
// Degraded (cached roster kept, baselines invalidated) — warn once.
Expand Down
Loading