💰 Currency System¶
Currency is a modular, service-driven wallet system for managing balances such as gold, gems, tickets, energy, etc.
It is:
- Wallet-agnostic --- you can swap the underlying wallet implementation.
- Network-agnostic --- authority and idempotency are optional layers.
- UI-agnostic at its core --- the runtime service has no UI dependency.
- Inventory-agnostic at its core --- the Inventory adapter
compiles only when
REV_INVENTORY_PRESENTis defined. - Decorator-composed --- behaviour is layered explicitly via factories.
Everything revolves around one contract:
ICurrencyServiceis the currency system.\ Everything else is optional composition layered on top.
🧠 System Philosophy¶
Currency is:
- Deterministic
- Explicit
- Composition-first
- Service-driven
- Honest about its guarantees
It is not:
- A networking framework
- A replication layer
- A transactional ACID database
- A hidden magic global
📦 What's in this system¶
Concept What it is
ICurrencyService Core wallet operations
(credit/debit/set/transfer +
events).
CurrencyId Canonical id ("gold", "gems")
normalized lowercase + trimmed.
Money long minor units (no floats),
overflow-checked operators.
Decorators Optional layers composed via
CurrencyFactories (Caps, Audit,
Authority, Escrow, RequireEscrow,
Idempotency, BatchEvents).
Bootstrap Scene helper that composes a recommended stack and publishes it for resolution.
Persistence (optional) Snapshot + JSON save/load helpers.
Exchange (optional) Table-based currency exchange engine.
UI (optional) UGUI / TMP / UITK currency bars.
Policies (optional) CurrencyPolicy authoring (caps +
RequireEscrow) with a stable
runtime query surface.
Definitions (optional) CurrencyDefinition/Set metadata + formatting helpers.
Public API Small, stable helper surfaces (batching handle, transfer policy providers).
UX Shared reason-text mapping for UI and tooling.
🚀 Quickstart¶
1) Add to your scene¶
Minimum working setup:
SceneCurrencyService(baseline in-memory wallet)- (Recommended)
CurrencyServiceBootstrap(composes and publishes an outer service)
2) Use from gameplay code¶
var svc = CurrencyResolve.ServiceFrom(this);
var gold = new CurrencyId("gold");
var result = svc.Debit(player, gold, 50);
if (!result.Success)
Debug.LogWarning($"Debit failed: {result.Code} ({result.Message})");
3) Listen for changes¶
svc.OnWalletChanged += d =>
{
Debug.Log($"{d.owner.name} {d.currency} changed: {d.before.amount} -> {d.after.amount}");
};
🧩 Core Contract: ICurrencyService¶
bool EnsureWallet(GameObject owner);
Money GetBalance(GameObject owner, CurrencyId currency);
CurOpResult Credit(GameObject owner, CurrencyId currency, Money amount);
CurOpResult Debit(GameObject owner, CurrencyId currency, Money amount);
CurOpResult SetBalance(GameObject owner, CurrencyId currency, Money newBalance);
CurOpResult Transfer(GameObject from, GameObject to, CurrencyId currency, Money amount);
event Action<CurrencyDelta> OnWalletChanged;
Guarantees¶
- All operations are synchronous.
- Each successful mutation emits
OnWalletChanged. - Single operations are atomic.
- Multi-operation atomicity is not guaranteed unless explicitly orchestrated (e.g., via escrow-backed flows).
🧱 Composition & Decorators¶
You do not instantiate decorator types directly.\
Use CurrencyFactories to compose behaviour safely.
Example:
svc = CurrencyFactories.WithCapsThenAudit(svc, policy);
svc = CurrencyFactories.WithAuthority(svc, this);
svc = CurrencyFactories.WithIdempotency(svc);
svc = CurrencyFactories.WithBatchEvents(svc);
Recommended Ordering¶
- Caps\
- Audit\
- Authority\
- RequireEscrow (guard when policy demands it)\
- Idempotency\
- BatchEvents
Ordering matters.
🔐 RequireEscrow vs Escrow¶
-
RequireEscrow is a guard.\ When
CurrencyPolicy.RequireEscrow == true, debits/transfers fail unless escrow is present. -
Escrow is the capability.\ Adds
ICurrencyEscrow(TryHold,Commit,Release,ExpireStale).
Policies do not add escrow automatically --- they only enforce its presence.
🔍 Service Resolution¶
var svc = CurrencyResolve.ServiceFrom(this);
Resolution order:
- Published override (via
CurrencyBootstrap.Publish) - Scene search for
SceneCurrencyService(cached)
If a bootstrap republishes an override (including in additive scenes), UI and callers will rebind automatically.
🧰 Bootstrap¶
CurrencyServiceBootstrap:
- Finds the inner
SceneCurrencyService - Composes a recommended stack
- Publishes it via
CurrencyBootstrap.Publish - Disposes cleanly on disable/destroy
In single-player, add CurrencyAuthorityBinder (or your own authority
implementation).
💾 Persistence (Optional)¶
Snapshots operate on absolute balances.
var snap = CurrencyPersistence.Capture(svc, player, ids);
CurrencyPersistence.Restore(svc, player, snap, "LoadWallets");
Restore:
- Uses batching
- Rolls back best-effort on failure
- Propagates audit metadata when supported
- Obeys caps, authority, idempotency, and RequireEscrow guards
💱 Exchange (Optional)¶
var ex = CurrencyFactories.BuildExchange(table);
if (ex.TryQuote(src, dst, 100, out var amount))
Debug.Log(amount);
ex.TryExchange(svc, player, src, dst, 100, "Exchange");
Exchange delegates all mutation to the composed ICurrencyService.\
Atomic swap is not guaranteed.
🖥️ UI (Optional)¶
CurrencyBar(UGUI)CurrencyBarTMPCurrencyBarUITK
Event-driven updates with safe auto-rebinding.
Owner fallback to "Player" tag is single-player convenience only
--- set explicitly for multiplayer setups.
🧰 Public Helpers¶
CurrencyBatching--- safe multi-op batching handleTransferPolicyProviders--- stableITransferPolicyProviderbuilders
🗂 Folder Overview¶
Abstractions/-- public contracts & primitives\Core/-- factories, resolve helpers, transactions\Internal/-- decorator implementations\Bootstrap/-- scene composition\Authority/-- authority seam & binder\Policies/-- cap rules & escrow requirement\Definitions/-- metadata & formatting\Exchange/-- optional conversion engine\Persistence/-- save/load helpers\UI/-- optional runtime UI\PublicAPI/-- stable wrappers\EditorApi/-- editor-only helpers\Adapters/-- optional bridges\UX/-- reason-text helpers
✅ Summary¶
Currency gives you a stable ICurrencyService contract with optional
layered behaviour:
- Caps\
- Audit\
- Authority\
- Escrow\
- RequireEscrow guard\
- Idempotency\
- Batch events\
- Persistence\
- Exchange\
- UI & UX helpers
You choose the stack.\ The contract stays stable.