From f72d35e114fbcb312fdb55307430af5a9f219248 Mon Sep 17 00:00:00 2001 From: Brandon Cook Date: Fri, 3 Jul 2026 17:38:38 +1000 Subject: [PATCH] feat: add credits_buyback host function (wire revision 8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Casino-kind games can now offer a broke player a mid-session rebuy without leaving the game. `Credits.Buyback(p) (int64, error)` returns the new balance (symmetric to Balance); the host owns the gate (broke-only floor + a per-day rebuy limit) and makes the credited amount wagerable in the current seat. A refusal (solvent, or the daily limit reached) is CreditsErrInsufficient with the balance unchanged. Additive minor: existing games are unaffected; the meta payload is unchanged (only a new host function the guest may import). Wire Revision 7 -> 8. - wire: FnCreditsBuyback, Revision 8 + ledger entry; rust WIRE_REVISION 8; scalar + hardcoded rust meta goldens regenerated for the revision byte. - guest SDK: Credits.Buyback (Go internal/game api/host/room + dev runner; rust host extern/wrapper/test-host). - host: sdk.CreditsService.Buyback + gameabi credits_buyback dispatch (routes through creditsCall, inheriting the casino-kind guard / roster bounds / timeout); memsvc + kittest doubles + fakeCredits stub. - ABI.md §3: credits_buyback row + buyback semantics; revision-8 note. - memsvc test: solvent-refused / mid-hand-not-broke / broke-tops-up. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01W7oJQSTva5xgbAZomdV5Jj --- .changeset/credits-buyback.md | 5 +++++ ABI.md | 17 +++++++++++---- host/gameabi/gamekind_test.go | 4 ++++ host/gameabi/host.go | 8 +++++++ host/memsvc/memsvc.go | 24 +++++++++++++++++++++ host/memsvc/memsvc_test.go | 37 +++++++++++++++++++++++++++++++++ host/sdk/services.go | 5 +++++ internal/game/api.go | 8 +++++++ internal/game/devrun.go | 13 ++++++++++++ internal/game/host.go | 7 +++++++ internal/game/room.go | 12 +++++++++++ kittest/kittest.go | 39 +++++++++++++++++++++++++++++++++++ rust/Cargo.lock | 2 +- rust/src/host.rs | 26 +++++++++++++++++++++++ rust/src/wire.rs | 10 ++++----- rust/tests/golden/scalars.txt | 8 +++---- wire/wire.go | 7 ++++++- 17 files changed, 217 insertions(+), 15 deletions(-) create mode 100644 .changeset/credits-buyback.md diff --git a/.changeset/credits-buyback.md b/.changeset/credits-buyback.md new file mode 100644 index 0000000..72b9255 --- /dev/null +++ b/.changeset/credits-buyback.md @@ -0,0 +1,5 @@ +--- +"kit": minor +--- + +Add the `credits_buyback` host function (wire revision 8) for casino-kind games: a broke player can trigger a mid-session rebuy without leaving the game. `Credits.Buyback(p) (int64, error)` returns the new balance (symmetric to `Balance`); the host gates it (broke-only floor + a per-day rebuy limit it owns) and makes the credited amount wagerable in the current seat. Additive — existing games are unaffected. The guest SDK (Go + Rust), the host `sdk.CreditsService`, the `memsvc`/`kittest`/dev-runner doubles, and `ABI.md` all gain the method. diff --git a/ABI.md b/ABI.md index d87c1f6..d7a069e 100644 --- a/ABI.md +++ b/ABI.md @@ -94,6 +94,7 @@ means not-found. | `credits_balance` | (i64 playerIdx) → i64 | the player's account-wide credits balance (≥ 0), or a negative status (below); casino-kind guests only | | `credits_wager` | (i64 playerIdx, i64 amount) → i64 | atomically escrow `amount` from the balance into the seat's open stake; 0 ok or a negative status | | `credits_settle` | (i64 playerIdx, i64 payout) → i64 | close the seat's open stake with the GROSS (stake-inclusive) payout, clamped to stake × the declared `maxPayoutMultiplier`; 0 ok or a negative status | +| `credits_buyback` | (i64 playerIdx) → i64 | broke-relief rebuy for a player who ran out mid-session; returns the new balance (≥ 0) or a negative status. The host gates it (broke-only floor + per-day rebuy limit); a refusal is `-1` insufficient with the balance unchanged. Revision 8. | `send` and `identical` return an `i64` whose **low 32 bits carry the epoch** the guest MUST stamp its baseline with for that slot; the **upper 32 bits are @@ -114,9 +115,10 @@ Scoping is host-side: the guest names only a roster index and a key — the account and the game's namespace are derived by the host. A guest cannot address another game's data or a non-member account. -**Credits (casino-kind games, revision 7).** The three `credits_*` functions -exist for guests whose meta declares the casino kind (§4.2); the host rejects -calls from game-kind guests. Negative returns are shared status codes: +**Credits (casino-kind games, revision 7; `credits_buyback` added in +revision 8).** The `credits_*` functions exist for guests whose meta declares +the casino kind (§4.2); the host rejects calls from game-kind guests. Negative +returns are shared status codes: `-1` insufficient (the wager exceeded the balance or a platform bet limit — the bet did not happen) · `-2` disabled (the host's economy is switched off: render an out-of-service state, never trap) · `-3` denied (game-kind guest, @@ -130,7 +132,14 @@ keeps the triggering stake open and settles once with the total. The host clamps every settlement to stake × the game's declared payout multiplier (itself clamped by a platform ceiling), refunds open stakes on paths where no game code can run (crash, teardown), and voids in-flight stakes across a -restore — a game never persists a balance of its own. +restore — a game never persists a balance of its own. Buyback semantics +(`credits_buyback`, revision 8): a casino guest offers a broke player a rebuy; +the host applies it only when the player is broke (balance below a platform +floor, open stake excluded) and under a per-day rebuy limit, credits the +platform's buyback amount to the account AND makes it wagerable in the current +seat, and returns the new balance. A refusal (solvent, or the daily limit +reached) is `-1` insufficient with the balance unchanged — render it, do not +retry. ## 4. Payload encodings diff --git a/host/gameabi/gamekind_test.go b/host/gameabi/gamekind_test.go index 6e9072e..58d0c92 100644 --- a/host/gameabi/gamekind_test.go +++ b/host/gameabi/gamekind_test.go @@ -56,6 +56,10 @@ func (f *fakeCredits) Settle(context.Context, sdk.Player, int64) error { f.calls++ return f.err } +func (f *fakeCredits) Buyback(context.Context, sdk.Player) (int64, error) { + f.calls++ + return f.balance, f.err +} // creditsCall's gating and error mapping: game-kind guests, out-of-roster // indices, and a host without an economy are refused BEFORE any service diff --git a/host/gameabi/host.go b/host/gameabi/host.go index 8efffbe..dbbc089 100644 --- a/host/gameabi/host.go +++ b/host/gameabi/host.go @@ -1193,6 +1193,14 @@ func hostFunctions() []extism.HostFunction { return wire.CreditsOK, svc.Settle(cctx, pl, payout) })) }), + hf(wire.FnCreditsBuyback, []extism.ValueType{i64}, []extism.ValueType{i64}, + func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) { + h := currentHandler(ctx) + idx := int(stack[0]) + stack[0] = uint64(h.creditsCall(ctx, idx, func(cctx context.Context, svc sdk.CreditsService, pl sdk.Player) (int64, error) { + return svc.Buyback(cctx, pl) + })) + }), } } diff --git a/host/memsvc/memsvc.go b/host/memsvc/memsvc.go index 863f8fb..987d553 100644 --- a/host/memsvc/memsvc.go +++ b/host/memsvc/memsvc.go @@ -230,6 +230,30 @@ func (c *memCredits) Settle(_ context.Context, p sdk.Player, payout int64) error return nil } +// memBuybackFloor / memBuybackAmount mirror the platform defaults so `check` +// and the dev runner exercise the same broke-relief shape. memsvc does NOT +// enforce the platform's per-day rebuy limit — that count cap lives only in +// the durable ledger; here a broke player can always rebuy. +const ( + memBuybackFloor int64 = 100 + memBuybackAmount int64 = 1000 +) + +func (c *memCredits) Buyback(_ context.Context, p sdk.Player) (int64, error) { + c.f.mu.Lock() + defer c.f.mu.Unlock() + acc := c.account(p) + // Broke-only: solvent players are refused (as the real floor gate does). + // The open stake counts toward solvency — a seat mid-hand is not broke. + if acc.balance+acc.stakes[c.roomID] >= memBuybackFloor { + return acc.balance, sdk.ErrInsufficientCredits + } + if acc.balance < memBuybackAmount { + acc.balance = memBuybackAmount + } + return acc.balance, nil +} + // ---- per-user KV ---- type kvKey struct{ slug, account, key string } diff --git a/host/memsvc/memsvc_test.go b/host/memsvc/memsvc_test.go index f0f5222..a44f82b 100644 --- a/host/memsvc/memsvc_test.go +++ b/host/memsvc/memsvc_test.go @@ -317,3 +317,40 @@ func (g testGame) Meta() sdk.GameMeta { return sdk.GameMeta{Slug: g.slug, Name: func (g testGame) NewRoom(cfg sdk.RoomConfig, svc sdk.Services) sdk.Handler { return sdk.Base{} // the Reader path never builds a room; identity only } + +// The credits double supports the mid-session broke-relief rebuy: a solvent +// player is refused, a broke player (open stake excluded) is topped up, and a +// player mid-hand (all funds in the open stake) still counts as broke only +// once the stake is settled to a loss. +func TestCreditsBuyback(t *testing.T) { + f := quietFactory(t) + svc := f.For("room-buyback", "demo") + ctx := context.Background() + p := member("a", "Ada") + + // Seeded at 1000: solvent, so buyback refuses and leaves the balance. + if bal, err := svc.Credits.Buyback(ctx, p); !errIs(err, sdk.ErrInsufficientCredits) || bal != 1000 { + t.Fatalf("solvent buyback = %d err=%v, want 1000 + ErrInsufficientCredits", bal, err) + } + + // Wager the whole balance and lose it: now broke. + if err := svc.Credits.Wager(ctx, p, 1000); err != nil { + t.Fatalf("wager: %v", err) + } + // Mid-hand (stake open) is NOT broke — the stake counts toward solvency. + if _, err := svc.Credits.Buyback(ctx, p); !errIs(err, sdk.ErrInsufficientCredits) { + t.Fatalf("mid-hand buyback err = %v, want ErrInsufficientCredits (stake is not broke)", err) + } + if err := svc.Credits.Settle(ctx, p, 0); err != nil { // lose it + t.Fatalf("settle loss: %v", err) + } + if bal, _ := svc.Credits.Balance(ctx, p); bal != 0 { + t.Fatalf("balance after loss = %d, want 0", bal) + } + // Now broke: buyback tops up to the amount and returns the new balance. + if bal, err := svc.Credits.Buyback(ctx, p); err != nil || bal != 1000 { + t.Fatalf("broke buyback = %d err=%v, want 1000", bal, err) + } +} + +func errIs(err, target error) bool { return err != nil && err == target } diff --git a/host/sdk/services.go b/host/sdk/services.go index 699cbb7..5ddf61a 100644 --- a/host/sdk/services.go +++ b/host/sdk/services.go @@ -33,6 +33,11 @@ type CreditsService interface { // Settle closes the seat's open stake with the gross payout (0 = loss), // clamped by the implementation. Settle(ctx context.Context, p Player, payout int64) error + // Buyback applies the platform broke-relief rebuy for a player who ran + // out mid-session and returns the new balance. The implementation owns + // the gate (broke-only floor, per-day rebuy limit) and makes the credited + // amount wagerable in the seat; a refusal is ErrInsufficientCredits. + Buyback(ctx context.Context, p Player) (int64, error) } // ServicesFactory constructs a per-room Services. It has distinct diff --git a/internal/game/api.go b/internal/game/api.go index 2d2ea00..0718cd8 100644 --- a/internal/game/api.go +++ b/internal/game/api.go @@ -106,6 +106,14 @@ type Credits interface { Wager(p Player, amount int64) error // Settle closes the seat's open stake with the gross payout (0 = loss). Settle(p Player, payout int64) error + // Buyback triggers the platform broke-relief rebuy for a player who has + // run out of credits mid-session, returning the new balance. The host + // gates it (a broke-only floor and a per-day rebuy limit); a refusal + // (already solvent, or the daily limit reached) surfaces as + // ErrInsufficientCredits with the balance unchanged — render it, do not + // retry. The credited amount lands in the player's account AND is made + // wagerable in the current seat. + Buyback(p Player) (int64, error) } // Credits errors, mirrored from the ABI status codes. ErrEconomyDisabled diff --git a/internal/game/devrun.go b/internal/game/devrun.go index 56a72a5..38d290f 100644 --- a/internal/game/devrun.go +++ b/internal/game/devrun.go @@ -416,6 +416,19 @@ func (c nativeCredits) Settle(p Player, payout int64) error { return nil } +func (c nativeCredits) Buyback(p Player) (int64, error) { + st := c.state(p) + // Broke-only, mirroring the platform floor/amount defaults; the dev + // runner does not model the per-day rebuy limit. + if st.balance+st.stake >= 100 { + return st.balance, ErrInsufficientCredits + } + if st.balance < 1000 { + st.balance = 1000 + } + return st.balance, nil +} + type nativeAccounts struct{ r *nativeRoom } func (a nativeAccounts) For(p Player) Account { return nativeAccount{a.r, p} } diff --git a/internal/game/host.go b/internal/game/host.go index 944e024..c759d25 100644 --- a/internal/game/host.go +++ b/internal/game/host.go @@ -58,6 +58,13 @@ func hostCreditsWager(playerIdx uint64, amount uint64) uint64 //go:wasmimport extism:host/user credits_settle func hostCreditsSettle(playerIdx uint64, payout uint64) uint64 +// credits_buyback (revision 8): broke-relief rebuy; returns the new balance +// (>= 0) or a negative wire.CreditsErr* (ErrInsufficient = refused: solvent +// or daily limit reached). + +//go:wasmimport extism:host/user credits_buyback +func hostCreditsBuyback(playerIdx uint64) uint64 + // alloc copies b into Extism kernel memory; the caller MUST Free it after the // host call returns (kernel memory is not garbage collected). func alloc(b []byte) pdk.Memory { return pdk.AllocateBytes(b) } diff --git a/internal/game/room.go b/internal/game/room.go index c19fddd..57e36d0 100644 --- a/internal/game/room.go +++ b/internal/game/room.go @@ -189,6 +189,18 @@ func (c creditsSvc) Settle(p Player, payout int64) error { return creditsErr(int64(hostCreditsSettle(uint64(idx), uint64(payout)))) } +func (c creditsSvc) Buyback(p Player) (int64, error) { + idx := c.index(p) + if idx < 0 { + return 0, ErrCreditsDenied + } + code := int64(hostCreditsBuyback(uint64(idx))) + if code < 0 { + return 0, creditsErr(code) + } + return code, nil +} + type accountStore struct{ r *room } func (s accountStore) For(p Player) Account { diff --git a/kittest/kittest.go b/kittest/kittest.go index 43fd8cb..3727f07 100644 --- a/kittest/kittest.go +++ b/kittest/kittest.go @@ -85,6 +85,17 @@ type Room struct { // credits call returns kit.ErrEconomyDisabled, the state a casino game // must render as out-of-service rather than trap. CreditsDisabled bool + + // CreditsBuybackFloor / CreditsBuybackAmount configure the broke-relief + // rebuy (defaults 100 / 1000, the platform defaults). Buyback refuses a + // solvent player (balance + open stake >= floor) with + // ErrInsufficientCredits and otherwise tops the balance up to the amount. + // The kittest double does NOT enforce the platform's per-day rebuy limit + // (that lives in the durable ledger); CreditsRebuys counts calls that + // credited, for test assertions. + CreditsBuybackFloor int64 + CreditsBuybackAmount int64 + CreditsRebuys map[string]int } // NewRoom returns a Room with the given members, a seeded RNG (seed 1), and a @@ -268,6 +279,34 @@ func (c credits) Settle(p kit.Player, payout int64) error { return nil } +func (c credits) Buyback(p kit.Player) (int64, error) { + if c.r.CreditsDisabled { + return 0, kit.ErrEconomyDisabled + } + floor := c.r.CreditsBuybackFloor + if floor == 0 { + floor = 100 + } + amount := c.r.CreditsBuybackAmount + if amount == 0 { + amount = 1000 + } + bal := c.balance(p.AccountID) + // Broke-only: a solvent player — counting any open stake — is refused, + // exactly as the platform floor gate does. + if bal+c.r.CreditsStakes[p.AccountID] >= floor { + return bal, kit.ErrInsufficientCredits + } + if bal < amount { + c.r.Credits[p.AccountID] = amount + } + if c.r.CreditsRebuys == nil { + c.r.CreditsRebuys = map[string]int{} + } + c.r.CreditsRebuys[p.AccountID]++ + return c.r.Credits[p.AccountID], nil +} + type config struct{ r *Room } func (c config) Get(_ context.Context, key string) ([]byte, bool, error) { diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9c6c7b7..570e430 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -304,7 +304,7 @@ dependencies = [ [[package]] name = "shellcade-kit" -version = "2.14.0" +version = "2.15.0" dependencies = [ "extism-pdk", ] diff --git a/rust/src/host.rs b/rust/src/host.rs index ada74b6..4c31487 100644 --- a/rust/src/host.rs +++ b/rust/src/host.rs @@ -48,6 +48,9 @@ mod imp { fn credits_wager(player_idx: u64, amount: i64) -> i64; /// credits_settle(i64 playerIdx, i64 payout) → i64 status. fn credits_settle(player_idx: u64, payout: i64) -> i64; + /// credits_buyback(i64 playerIdx) → i64 new balance (>= 0) or a + /// negative CREDITS_ERR_* (ABI.md §3; revision 8). + fn credits_buyback(player_idx: u64) -> i64; } /// Stage pre-serialized bytes in extism kernel memory. Infallible by @@ -172,6 +175,11 @@ mod imp { // SAFETY: as host_credits_balance. unsafe { credits_settle(player_idx as u64, payout) } } + + pub fn host_credits_buyback(player_idx: usize) -> i64 { + // SAFETY: as host_credits_balance. + unsafe { credits_buyback(player_idx as u64) } + } } // ---- native test host (cargo test) --------------------------------------------- @@ -337,6 +345,24 @@ mod imp { crate::wire::CREDITS_OK }) } + + pub fn host_credits_buyback(player_idx: usize) -> i64 { + with_test_host(|h| { + if h.credits_disabled { + return crate::wire::CREDITS_ERR_DISABLED; + } + // Broke-only, platform floor/amount defaults (100 / 1000); the + // test host does not model the per-day rebuy limit. + let bal = credits_balance_of(h, player_idx); + let stake = h.credits_stakes.get(&player_idx).copied().unwrap_or(0); + if bal + stake >= 100 { + return crate::wire::CREDITS_ERR_INSUFFICIENT; + } + let new_bal = bal.max(1000); + h.credits.insert(player_idx, new_bal); + new_bal + }) + } } pub(crate) use imp::*; diff --git a/rust/src/wire.rs b/rust/src/wire.rs index e5197e2..662505a 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -204,7 +204,7 @@ fn decode_members(r: &mut Rd<'_>, n: usize, features: u32) -> Vec { /// `wire.Revision` — one protocol constant, asserted equal in lockstep by /// the Go cross-check test `wire.TestRustWireRevisionMatchesWire` (which /// parses this source line; keep the declaration on one line). -pub(crate) const WIRE_REVISION: u16 = 7; +pub(crate) const WIRE_REVISION: u16 = 8; /// Credits host-function status codes (ABI.md §3): `credits_balance` returns /// the balance (>= 0) or one of the negatives; wager/settle return @@ -635,7 +635,7 @@ mod tests { ], ..Meta::DEFAULT }; - let golden = "0600676f6c64656e0600476f6c64656e0e00676f6c64656e206669787475726501000400020001006101006200000000000001050073636f726501000202000c006f6464732d76617269616e740c004f6464732076617269616e740a005041522073686565742e0312007b226e616d65223a2244656661756c74227d11007b2274797065223a226f626a656374227d04006d6f7464060042616e6e65720d00466c6f6f722062616e6e65722e000000000000000000000000070000000000000000"; + let golden = "0600676f6c64656e0600476f6c64656e0e00676f6c64656e206669787475726501000400020001006101006200000000000001050073636f726501000202000c006f6464732d76617269616e740c004f6464732076617269616e740a005041522073686565742e0312007b226e616d65223a2244656661756c74227d11007b2274797065223a226f626a656374227d04006d6f7464060042616e6e65720d00466c6f6f722062616e6e65722e000000000000000000000000080000000000000000"; let got: String = encode_meta(&m).iter().map(|b| format!("{b:02x}")).collect(); assert_eq!(got, golden, "Rust meta encoding diverges from the Go golden"); } @@ -658,7 +658,7 @@ mod tests { ], ..Meta::DEFAULT }; - let golden = "040063746c67040043746c47000001000200000000000000000000000000000000000000070002000072000000060052455349474e01020400554e444f0000000000"; + let golden = "040063746c67040043746c47000001000200000000000000000000000000000000000000080002000072000000060052455349474e01020400554e444f0000000000"; let got: String = encode_meta(&m).iter().map(|b| format!("{b:02x}")).collect(); assert_eq!(got, golden, "Rust controls encoding diverges from the Go golden"); } @@ -790,10 +790,10 @@ mod tests { ..Meta::DEFAULT }; let got: String = encode_meta(&m).iter().map(|b| format!("{b:02x}")).collect(); - // trailer = u32 1 LE + u16 100 LE + u8 lifecycle + u16 revision 7 LE + // trailer = u32 1 LE + u16 100 LE + u8 lifecycle + u16 revision 8 LE // + u16 controls count 0 + u8 kind 0 + u32 multiplier 0 // = "01000000" + "6400" + "00" + "0700" + "0000" + "00" + "00000000" - assert!(got.ends_with("000001000000640000070000000000000000"), "trailer bytes diverge from the Go encoding: ...{}", &got[got.len()-36..]); + assert!(got.ends_with("000001000000640000080000000000000000"), "trailer bytes diverge from the Go encoding: ...{}", &got[got.len()-36..]); } } diff --git a/rust/tests/golden/scalars.txt b/rust/tests/golden/scalars.txt index 67b51e5..9047549 100644 --- a/rust/tests/golden/scalars.txt +++ b/rust/tests/golden/scalars.txt @@ -14,11 +14,11 @@ # fields and reader position); meta_trunc_* are truncated older-form metas # pinning the host-side decoder's presence guards (Go-only — the guest SDKs # carry no meta decoder). -meta_default = 070064656661756c740000000001000100000000000000000000000000000000000000070000000000000000 -meta_full = 0b00676f6c64656e2d66756c6c0b00476f6c64656e2046756c6c170065766572792073656374696f6e20706f70756c6174656402000800020005006d756c74690400636172640a004465616c206d6520696e080050726163746963650d004a6f696e206d79207461626c65010500636869707301010202000c006f6464732d76617269616e740c004f6464732076617269616e740a005041522073686565742e0312007b226e616d65223a2244656661756c74227d11007b2274797065223a226f626a656374227d04006d6f7464060042616e6e65720d00466c6f6f722062616e6e65722e000000000001000000fa0001070002000072000000060052455349474e01020400554e444f0110270000 +meta_default = 070064656661756c740000000001000100000000000000000000000000000000000000080000000000000000 +meta_full = 0b00676f6c64656e2d66756c6c0b00476f6c64656e2046756c6c170065766572792073656374696f6e20706f70756c6174656402000800020005006d756c74690400636172640a004465616c206d6520696e080050726163746963650d004a6f696e206d79207461626c65010500636869707301010202000c006f6464732d76617269616e740c004f6464732076617269616e740a005041522073686565742e0312007b226e616d65223a2244656661756c74227d11007b2274797065223a226f626a656374227d04006d6f7464060042616e6e65720d00466c6f6f722062616e6e65722e000000000001000000fa0001080002000072000000060052455349474e01020400554e444f0110270000 meta_trunc_pre_config = 05007472756e6305005472756e63000001000400000000000000000001050073636f7265010000 -meta_trunc_pre_controls = 05007472756e6305005472756e63000001000400000000000000000001050073636f72650100000000010000006400010700 -meta_trunc_pre_kind = 05007472756e6305005472756e63000001000400000000000000000001050073636f72650100000000010000006400010700010001020400554e444f +meta_trunc_pre_controls = 05007472756e6305005472756e63000001000400000000000000000001050073636f72650100000000010000006400010800 +meta_trunc_pre_kind = 05007472756e6305005472756e63000001000400000000000000000001050073636f72650100000000010000006400010800010001020400554e444f meta_trunc_pre_largeroom = 05007472756e6305005472756e63000001000400000000000000000001050073636f72650100000000 meta_trunc_pre_lifecycle = 05007472756e6305005472756e63000001000400000000000000000001050073636f72650100000000010000006400 meta_trunc_pre_revision = 05007472756e6305005472756e63000001000400000000000000000001050073636f7265010000000001000000640001 diff --git a/wire/wire.go b/wire/wire.go index e1995a9..9b58102 100644 --- a/wire/wire.go +++ b/wire/wire.go @@ -43,6 +43,10 @@ const Version uint32 = 2 // the controls section), the credits host functions // (credits_balance/credits_wager/credits_settle) for casino-kind // guests, and the CtxFeatCredits declaration bit +// 8 — the credits_buyback host function: a casino-kind guest triggers a +// mid-session broke-relief rebuy (returns the new balance like +// credits_balance). No meta-payload change; a new host function the +// guest may call. // // Revisions 1–3 predate the field, so artifacts of those eras decode as 0; // only 0 or values ≥ 4 are ever observed on the wire. Any future change that @@ -54,7 +58,7 @@ const Version uint32 = 2 // (rust/src/wire.rs WIRE_REVISION, asserted equal to this constant by // TestRustWireRevisionMatchesWire in this package) — the two must change in // lockstep. -const Revision uint16 = 7 +const Revision uint16 = 8 // Guest export names. const ( @@ -86,6 +90,7 @@ const ( FnCreditsBalance = "credits_balance" FnCreditsWager = "credits_wager" FnCreditsSettle = "credits_settle" + FnCreditsBuyback = "credits_buyback" ) // Frame geometry: 80x24 cells, 24 bytes per v2 grapheme cell.