Skip to content

💰 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_PRESENT is defined.
  • Decorator-composed --- behaviour is layered explicitly via factories.

Everything revolves around one contract:

ICurrencyService is 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:

  1. SceneCurrencyService (baseline in-memory wallet)
  2. (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);
  1. Caps\
  2. Audit\
  3. Authority\
  4. RequireEscrow (guard when policy demands it)\
  5. Idempotency\
  6. 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:

  1. Published override (via CurrencyBootstrap.Publish)
  2. 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)
  • CurrencyBarTMP
  • CurrencyBarUITK

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 handle
  • TransferPolicyProviders --- stable ITransferPolicyProvider builders

🗂 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.