Add agent skill (SKILL.md + per-domain reference)
CI / check (push) Successful in 6s
CI / publish (push) Has been skipped

This commit is contained in:
edmand46
2026-08-29 11:46:25 +03:00
parent b8e226722d
commit 22805173eb
11 changed files with 565 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
---
name: rudder-unity-sdk
description: Use when working with the Rudder Unity SDK rudder.sdk in a Unity project — install, bootstrap, auth, remote config, storage, stores, inventory, leaderboards, quests, battle pass, scenarios, realtime.
---
# Rudder Unity SDK (`rudder.sdk`)
UPM package `rudder.sdk` (v0.4.0): LiveOps SDK for Unity. It wraps
`Rudder.Core.dll` (namespace `RudderSdk.Core`, the .NET SDK) with Unity
adapters in `RudderSdk.Unity`. Public API surface:
- `RudderSdk.Unity``Rudder` (MonoBehaviour bootstrap), `RudderConfiguration`,
`RudderState`, platform adapters.
- `RudderSdk.Core``RudderClient` and feature services (`Auth`, `Player`,
`RemoteConfig`, `Storage`, `ProjectStorage`, `Stores`, `Inventory`,
`Leaderboards`, `Quests`, `BattlePass`, `Scenario`, `Realtime`).
- `RudderSdk.Core.Models.*` — DTOs (`PlayerProfile`, `Wallet`, `Offer`,
`Store`, `StorageItem`, `RankEntry`, `Quest`, `Reward`, ...).
Full integration rules live in `Packages/rudder.sdk/AGENTS.md`; copy-paste
sample scenes live in `Packages/rudder.sdk/Samples~/`. Verify signatures
against those, never from memory.
## Install
Unity 6000.0+. Add the scoped registry and both dependencies to
`Packages/manifest.json` (`com.unity.nuget.newtonsoft-json` is required —
the package does not bundle Newtonsoft.Json):
```json
{
"scopedRegistries": [
{
"name": "Rudder",
"url": "https://hub.rudder.build/api/packages/rudder/npm/",
"scopes": ["rudder"]
}
],
"dependencies": {
"rudder.sdk": "0.4.0",
"com.unity.nuget.newtonsoft-json": "3.2.2"
}
}
```
## Bootstrap
1. Create the configuration asset: `Assets > Create > Rudder > Configuration`.
2. Set `ProjectKey` (required). Defaults: `BaseUrl` `https://api.rudder.build`,
`RealtimeUrl` `wss://api.rudder.build/api/realtime/ws`, `TimeoutSeconds` 10.
3. Add the `Rudder` component to a startup scene object and assign the asset.
`Rudder` is `[DefaultExecutionOrder(-1500), DisallowMultipleComponent]`,
uses `DontDestroyOnLoad`, and pumps `client.Update(Time.deltaTime)` every
frame — never call `client.Update` yourself.
4. Initialize and sign in:
```csharp
using RudderSdk.Unity;
var client = Rudder.Initialize(); // synchronous, idempotent
await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player");
```
`Rudder.Initialize()` throws if the component or configuration is missing.
`Rudder.State` (`NotInitialized`/`Initializing`/`Ready`/`Failed`),
`Rudder.LastError`, `Rudder.Client`, `Rudder.Ready` (Task) and
`Rudder.Initialized` (event) expose lifecycle. Details:
[reference/bootstrap.md](reference/bootstrap.md).
## Platform adapters
`RudderUnityClientFactory.Create(RudderUnityClientOptions)` wires Core
abstractions to Unity implementations; `Rudder.Initialize()` does this for
you with `PlayerPrefsUnityKeyValueStore` + `UnityDebugLoggerSink`:
| Core abstraction | Unity adapter | Notes |
| --- | --- | --- |
| `IRudderTransport` | `UnityTransportAdapter` over `UnityWebRequestExecutor` | `UnityWebRequest`, timeout from config |
| `ITokenStore` | `UnityTokenStoreAdapter` | tokens in PlayerPrefs |
| `IDeviceIdProvider` | `UnityDeviceIdProvider` | GUID in PlayerPrefs (`rudder_device_id`) — not `SystemInfo.deviceUniqueIdentifier` |
| `IClock` | `UnityClock` | `DateTimeOffset.UtcNow` |
| `IPlanStateStore` | `UnityPlanStateStore` | scenario runs persist in PlayerPrefs |
| `IRudderLogger` | `UnityLoggerAdapter``IUnityLoggerSink` | default `UnityDebugLoggerSink``Debug.Log*` |
| `IRealtimeTransportFactory` | `UnityRealtimeTransportFactory` | WebGL: `JsWebSocketAdapter` (jslib); else `NativeWebSocketAdapter` |
## Capabilities
All calls hang off `client` / `Rudder.Client`. Match the task to one row,
then read the reference file for exact signatures.
| Task | API | Reference |
| --- | --- | --- |
| Init, sign in, profile, wallets, errors | `Rudder.Initialize`, `client.AuthorizeWithDeviceAsync`, `Auth.*`, `Player.GetProfileAsync` | [reference/bootstrap.md](reference/bootstrap.md), [reference/authentication.md](reference/authentication.md) |
| Remote tunables | `RemoteConfig.LoadAsync`, `Get<T>(key, fallback)`, `GetAsync` | [reference/remote-config.md](reference/remote-config.md) |
| Player cloud save / project storage | `Storage.GetAsync`/`SaveAsync`/`DeleteAsync`, `ProjectStorage.*` | [reference/storage.md](reference/storage.md) |
| Shop + inventory | `Stores.ListAsync`/`GetAsync`/`PurchaseAsync`, `Inventory.GetAsync` | [reference/stores-inventory.md](reference/stores-inventory.md) |
| High scores | `Leaderboards.FindBySlug(slug)``SubmitAsync`/`ListAsync` | [reference/leaderboards.md](reference/leaderboards.md) |
| LiveOps scenarios | `Scenario.TriggerAsync`, `OnNotification`/`OnStoreOffer`/... | [reference/scenarios.md](reference/scenarios.md) |
| Global quests | `Quests.ListAsync`/`ClaimAsync`/`ReportProgressAsync` | [reference/quests.md](reference/quests.md) |
| Battle pass | `BattlePass.*` with scenario/node ids, `BattlePassSession` | [reference/battlepass.md](reference/battlepass.md) |
| Realtime websocket | `Realtime.ConnectAsync`/`DisconnectAsync` (relay not deployed) | [reference/realtime.md](reference/realtime.md) |
## Hard rules
- Call the SDK from a `MonoBehaviour` or the game's service object; no second
singleton next to `Rudder`, no wrapper layers, no consumer-local interfaces
over `RudderClient`.
- Use SDK model types directly; do not re-wrap `Offer`/`PlayerProfile`/
`RankEntry` in project DTOs unless asked.
- No `ConfigureAwait(false)` in game code — continuations must return to the
Unity synchronization context.
- Catch `RudderApiException` subclasses and surface `exception.Message`;
do not swallow errors. A 401 is retried once after a token refresh by the
client itself.
- Backend entities must exist: store slug, leaderboard slug, remote-config
key, scenario event. Missing data is a 404, not an SDK bug.
- Every feature call requires a matching dashboard entity; the API base is
`https://api.rudder.build`, dashboard `https://app.rudder.build/`.
@@ -0,0 +1,46 @@
# Authentication and player profile
Namespace `RudderSdk.Core`. Verified against the bundled `Rudder.Core.dll`
and `Samples~/Authentication/AuthenticationSample.cs`.
## Sign in
```csharp
var client = Rudder.Initialize();
await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player");
```
`RudderClient.AuthorizeWithDeviceAsync(string region, string language,
string nickname = null, CancellationToken cancellationToken = default)`
returns `LoginViaDeviceResponse` (`AccessToken`, `RefreshToken`). It performs
device login and then restores persisted scenario runs. Tokens are saved to
PlayerPrefs by the Unity adapter.
Lower-level alternative on `client.Auth` (login only, no scenario restore):
- `Task<LoginViaDeviceResponse> LoginWithDeviceAsync(string region, string language, string nickname = null, CancellationToken = default)`
- `Task<bool> RefreshAsync()` — explicit token refresh.
- `void Logout()` — clears tokens locally.
- `event Action<RudderAuthState> AuthStateChanged``RudderAuthState.SignedIn` / `SignedOut`. `SignedOut` also fires when a refresh fails.
Login uses the SDK device id from PlayerPrefs. Do not pass
`SystemInfo.deviceUniqueIdentifier`.
## Profile and wallets
```csharp
var profile = await client.Player.GetProfileAsync();
var nickname = profile.Player.Nickname;
foreach (var wallet in profile.Wallets)
Debug.Log($"{wallet.Currency}: {wallet.Balance}");
```
- `Task<PlayerProfile> PlayerService.GetProfileAsync(CancellationToken = default)`
- `PlayerProfile``Player Player`, `List<Wallet> Wallets`.
- `Player``Id`, `ProjectId`, `Nickname`, `Region`, `Language`,
`CreatedAt` (`DateTimeOffset?`).
- `Wallet``Currency` (string), `Balance` (`long?`).
Models are nullable since 0.4.0 — check fields before use. After any
purchase or reward grant, reload the profile to see new wallet balances;
there is no change callback.
@@ -0,0 +1,58 @@
# Battle pass
`client.BattlePass` (`BattlePassService`), verified against the bundled
`Rudder.Core.dll`. There is no sample scene for this feature.
Progress is tied to a scenario battle-pass node. Prefer `BattlePassSession`
delivered by `Scenario.OnBattlePass` — it binds `scenarioId`/`nodeId`/
`runId` for you. Direct `BattlePassService` calls need those ids manually.
## BattlePassSession (preferred)
```csharp
client.Scenario.OnBattlePass += async session =>
{
var progress = await session.GetProgressAsync();
await session.AddXpAsync("kills", 100);
var claim = await session.ClaimRewardAsync(level, BattlePassService.TrackFree);
};
```
- `Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken = default)`
- `Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken = default)`
- `Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken = default)`
- `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken = default)`
- `LevelUp()` / `LevelUpAsync()`, `MaxLevel()` / `MaxLevelAsync()`,
`Complete()` / `CompleteAsync()` — resolve the scenario node.
- `Get<T>(key, fallback)`, `Id`, `Context` like other sessions.
`BattlePassLevelSession` (from `OnBattlePassLevel`) has `Claim()` /
`ClaimAsync()` and a `Level` property.
## BattlePassService (direct)
- `Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioId, string nodeId, CancellationToken = default)`
- `Task<AddBattlePassXpResponse> AddXpAsync(AddBattlePassXpRequest request, CancellationToken = default)`
- `Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(ClaimBattlePassRewardRequest request, CancellationToken = default)`
- `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(PurchaseBattlePassPremiumRequest request, CancellationToken = default)`
- Track constants: `BattlePassService.TrackFree`, `BattlePassService.TrackPremium` (static fields).
Request models carry `ScenarioId`, `NodeId`, and (except progress)
`RunId`; `PurchaseBattlePassPremiumRequest` also takes `IdempotencyKey`.
## Response models
- `GetBattlePassProgressResponse``Level` (`int?`), `Xp` (`long?`),
`PremiumOwned` (`bool?`), `ClaimedTiers` (`List<ClaimedTier>`: `Level`,
`Track`).
- `AddBattlePassXpResponse``Xp` (`long?`), `Level` (`int?`),
`LeveledUp` (`bool?`), `MaxLevel` (`bool?`), `Plan` (`ExecutionPlan`).
- `ClaimBattlePassRewardResponse``Success` (`bool?`),
`AlreadyClaimed` (`bool?`), `Granted` (`List<Reward>`), `Error`.
- `PurchaseBattlePassPremiumResponse``Success` (`bool?`), `Error`,
`Plan` (`ExecutionPlan`).
Business failures come back in the response (`Success != true`, `Error`)
with codes from `RudderErrorCodes` (`LevelNotReached`,
`ObjectivesIncomplete`, `NodeNotActive`, `RunExpired`, ...); transport
failures throw `RudderApiException` subclasses.
@@ -0,0 +1,89 @@
# Bootstrap and lifecycle
Namespace `RudderSdk.Unity`. Verified against
`Packages/rudder.sdk/Runtime/Sources/Rudder.cs`,
`RudderConfiguration.cs` and the bundled `Rudder.Core.dll`.
## RudderConfiguration (ScriptableObject)
Create via `Assets > Create > Rudder > Configuration`.
| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `ProjectKey` | `string` | — | Required. Issued in the Rudder admin panel. Empty key logs a warning in `OnValidate`. |
| `BaseUrl` | `string` | `https://api.rudder.build` | API base URL. |
| `RealtimeUrl` | `string` | `wss://api.rudder.build/api/realtime/ws` | Realtime websocket URL. The relay is not deployed yet; leave as-is. |
| `TimeoutSeconds` | `int` (`[Min(1)]`) | `10` | HTTP request timeout. |
Keep the asset with the real key out of git (this repo uses the git-ignored
`Assets/LiveOpsLocal.asset` pattern).
## Rudder (MonoBehaviour)
`[DefaultExecutionOrder(-1500), DisallowMultipleComponent]`. Put it on a
startup scene object, assign the configuration asset. `Awake` keeps the first
instance (`DontDestroyOnLoad`) and destroys duplicates. `Update` pumps
`_client.Update(Time.deltaTime)` (scenarios and realtime need it — do not
call `client.Update` yourself). On quit/destroy it disconnects realtime.
Static API:
- `RudderState State``NotInitialized` / `Initializing` / `Ready` / `Failed`.
- `Exception LastError` — initialization error when `State` is `Failed`.
- `RudderClient Client` — the Core client; throws `InvalidOperationException`
until `State` is `Ready`.
- `Task<RudderClient> Ready` — completes with the client after `Initialize`,
faults on initialization error. Prefer `Initialize()` in game code.
- `event Action<RudderClient> Initialized` — attach point for add-on
packages; late subscribers must check `State` themselves.
- `RudderClient Initialize()` — synchronous. Reads the serialized
configuration, creates `RudderClient` via `RudderUnityClientFactory`.
Idempotent: returns the same client when already `Ready`; resets and
retries after `Failed`. Throws if the component is not in the scene or no
configuration is assigned. Does not create a GameObject.
Statics reset on Enter Play Mode without domain reload
(`RuntimeInitializeOnLoadMethod(SubsystemRegistration)`).
## Minimal startup
```csharp
using RudderSdk.Unity;
using UnityEngine;
public class Bootstrap : MonoBehaviour
{
private async void Start()
{
var client = Rudder.Initialize();
await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player");
}
}
```
`AuthorizeWithDeviceAsync` logs in with the SDK device id (a GUID persisted
in PlayerPrefs under `rudder_device_id`) and restores persisted scenario
runs. It is the first awaited call; afterwards use `client.*` /
`Rudder.Client.*`. Tokens persist in PlayerPrefs, so a second launch can call
it again (it refreshes the session). See
[authentication.md](authentication.md) for the Auth service.
## Errors
All API errors are `RudderApiException` subclasses from `RudderSdk.Core`:
- `RudderAuthException` — 401; refresh failed, session over
(`AuthStateChanged(SignedOut)` fires).
- `RudderNotFoundException` — 404.
- `RudderRateLimitException` — 429.
- `RudderNetworkException` — no response / timeout.
`RudderApiException` exposes `StatusCode` (int), `Code` (string),
`RequestId` (string). The client retries a 401 once after a single-flight
token refresh — game code does not implement that retry. Catch and surface
`exception.Message`; do not swallow errors.
Removed APIs (do not use): `Rudder.Initialize(configuration)`,
`Rudder.Auth`-style static service shortcuts, `client.Scenarios`,
`BuyAsync`, `LoginViaDeviceAsync`, `Scenario.RestoreAsync` (folded into
`AuthorizeWithDeviceAsync`).
@@ -0,0 +1,26 @@
# Leaderboards
`client.Leaderboards` (`LeaderboardsService`), verified against the bundled
`Rudder.Core.dll` and `Samples~/Leaderboards/LeaderboardsSample.cs`.
```csharp
var board = client.Leaderboards.FindBySlug("my-board");
await board.SubmitAsync(score);
var top = await board.ListAsync(10);
```
API:
- `LeaderboardHandle FindBySlug(string slug)` — no network call; returns a
handle bound to the slug.
- `LeaderboardHandle.SubmitAsync(double score, CancellationToken = default)`
- `LeaderboardHandle.ListAsync(int limit = 100, CancellationToken = default)`
`IReadOnlyList<RankEntry>`.
- `LeaderboardHandle.Slug { get; }`
`RankEntry``PlayerId`, `PlayerName`, `Rank` (`long?`), `Score`
(`double?`).
The leaderboard slug must exist in the dashboard, else the calls 404. For
scenario leaderboard nodes see `OnLeaderboard` in
[scenarios.md](scenarios.md).
@@ -0,0 +1,33 @@
# Quests (global)
`client.Quests` (`QuestsService`) — the player's global quest list, distinct
from scenario `OnQuest` nodes (`QuestSession`, see
[scenarios.md](scenarios.md)). Verified against the bundled
`Rudder.Core.dll`. There is no sample scene for this feature.
```csharp
var quests = await client.Quests.ListAsync();
var claim = await client.Quests.ClaimAsync(quest.Id);
var completedIds = await client.Quests.ReportProgressAsync("kills", 1);
```
API:
- `Task<IReadOnlyList<Quest>> ListAsync(CancellationToken = default)`
- `Task<ClaimQuestResponse> ClaimAsync(string questId, CancellationToken = default)`
- `Task<IReadOnlyList<string>> ReportProgressAsync(string metric, long amount, CancellationToken = default)` — returns the ids of quests completed by this report.
Models:
- `Quest``Id`, `Name`, `Status`, `Objectives`
(`List<QuestObjectiveProgress>`), `Rewards` (`List<Reward>`).
- `QuestObjectiveProgress``ObjectiveId`, `Metric`, `Current` (`long?`),
`Target` (`long?`), `Completed` (`bool?`).
- `Reward``Currency`, `ItemId`, `Amount` (`long?`).
- `ClaimQuestResponse``Success` (`bool?`), `AlreadyClaimed` (`bool?`),
`Granted` (`List<Reward>`), `Error`.
Store purchases already report metrics automatically; the metric strings are
built by `QuestMetrics.PurchaseOffer(offerId)``purchase.offer:<offerId>`
and `QuestMetrics.PurchaseItem(itemId)``purchase.item:<itemId>`
(`RudderSdk.Core.QuestMetrics`, static).
@@ -0,0 +1,36 @@
# Realtime
`client.Realtime` (`RealtimeService`), verified against the bundled
`Rudder.Core.dll` and
`Packages/rudder.sdk/Runtime/Sources/Realtime/`.
**The realtime relay is not deployed yet.** `ConnectAsync` is available, but
do not build product features on it unless the user explicitly asks.
```csharp
var session = await client.Realtime.ConnectAsync();
session.MessageReceived += data => { /* ArraySegment<byte> payload */ };
await session.SendAsync(payload);
await client.Realtime.DisconnectAsync();
```
API:
- `Task<RealtimeSession> ConnectAsync(TimeSpan? timeout = null, CancellationToken = default)` — connects to `RudderConfiguration.RealtimeUrl` (default `wss://api.rudder.build/api/realtime/ws`).
- `Task<RealtimeSession> ConnectAsync(Uri uri, TimeSpan? timeout = null, CancellationToken = default)` — explicit URI.
- `Task DisconnectAsync(CancellationToken = default)` — also called by the
`Rudder` component on quit/destroy.
- `RealtimeSession Session { get; }`, `bool IsConnected { get; }`.
- `void Update(float deltaTime)` — pumped by the `Rudder` component.
`RealtimeSession`:
- `Task SendAsync(byte[] payload, CancellationToken = default)`
- `Task DisconnectAsync(CancellationToken = default)`
- `bool IsConnected { get; }`
- `event Action Closed`, `event Action<Exception> Error`,
`event Action<ArraySegment<byte>> MessageReceived`.
Transport selection is automatic (`UnityRealtimeTransportFactory`):
WebGL builds use a JavaScript websocket via `RudderWebSocket.jslib`
(`JsWebSocketAdapter`); all other platforms use `NativeWebSocketAdapter`.
@@ -0,0 +1,26 @@
# Remote config
`client.RemoteConfig` (`RemoteConfigService`, namespace `RudderSdk.Core`).
Verified against the bundled `Rudder.Core.dll` and
`Samples~/RemoteConfig/RemoteConfigSample.cs`.
```csharp
await client.RemoteConfig.LoadAsync();
var speed = client.RemoteConfig.Get("player_speed", 5f);
```
API:
- `Task<IReadOnlyDictionary<string, RemoteConfig>> LoadAsync(CancellationToken = default)` — fetches all configs into the cache.
- `T Get<T>(string key, T defaultValue = default)` — reads the cache synchronously. Call `LoadAsync` first (or use `GetAsync`).
- `Task<T> GetAsync<T>(string key, T defaultValue = default, CancellationToken = default)` — loads on first use, then reads.
- `Task<RemoteConfig> GetConfigAsync(string key, CancellationToken = default)` — full config entry.
- `bool IsLoaded { get; }`, `IReadOnlyDictionary<string, RemoteConfig> Configs { get; }` — cache state.
`RemoteConfig` model: `Id`, `Key`, `Value` (string), `ValueType`,
`Description`, `Environment`, `ProjectId`, `Active` (`bool?`), `CreatedAt`,
`UpdatedAt`.
Scenario `OnConfigChanged` sessions mean the cache was already patched —
re-read with `Get`, do not reload. A missing key returns the fallback; the
key must exist in the dashboard to have a server value.
@@ -0,0 +1,52 @@
# Scenarios
`client.Scenario` (`ScenarioService`) — the LiveOps plan runtime. Verified
against the bundled `Rudder.Core.dll` and
`Samples~/Scenarios/ScenariosSample.cs`.
## Flow
Subscribe to `On*` events **before** triggering, then trigger an event name
configured in the dashboard (examples: `player_login`,
`demo_round_finished`). Unsubscribe on destroy.
```csharp
client.Scenario.OnNotification += session => { /* show UI, then session.Complete() */ };
var runs = await client.Scenario.TriggerAsync("player_login");
```
API:
- `Task<IReadOnlyList<PlanRun>> TriggerAsync(string eventName, CancellationToken = default)` — returns the runs it started.
- `Task RestoreAsync(CancellationToken = default)` — restore persisted runs.
Already called by `client.AuthorizeWithDeviceAsync`; do not call both.
- `void Update(float deltaTime)` — pumped by the `Rudder` component.
- `IReadOnlyList<PlanRun> ActiveRuns { get; }``PlanRun` exposes `RunId`,
`PlanId`, `ScenarioId`, `UserId`, `ActiveNodeIds`, `Plan`.
Run state persists in PlayerPrefs (`UnityPlanStateStore`), so sessions
survive restarts.
## Events and sessions
All session types expose `Id`, `ScenarioNodeContext Context`, and
`T Get<T>(string key, T defaultValue = default)` for node data.
`ScenarioNodeContext` exposes `Run`, `Node`, `RunId`, `PlanId`, `ScenarioId`,
`NodeId`, `Type`, `Data` (`JObject`), `AsObjectDictionary()`.
| Event | Session | Game must |
| --- | --- | --- |
| `OnNotification` | `NotificationSession` | Show UI, then `Complete()` / `CompleteAsync()`. |
| `OnStoreOffer` | `StoreOfferSession` | Show offer (`session.Data`, `session.Get("storeSlug", ...)`); on buy call `Stores.PurchaseAsync` then `Purchase()` / `PurchaseAsync()`, else `Decline()` / `DeclineAsync()`. `IsResolved` marks resolution. |
| `OnConfigChanged` | `ConfigChangedSession` | Re-read `RemoteConfig.Get` — patches are already applied to the cache. |
| `OnWait` | `WaitSession` | Deadline only (`DeadlineUtc`); the runtime completes it. |
| `OnQuest` | `QuestSession` | Scenario quest node: `AddProgress(counterKey, amount)` / `AddProgressAsync`, `Complete()` / `CompleteAsync()`, `Fail()` / `FailAsync()`. Not `client.Quests`. |
| `OnLeaderboard` | `LeaderboardSession` | `End()` / `EndAsync()`, `RewardClaimed()` / `RewardClaimedAsync()`. |
| `OnBattlePass` | `BattlePassSession` | Drive the session — see [battlepass.md](battlepass.md). |
| `OnBattlePassLevel` | `BattlePassLevelSession` | `Claim()` / `ClaimAsync()` for the level reward; `Level` prop. |
| `OnScenarioCompleted` | `PlanRun` | Log / surface. |
| `OnScenarioFailed` | `ScenarioFailedEvent` | `Run`, `NodeId`, `Exception`. Log / surface. |
Async and sync variants are equivalent; the sync variants fire-and-forget
the same server callback. The event name must match a scenario trigger
configured in the dashboard — an unknown event simply starts zero runs.
@@ -0,0 +1,37 @@
# Storage
Two services, both verified against the bundled `Rudder.Core.dll` and
`Samples~/Storage/StorageSample.cs`.
## Player storage — `client.Storage` (`StorageService`)
Per-player key-value items. `StorageItem` is `{ Type, Id, Data }` where
`Data` is an opaque JSON string — serialize your save object with
`JsonUtility` or Newtonsoft; do not invent a second save format.
- `Task<GetStorageResponse> GetAsync(string type = null, int limit = 100, string cursor = null, CancellationToken = default)` — pages items; response has `Items` (`List<StorageItem>`) and `NextCursor`.
- `IAsyncEnumerable<StorageItem> ListAllAsync(string type = null, int limit = 100, CancellationToken = default)` — follows cursors for you.
- `Task SaveAsync(IEnumerable<StorageItem> items, CancellationToken = default)` — upserts by `Type`+`Id`.
- `Task DeleteAsync(string type, CancellationToken = default)` — deletes all items of that type.
```csharp
await client.Storage.SaveAsync(new[]
{
new StorageItem { Type = "sample_save", Id = "main", Data = json }
});
var response = await client.Storage.GetAsync("sample_save");
await client.Storage.DeleteAsync("sample_save");
```
## Project storage — `client.ProjectStorage` (`ProjectStorageService`)
Global key-value storage shared across players (read from the SDK; writes
respect item permissions).
- `Task<GetProjectStorageResponse> GetAsync(string type = null, int limit = 100, string cursor = null, CancellationToken = default)``Items` + `NextCursor`.
- `IAsyncEnumerable<ProjectStorageItem> ListAllAsync(string type = null, int limit = 100, CancellationToken = default)`
- `Task SaveAsync(IEnumerable<ProjectStorageUpdateItem> items, string idempotencyKey = null, CancellationToken = default)`
`ProjectStorageItem`: `Type`, `Id`, `Data` (JSON string), `ReadPermission`,
`WritePermission`, `Version` (`long?`), `Size` (`long?`), `ExpiresAt`,
`UpdatedAt`. `ProjectStorageUpdateItem`: `{ Type, Data }`.
@@ -0,0 +1,44 @@
# Stores and inventory
Verified against the bundled `Rudder.Core.dll` and
`Samples~/StoreInventory/StoreInventorySample.cs`.
## Stores — `client.Stores` (`StoresService`)
```csharp
var stores = await client.Stores.ListAsync();
var response = await client.Stores.PurchaseAsync(storeSlug, offerId);
if (response != null && response.Success != true)
throw new InvalidOperationException(response.Error ?? "Purchase rejected");
```
- `Task<IReadOnlyList<Store>> ListAsync(CancellationToken = default)`
- `Task<Store> GetAsync(string slug, CancellationToken = default)`
- `Task<PurchaseOfferResponse> PurchaseAsync(string storeSlug, string offerId, string idempotencyKey = null, CancellationToken = default)` — generates an idempotency key when omitted.
Models:
- `Store``Id`, `Slug`, `Name`, `Description`, `Status`, `Environment`,
`ProjectId`, `ScenarioId`, `Data` (`JToken`), `Offers` (`List<Offer>`),
`CreatedAt`, `UpdatedAt`.
- `Offer``Id`, `Name`, `Price` (`OfferPrice`), `Contents`
(`List<OfferContent>`), `MaxPurchases` (`int?`), `CreatedAt`, `UpdatedAt`.
- `OfferPrice``Currency`, `Amount` (`long?`).
- `OfferContent``ItemId`, `Amount` (`long?`).
- `PurchaseOfferResponse``Success` (`bool?`), `Error`, `PurchaseId`.
Check `Success != true` (nullable bool), not `!Success`. After a purchase,
reload wallets (`Player.GetProfileAsync`) and inventory — there is no
`onChange` callback. Purchases auto-report quest metrics
`purchase.offer:<offerId>` / `purchase.item:<itemId>` (see
[quests.md](quests.md)). The store slug must exist in the dashboard, else 404.
## Inventory — `client.Inventory` (`InventoryService`)
```csharp
var items = await client.Inventory.GetAsync();
```
- `Task<IReadOnlyList<PlayerInventoryItem>> GetAsync(CancellationToken = default)`
- `PlayerInventoryItem``Slug`, `Amount` (`long?`), `NameOverride`,
`PropertiesOverride` (`JToken`), `UpdatedAt`.