Skip to content

09 — Save

Goal

Learn what a currency save is actually claiming, and meet the duplication exploit that a naive restore leaves wide open.


What This Scene Demonstrates

The participant flow:

Owners with wallets → configured currency ids plus whatever each wallet actually holds → one labelled section → coordinator → save payload

CurrencySaveParticipant adapts CurrencyPersistence to the coordinator. Unlike the Health participant it takes dependencies — a service and a list of currency ids — which means you construct it deliberately rather than something reflective finding it. That is the intended shape: a participant is something your game assembles and hands over.


The Exploit This Scene Exists For

A restore is meant to be the whole truth about money. Here is how a reasonable implementation quietly is not.

A capture skips owners with no wallet — correctly, because writing zero lines for every door and pickup in a scene would create the wallets they never had. So:

  1. Fund the Player. Capture.
  2. Pay the Companionafter the save was taken.
  3. Restore.

The companion had no wallet when the save was written, so the save has no entry for it, so an apply-onto-existing-state restore never touches it. The player's balance is restored and the companion keeps the 500. Repeat for as much money as you like.

ICurrencyWalletQuery answers per owner, so a game cannot close this for itself: there is no way to enumerate the wallets a load ought to be zeroing.

UnsavedWalletPolicy.Zero is therefore the default, and it is the one place this participant breaks the framework's apply-onto-existing-state rule. That rule is right when the alternative is losing data. Here the alternative is minting it, and a duplication exploit that no report mentions is worse than a load that clears more than you expected.

Toggle the policy to Leave and run the same three steps to watch the exploit work. That is the point of it being runnable rather than described.


The Same Hole, One Level Down

Before owners, there was the same problem at currency level: relying on the configured list alone meant a currency you forgot to list was neither saved nor set by a load, so its balance survived a quickload and the player kept whatever they had spent it on.

So a capture now writes the configured list plus whatever each wallet actually holds, asked through ICurrencyWalletQuery.TryGetHeldCurrencies. The list still earns its place: a currency the wallet has never touched has no key to enumerate, and a line at zero for it is what lets a restore put that wallet back to zero.

Section 2 of the panel shows both — the configured set, and what each owner is actually holding.


What To Look For

  • The Wallets In The Scene shows live balances for both owners across every configured currency
  • The Currency List Is A Floor shows the configured ids next to what each wallet actually holds. A service that cannot enumerate falls back to the list alone and says so once per restore, rather than doing nothing quietly
  • Capture — do it with only the player funded. That is what makes section 4 work
  • The Wallet The Save Never Heard Of is the policy toggle and the restore. Read the companion's balance in section 1 afterwards
  • Under Leave, the restore still warns and names the owners it left, because a balance it is preserving is indistinguishable from one arriving out of nowhere

Scene Setup

  • SceneCurrencyService, CurrencyServiceBootstrap, CurrencyAuthorityBinder and a CurrencySet of two currencies, all kept from the base scene
  • Three owners already carrying StableIds — Player, OwnerA, OwnerB. Player and OwnerA are the panel's Player and Companion
  • CurrencySavePanel in place of CurrencyPersistenceAuditPanel
  • CurrencyBasicsPanel kept, as an independent readout and a way to move money without the save panel

OwnerB is not bound to anything, and that is useful. Give it currency and restore under Zero and it is swept too — because nothing distinguishes an object funded after the save from one spawned since it. That object should not have survived the load either; the save system does not destroy objects, so zeroing is the half of the problem it can reach.

The service must be able to enumerate for any of this to work. SceneCurrencyService implements ICurrencyWalletQuery directly, and CurrencyServiceDecorator forwards it to whatever it wraps, so the composed chain from the bootstrap answers correctly. A service that cannot enumerate degrades to the configured list and cannot sweep at all.


Sample Scope

This scene focuses only on:

  • Capturing and restoring balances across owners
  • The configured currency list as a floor rather than the whole answer
  • The owner-level duplication exploit, and the policy that closes it
  • Both sides of that policy, runnable

This scene does NOT cover:

  • Writing the payload to disk — nothing here does any file I/O, on purpose
  • Escrow holds, which deliberately do not survive a save
  • Audit trails — that is 03 • Persistence
  • Save slots, versioning beyond the participant's own guard, or networking

Authority Note

This scene may include a permissive sample authority setup. If no authority is resolved, mutations are allowed for demonstration purposes.


Networking Reminder

No networking is included. Multiplayer authority and synchronization are the developer's responsibility.


How To Use

  1. Press Fund Player 1000
  2. Press Capture. The companion has no wallet at this moment, so the save has no entry for it
  3. Press Pay Companion 500
  4. Press Restore, with the policy on Zero. The companion is zeroed — read section 1
  5. Toggle to Leave. Pay the companion again, restore again. It keeps the money
  6. Read the result: under Leave the report names the owners it left alone

Steps 3 and 5 in that order are the entire lesson. Capturing after paying the companion shows nothing, because then the save knows about it.


Failure Behaviour

Failures are reported through RevSaveReport rather than thrown.

Common patterns:

  • Section 2 warns that the service cannot enumerate → it does not implement ICurrencyWalletQuery → A capture falls back to the configured list alone, and a restore cannot sweep unsaved wallets at all. It says so once per restore rather than doing nothing quietly

  • The companion keeps its money under Zero → it was funded before the capture, so the save has an entry for it → Fund the player, capture, then pay the companion

  • Nothing was credited → a cap policy, an authority, or a minimum-balance rule refused it → Not a save concern; the panel reports it so you can tell it apart from a save problem

  • A wallet you expected to keep was cleared → working as designed under ZeroLeave is the opt-out, for a persistent world or a shop economy that is not part of the player's save


Behind The Scenes

This panel uses public save and currency APIs only:

  • CurrencySaveParticipant / UnsavedWalletPolicy
  • RevSaveCoordinator.Capture(...) / RevSaveCoordinator.Restore(...)
  • RevSaveReport
  • ICurrencyService.Credit(...) / GetBalance(...) / EnsureWallet(...)
  • ICurrencyWalletQuery.HasWallet(...) / TryGetHeldCurrencies(...)
  • CurrencySet.ToIds() / CurrencySetLocator
  • CurrencyResolve.ServiceFrom(...)
  • StableId

Key Takeaway

Saving money is not "write the balances down and put them back". It is a claim about every wallet in the scene, and the moment a capture is allowed to skip one, a restore has to decide what that silence meant.

The framework's answer is that silence means zero, because the alternative is a duplication exploit no report would ever mention. If your game genuinely has wallets that outlive a load, Leave is there — and it will warn you about every one of them, every time, on purpose.


  • 03 • Persistence — this scene was built from it. That one is the layer below: CurrencyPersistence driven directly, with the audit trail.