Economy

Upgrades

Coin-priced, persistent, server-authoritative meta-upgrades: price from the buyer's stored tier (ceil(InitialPrice*Growth^tier)), atomic currency debit, tier persisted per-character. Currency + persistence via seams bridged to Economy + PlayerData.

Server
services.AddCrossplayUpgrades();
Unity package
com.crossplay.upgrades
Depends on
coreeconomyplayerdata

Seams you implement 2

Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayUpgrades(); and it wins.

IUpgradeTierStore game SPI

The persistence seam: read / write an owner's upgrade TIER by key. A Hosting bridge binds this to IPlayerDataService (per-character, durable). Absent a bridge the default InMemoryUpgradeTierStore keeps tiers in memory (volatile) — enough for tests / local dev.

  • int GetTier(long ownerId, string tierKey)

    The owner's current tier for tierKey (0 when never bought).

  • void SetTier(long ownerId, string tierKey, int tier)

    Persists the owner's new tier for tierKey.

IUpgradeWallet game SPI

The currency seam: debit amount of currency for an upgrade purchase, atomically. The Upgrades piece never references Economy — a Hosting bridge binds this to IEconomyService.TryDebit (RULE 13). Absent a bridge the default NullUpgradeWallet denies, so purchases fail cleanly.

  • bool TryDebit(long ownerId, ushort currencyId, long amount, string reason)

    Debits the price atomically; false when the owner can't afford it (nothing changes).

Services you call 1

Crossplay implements these. Resolve them from DI and call them from your own systems.

IUpgradeService

Server-authoritative upgrade purchases. The client sends only an upgrade id; this service owns the price (derived from the buyer's persisted tier), the atomic currency debit, and the tier bump — so a client can never forge a price or a tier. Currency + persistence come through the IUpgradeWallet / IUpgradeTierStore seams (a Hosting bridge binds them to Economy + PlayerData).

  • UpgradeStatusEntry[] StatusOf(ISession session)

    Snapshots the session owner's current tier + next price for every catalog upgrade — the read path the shop UI uses to bootstrap (initial open / reconnect) without a purchase. Empty when the session has no bound owner.

  • bool TryPurchase(ISession session, ushort upgradeId, out int newTier, out ushort reasonCode)

    Buys the NEXT tier of upgradeId for the session's owner: prices it from the stored tier, debits the currency atomically, and persists tier + 1. Returns true with the new tier on success; false with a UpgradeNotificationCodes reason otherwise (nothing changes).

Configuration 1

Every tunable lives in an options object — there are no magic numbers to hunt for.

UpgradeOptions

The upgrade catalog (upgrade id → UpgradeDef). Empty = the piece is inert.

  • Dictionary<ushort, UpgradeDef> Catalog { get; set; }

    Upgrade id → its definition.

Wire messages 4

The protocol this piece speaks. Ids are allocated per piece so they can never collide.

5601 UpgradePurchaseRequest

Client → server: buy the NEXT tier of an upgrade. The client sends only the upgrade id — the server owns the price (derived from the buyer's persisted tier), the currency, the debit, and the tier bump, so a client can never forge a price or a tier. The UpgradeId is a game-defined catalog key.

5602 UpgradePurchaseResult

Server → client: the outcome of an UpgradePurchaseRequest.

5603 UpgradesStatusRequest

Client → server: send me my current tier + next price for every upgrade — the shop UI bootstrap (initial open, or a re-open after reconnect, without making a purchase).

5604 UpgradesStatusResponse

Server → client: the per-upgrade tier/price snapshot (the whole catalog, not a delta).

Unity services you inject 1

Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.

IUpgradeClient

The Upgrades piece, client side: buy the next tier of a coin-priced meta-upgrade. The client sends only the upgrade id — the server owns the price (from the buyer's persisted tier), the currency debit, and the tier bump — so nothing here can be forged. The reply carries the new tier or a refusal reason code; the game reads the tier and applies whatever it means (the upgrade's effect is the game's).

  • UniTask<UpgradePurchaseResult> PurchaseAsync(ushort upgradeId, CancellationToken ct = default)

    Buys the next tier of upgradeId (refusals carry the reason code).