From fcacfc609ca224b6b9dd6d6e233e91491183302a Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:05:15 +0000 Subject: [PATCH] docs: explain character game ledgers and recovery --- projects/Games/docs/GUILD_TABLES.md | 23 +++++++++------ projects/Games/docs/HOLDEM.md | 2 +- projects/Games/docs/SYSTEM.md | 6 ++-- projects/Games/docs/TEST_MATRIX.md | 13 ++++++++- projects/Games/docs/WAGER_ENGINE.md | 44 ++++++++++++++++++++--------- 5 files changed, 60 insertions(+), 28 deletions(-) diff --git a/projects/Games/docs/GUILD_TABLES.md b/projects/Games/docs/GUILD_TABLES.md index e336ac1..5512caf 100644 --- a/projects/Games/docs/GUILD_TABLES.md +++ b/projects/Games/docs/GUILD_TABLES.md @@ -4,7 +4,7 @@ Guild-house tables connect blackjack operation to guild permissions, auto-dealer Bank money reaches the felt only through `BankAccount`, as a leg of a wager transaction. See [WAGER_ENGINE.md](WAGER_ENGINE.md). -Games **softdepends SimpleFactions** and may call guild / bank / modifiers. SimpleFactions **does not** import Games, persist table ids, or listen to table events. **No Bukkit events** for create, dealer, or bank. +Games requires DenarEconomy and **softdepends SimpleFactions** for guild identity, modifiers and profit bookkeeping. SimpleFactions **does not** import Games, persist table ids, or listen to table events. **No Bukkit events** for create, dealer, or bank. ## Modes @@ -26,21 +26,26 @@ Staff auto needs `games.autodealer.staff`. It is not the default for a normal pl On place, store: - `ownerPlayer` (placer UUID) -- `ownerGuildId` (guild id at place time, or null if no SF / no guild) +- `ownerGuildId` (guild id at place time, or null for a personal table) +- `ownerGuildIncarnation` (the owning guild's permanent UUID) -Cap and bank always use **that guild id**, not "whoever is standing here." Re-resolve the guild object on cover / pickup. If the guild is gone: refuse auto cover, and on pickup skip bank deposit (chips still despawn / given per existing teardown). +Guild membership and leadership use the active character. Editing or picking up a guild table requires that character to lead the captured guild incarnation, or staff permission. The placer account alone gives no guild-table rights. The account remains the host of a personal table. + +The captured incarnation selects the `guild:` Denars account. A deleted guild's later namesake cannot use its tables, bank or expansion slots. Missing guilds, unavailable faction services and unresolved incarnations refuse funding; tray settlement retains the table and chips for review instead of dropping money through a failure fallback. + +Existing guild tables wait for `SimpleFactionsApi.ready()`. Migration preserves each original JSON file in `migrations/guild-tables/backup/` and writes a complete migration plan before applying it. Each table resolves its guild id once. Restart reuses that plan; an unresolved guild remains unresolved even if a namesake appears later. Deferred loads retry every five seconds without reloading already active tables. ## Cap SF `GuildModifier.AUTO_DEALER_TABLES` from an **expansion upgrade** (not a branch). `allowed-types`: `guild` and `realm`. Stacks with upgrade level via existing `Guild.getModifier`. -A table counts against the cap if `autoDealer && !staffMint` and `ownerGuildId` matches. +A non-staff guild table counts against the cap when its guild id and captured incarnation match the current guild. Refuse (do not place, or do not turn auto on) when `count >= cap`. Cap `0` means guild auto is off. Missing SF: guild auto off; staff mint still works. If upgrades drop so `count > cap`, **do not** pick up tables or cancel a live round. Every counted table of that guild **cannot start a new round** until `count <= cap` (turn auto off or pick up extras). A bet window that was already open refunds and does not deal. -Toggling auto **off** frees the slot immediately. Human takeover of a guild-auto table (between rounds) does the same. +Human takeover changes who deals; guild ownership and its slot remain. ## House float (any backed table) @@ -49,11 +54,11 @@ No "please top up" wait. Cover is withdraw-or-refuse. 1. During the bet window, after a legal chip-in, `need = max(0, action - tray)`. Only auto tables run a bet window, so on a human-dealt table the round reserve at step 5 is the only cover step. 2. If `need == 0`, do nothing. 3. Staff auto: spawn `need` (today). -4. Guild auto: if bank wealth `< need` or bankrupt / no bank, **do not accept that chip-in** (restore item, message). Else `withdraw(need)`, spawn on tray. +4. Guild auto: if bank wealth `< need` or bankrupt / no bank, **do not accept that chip-in** (restore item, message). Else post the guild debit through Denars, then materialize the planned chips on the tray. 5. When bets close, blackjack reserves the round's worst case: `sum(boxStake) * max-hands-per-box * 2`, rounded up to a whole coin. Withdraw-or-refuse again, but at round scope: if the bank cannot supply it the round does not start and every box is refunded. Splits and doubles inside the round then take their cover from the reserve and never touch the bank. `closeBets` is the gate for both modes: the bet timer calls it on auto tables, and a human dealer reaches it with `/games bet close`, which is also the only way their round starts. 6. Player wins peel the tray. The settle shortfall path still exists but the reserve should make it unreachable, so it logs a mismatch when `wager.audit-log` is on. 7. Round end (settled or abandoned): the whole tray `deposit`s back to the owning guild bank, so the float is zero between rounds and house wins are banked each round rather than piling up as chips. Staff mint burns it. A table with no guild returns the float to its dealer instead. -8. Pickup / teardown: any remaining tray on a backed table `deposit`s to the owning guild bank; if that guild is gone, drop the tray items. Staff mint tray is despawned, not dropped. Player pots still drop. +8. Pickup / teardown: any remaining tray on a backed table `deposit`s to the owning guild bank; if the guild or posting is unavailable, retain the table and tray for review. Staff mint tray is despawned, not dropped. Player pots still drop. A guild running several tables shares one bank, so reserving per round is also what stops two tables from both counting the same denars. @@ -73,7 +78,7 @@ Right-click shoe (existing claim path): - Already this player: **unset** dealer. - Empty seat, human table: **set** if the player is in `ownerGuildId` (or staff). No guild on the table: placer / staff only. -- Guild auto, between rounds: **takeover** - become human dealer, `autoDealer=false`, tray stays, slot freed. +- Guild auto, between rounds: **takeover** - become human dealer, `autoDealer=false`, tray and guild ownership stay. - Staff auto: guild members **cannot** takeover. Staff can turn mint off from options (when not live) or pick up the table. Leave-distance still does not clear dealer. Logout / pickup still does (existing). @@ -101,6 +106,6 @@ Default `SHOE`. Engine stays dumb; blackjack asks the table for policy. ## Isolation -One Games class (same idea as `RpNames`): plugin present + enabled, then `FactionManager` / `Guild` / `Bank`. Catch `LinkageError`. Do not scatter SF types through `BlackjackGame`. +Guild access checks plugin presence, enabled state and `SimpleFactionsApi.ready()` before character membership and incarnation lookups. Errors fail closed. Denars owns money posting; SimpleFactions only reads identity/modifiers and records idempotent casino profit. New rounds wait for `Denars.taxReady()`. SimpleFactions integration: `GuildModifier` entry + `upgrades.yml` row. No Games dependency in SF `pom` / `plugin.yml`. diff --git a/projects/Games/docs/HOLDEM.md b/projects/Games/docs/HOLDEM.md index 8daeb13..5491b83 100644 --- a/projects/Games/docs/HOLDEM.md +++ b/projects/Games/docs/HOLDEM.md @@ -73,4 +73,4 @@ Play: - Each hand collects the ante from every seat as dead money, then posts the blinds. With a blind interval above `0`, blinds double every interval after the first hand, and each new level is collected from the next hand dealt; `0` keeps them fixed. - `call` takes the chips needed to match automatically; a short stack goes all in. Players with no chips sit out until they rebuy. - Leaving or being kicked before the first hand refunds the buy-in. After play starts it forfeits the entry. -- When Games is disabled (server stop or plugin reload), tables reset to idle: tournaments end, Denar stakes still on the table return to their owners through the table's refund path (dropped at the table for offline owners), and chip stacks are cleared. Tournament settings persist. +- When Games is disabled (server stop or plugin reload), settled tables reset to idle: tournaments end, Denar stakes still on the table return to their owners through the table's refund path (dropped at the table for offline owners), and chip stacks are cleared. Tournament settings persist. Tables with unresolved identity or money operations remain frozen for review; see [wager recovery](WAGER_ENGINE.md#character-ownership-and-recovery). diff --git a/projects/Games/docs/SYSTEM.md b/projects/Games/docs/SYSTEM.md index f58d5b7..9ab9b47 100644 --- a/projects/Games/docs/SYSTEM.md +++ b/projects/Games/docs/SYSTEM.md @@ -2,7 +2,7 @@ Tabletop plugin. Engines are dumb: they cannot tell poker from blackjack. Games only call engines. -Games provides blackjack, [Tenceur Hold'em](HOLDEM.md) (`poker`) and [Five-Draw](FIVEDRAW.md) (`draw`). Tables persist locations and house settings; stopping or restarting the plugin resets sessions to idle. +Games provides blackjack, [Tenceur Hold'em](HOLDEM.md) (`poker`) and [Five-Draw](FIVEDRAW.md) (`draw`). Tables persist locations and house settings; stopping or restarting resets settled sessions to idle; unresolved money operations retain frozen tables for review. ## Config folder layout @@ -106,9 +106,9 @@ Ace file and IA id is `_1` or `1`, not 14. Ace-high ranking is a poker flag, not ## Dependencies -`TLibs`, `ItemsAdder`, `ProtocolLib`. DenarEconomy softdepend (wager). RPCharacters softdepend (character names and voice). SimpleFactions softdepend (guild auto cap + bank; [GUILD_TABLES.md](GUILD_TABLES.md)). SF does not depend on Games. +`TLibs`, `ItemsAdder`, `ProtocolLib` and `DenarEconomy` are required. RPCharacters softdepend (character names and voice). SimpleFactions softdepend (guild auto cap + bank; [GUILD_TABLES.md](GUILD_TABLES.md)). SF does not depend on Games. -The build pins TLibs **2.3.0** and RPCharacters **2.16.0**. Names use the shared +The build pins TLibs **2.4.0**, DenarEconomy **0.3.0**, SimpleFactions **4.0.0** and RPCharacters **2.16.0**. Names use the shared character identity service; voice still uses RPCharacters' chat API. Deploy the pinned providers with Games and restart to activate them. diff --git a/projects/Games/docs/TEST_MATRIX.md b/projects/Games/docs/TEST_MATRIX.md index 16f0231..d4e083d 100644 --- a/projects/Games/docs/TEST_MATRIX.md +++ b/projects/Games/docs/TEST_MATRIX.md @@ -110,7 +110,7 @@ See [BLACKJACK.md](BLACKJACK.md). - [ ] 3:2 natural, double, split, DAS - [ ] Auto: 10s after first min bet, spawn pays - [ ] Voice via RPCharacters `dispatch`, not `/rp` command -- [ ] With TLibs 2.3.0 and RPCharacters 2.16.0, table labels and game messages show the current mask/alias; changing it or switching characters updates the next rendered name without revealing a canonical name +- [ ] With TLibs 2.4.0 and RPCharacters 2.16.0, table labels and game messages show the current mask/alias; changing it or switching characters updates the next rendered name without revealing a canonical name - [ ] An online player with no active character shows the RPCharacters no-character label (`Unknown` by default), matching chat - [ ] Without the character provider, online labels use account names; offline names use the last known account name or `-`; gameplay and wagers still work @@ -126,3 +126,14 @@ See [GUILD_TABLES.md](GUILD_TABLES.md). - [ ] Guild cover withdraws shortfall only; empty bank restores the chip; pickup deposits tray - [ ] Dealer: guild member set/unset; no mid-round; takeover auto frees slot; no takeover of staff mint - [ ] Shuffle `ROUND` vs `SHOE` matches policy after a full round + +## Ledger and identity failure checks + +- [ ] While installed SimpleFactions is waiting, new rounds and guild migration stay closed; readiness permits them without restarting Games. +- [ ] Switching characters on one account cannot collect or edit the other character's guild table/stakes; a later settled round can bind the new character. +- [ ] A deleted guild's namesake cannot claim the old table or bank, including after restarting migration. +- [ ] A bank-funded win and its actual withheld chip tax produce one balanced Denars posting; refused posting changes no items, float or casino profit. +- [ ] After a posting, interrupt table save or profit bookkeeping; restart retains a frozen table and intent without delivering money/items again. +- [ ] Missing winning character, mid-round tax failure and partial payout retain the remaining obligation and prevent round clear/pickup. + +Run automated checks with `tfmc-mvn -o -B --no-transfer-progress clean verify`. Journal tests inject posting, storage and bookkeeping failures; they do not prove atomic Minecraft inventory persistence or replace server integration checks. diff --git a/projects/Games/docs/WAGER_ENGINE.md b/projects/Games/docs/WAGER_ENGINE.md index 865aa83..5eefebe 100644 --- a/projects/Games/docs/WAGER_ENGINE.md +++ b/projects/Games/docs/WAGER_ENGINE.md @@ -10,11 +10,11 @@ A `MoneyAccount` is a place money can sit. All of them answer the same questions |---|---|---| | `PlayerAccount` | Coins in an inventory | Filtered, so a blackjack box only takes real money coins. Falls back to dropping at the table when the player is gone, so a payout is never deleted | | `BucketAccount` | One owner's stakes in the `TableLedger` | The tray is the bucket keyed by the table's own id | -| `BankAccount` | The owning guild bank | The only class in games that reaches SimpleFactions money, via the package-private `GuildBank` | +| `BankAccount` | Captured guild incarnation account | Reads Denars balance; mutations are legs of the transaction posting | | `MintAccount` | Nothing, creates money | Refuses on anything but a staff-mint table | -| `TaxSink` | Nothing, destroys chips | Withheld citizen tax on coin winnings. Not on the felt | +| `TaxSink` | Withheld chips | Credits their actual whole-chip value to the captured tax account in the same posting | -Build them through `Accounts`: `bucket`, `tray`, `pockets`, `coins`, `declared`, `payee`, `ground`, `bank`, `mint`, `taxSink`, and `house`, which picks mint or bank depending on the table. +Build them through `Accounts`: `bucket`, `tray`, `pockets`, `coins`, `declared`, `payee`, `ground`, `bank`, `mint`, and `house`, which picks mint or bank depending on the table. ## A transaction @@ -35,14 +35,13 @@ Legs come in four shapes: - `moveAll` and `moveStreet` - everything a bucket holds, or everything it holds from one betting street. - `spread` - one amount covered by several sources in turn, which is how a pot pays out of the buckets that built it. -`commit()` runs with no yield point in the middle: +`commit()` plans sources on detached copies, reserving each take before planning the next. Reuse one source handle for multiple legs; distinct handles naming the same source are refused. Destinations confirm acceptance before anything moves. -1. **Plan.** Every source works out which coins it would hand over, without moving any of them. A source that cannot make the amount exactly fails the leg. -2. **Accept.** Destinations confirm they can take the planned value. Buckets fed from the bank or the mint derive their value from what the coin template can represent, so the bank leg and the stake leg always agree and no compensating deposit is ever needed. -3. **Apply.** One synchronous pass: take, accept, log. -4. **Audit.** `LedgerAudit.check` re-asserts the invariants, and the felt total is compared against what the legs said it should move. +For guild-bank or tax legs, the transaction writes a flushed, atomic intent under `Data/wager-intents/.json` before one balanced `Denars.post`. The `games::` operation records its legs, item templates, destinations, captured tax quote and expected float/profit. Physical chips exchanged with a guild bank use the Denars mint account as the other ledger leg. Bank-funded tax directly debits the guild and credits the tax destination; it never mints the gross win into a character pouch. -Reserve then apply, never apply then undo. A caller that gets a failure knows nothing moved and can simply refuse its action. +Only `APPLIED` authorizes the planned item and float changes. The engine rechecks physical inventory and character snapshots, applies the changes, saves the table durably, then records casino profit with the same operation ID. The journal is removed only after all steps succeed. A repeated successful `commit()` returns the previous result without delivering again. An unchanged prepared intent can retry in the same process using its original operation ID. + +A known tax-readiness wait creates no payment intent. Refused postings leave items, float and profit untouched. A failure after posting may already have moved value; the retained journal freezes the table for staff review. Callers must not compensate or repeat delivery based solely on a failed result. See the recovery procedure below. `TxResult.reason()` names why a refusal happened - `PLAYER_SHORT`, `NO_CHANGE`, `FELT_SHORT`, `BANK_SHORT`, `NO_TEMPLATE`, `MINT_REFUSED`, `NO_TABLE` - and `messageKey()` maps it to `messages.yml`, so the player is told the real problem rather than a guess. @@ -56,19 +55,36 @@ A `Stake` carries the spot it was put down on, so a player's chips sit where the `BankAccount` is the only door between a guild bank and a table, so it is also where profit is worked out. Money going out raises the table's `houseFloat`; money coming in settles that float first, and only what is left over is profit. A guild pays tax on the profit, never on a float it took out and handed straight back. -Nothing is moved for tax. Winnings are already in the bank the moment `settleAutoTray` deposits the tray, so `BankAccount.accept` only reports the profit onward, through `GuildBank.declareProfit` into the owning guild's SimpleFactions ledger as its `GAMBLING` cashflow. That entry is gross counted, so it widens guild tax, overlord tax, tribute and reparations, and it settles as a ledger line with no delta of its own. Adding a delta would pay the guild for the same denars twice. +Casino-profit bookkeeping moves no extra money. Winnings enter the guild account in the transaction posting. After saving the resulting table, `GuildBank.declareProfit` reports only the surplus into the owning guild's SimpleFactions ledger as its `GAMBLING` cashflow, using `addCasinoProfitEntry(operationId, amount)`. Exact retries do not count profit twice, even after a tax settlement. That entry is gross counted, so it widens guild tax, overlord tax, tribute and reparations, and it settles as a ledger line with no delta of its own. Adding a delta would pay the guild for the same denars twice. A six-box table at 1000 reserves 48000. If players net lose 1500 the tray banks 49500, the float is cleared and 1500 is declared. If players net win 2000 only 46000 comes back, the float stays 2000, nothing is declared, and the table's next 2000 of winnings is sheltered until it is level again. So a losing streak carries forward by itself, and a bank covering a winner directly leaves the float raised for the same reason: the house is down until it earns that back. -`houseFloat` lives on `Table` and is saved with it, because every commit ends in `WagerHost.moneyMoved`. It is zeroed if a table changes owning guild, since a float belongs to whoever put it up. Staff mint tables never reach `BankAccount` at all, so they declare nothing, and a private table with a human dealer returns its float to the dealer rather than a bank and is likewise untaxed. +`houseFloat` lives on `Table` and is saved with it, with a checked durable save before its money intent can complete. It is zeroed if a table changes owning guild, since a float belongs to whoever put it up. Staff mint tables never reach `BankAccount` at all, so they declare nothing, and a private table with a human dealer returns its float to the dealer rather than a bank and is likewise untaxed. ## Player coin profit this round -`RoundMoney` on each `Table` tracks coin `moneyIn` and `moneyOut` per player for the current session. `MoneyTx` updates it on every successful leg: pockets to the felt raise `moneyIn`, any payout to a `PlayerAccount` raises `moneyOut` (tray, bank, pot, or a dealer covering from pockets). Loot stakes are ignored via `ChipItems.moneyValue`. `moneyProfit` is `max(0, moneyOut - moneyIn)`. Maps are cleared in `clearSession` (end of hand, idle reset) and are not saved with the table file. +`RoundMoney` on each `Table` tracks coin `moneyIn` and `moneyOut` per player for the current session. `MoneyTx` updates it on every successful leg: pockets to the felt raise `moneyIn`, any payout to a `PlayerAccount` raises `moneyOut` (tray, bank, pot, or a dealer covering from pockets). Loot stakes are ignored via `ChipItems.moneyValue`. `moneyProfit` is `max(0, moneyOut - moneyIn)`. Maps are cleared at settled session end and are not saved as a resumable game. Unpaid outcomes and interrupted postings retain separate durable records and prevent automatic reset. + +Blackjack settle calls `WagerEngine.payWin`, which quotes citizen tax on the `pay` map profit for the captured wager character, and sends DenarEconomy's `money.tax` line. + +Hold'em and Five-Draw pot payouts include the winner's own stake. `RoundMoney.taxableProfit(owner, payout)` subtracts stake still returning (`moneyIn - moneyOut`) before tax applies. `WagerEngine.payFromPot` and `sweepPot` spread net coins to the payee and tax to `TaxSink` from the same `potAccounts` in one transaction, then `sweepPot` transfers remaining loot. A failed residual transfer is retained for review. `returnStakes` stays untaxed. `coinPot` sums coin denars across pot buckets for sweep tax math. + +## Character ownership and recovery + +A table stores `wagerCharacters`, mapping account UUIDs to the character captured when money enters play or the session begins. Tax quotes use that immutable character reference, including offline winners. A missing reference or another active character on the same account cannot collect a payout or refund. A settled round clears bindings when no player stakes remain, even if the house tray is nonempty; the next occupant can then bind their own character. + +Citizen tax is a pure `TaxPolicy` quote. Whole-chip rounding and source priority remain unchanged: net payout first, then whatever tax chips the sources can actually supply. The credited tax is exactly the chip value destroyed, in cents, even when the quote rounds or the payout is partial. Partial payments retain the paid amount, actual withholding and remaining obligation separately; they never retry the full gross amount automatically. + +Interrupted intents, unresolved identity/tax policy, partial winnings and failed guild tray settlements freeze further transfers, round clearing, editing and pickup. On restart the table and journal remain for review. Legacy saved player stakes without a recorded character also require review; loading never guesses from the account's current or last-played character. + +Staff recovery is manual because Minecraft inventory delivery and the money ledger are not one atomic store: -Blackjack settle calls `WagerEngine.payWin`, which withholds citizen tax on the `pay` map profit to `TaxSink` before the net reaches the player, and sends DenarEconomy's `money.tax` line. +1. Preserve the table JSON, `Data/wager-intents/.json` and any guild migration backup before investigating. +2. Inspect the operation ID, ledger legs, encoded items, captured character/tax destination, float before/after and `unpaidOutcomes`. The latter are observations of failed or partial settlement attempts; repeated observations must not be summed as independent debts. +3. Check Denars status for the recorded operation and compare the saved table, inventories and casino-profit bookkeeping. `prepared` does not prove no posting occurred; `applying` does not prove delivery finished. +4. Reconcile the verified remaining obligation once through a reviewed ledger operation and item correction. Never recalculate the original tax against today's character or guild, replay gross winnings, or remove the journal merely to unfreeze a table. Keep the evidence with the correction. -Hold'em and Five-Draw pot payouts include the winner's own stake. `RoundMoney.taxableProfit(owner, payout)` subtracts stake still returning (`moneyIn - moneyOut`) before tax applies. `WagerEngine.payFromPot` and `sweepPot` spread net coins to the payee and tax to `TaxSink` from `potAccounts` in two commits, then `sweepPot` `moveAll`s loot and any leftover stakes untaxed. `returnStakes` stays untaxed. `coinPot` sums coin denars across pot buckets for sweep tax math. +Physical item escrow and automatic recovery of ambiguous item delivery are outside this system. There is no automatic journal replay on startup. ## Coin selection is exact, not greedy