diff --git a/skills/rudder-unity-sdk/SKILL.md b/skills/rudder-unity-sdk/SKILL.md new file mode 100644 index 0000000..14fa291 --- /dev/null +++ b/skills/rudder-unity-sdk/SKILL.md @@ -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(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/`. diff --git a/skills/rudder-unity-sdk/reference/authentication.md b/skills/rudder-unity-sdk/reference/authentication.md new file mode 100644 index 0000000..e3fc0ee --- /dev/null +++ b/skills/rudder-unity-sdk/reference/authentication.md @@ -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 LoginWithDeviceAsync(string region, string language, string nickname = null, CancellationToken = default)` +- `Task RefreshAsync()` — explicit token refresh. +- `void Logout()` — clears tokens locally. +- `event Action 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 PlayerService.GetProfileAsync(CancellationToken = default)` +- `PlayerProfile` — `Player Player`, `List 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. diff --git a/skills/rudder-unity-sdk/reference/battlepass.md b/skills/rudder-unity-sdk/reference/battlepass.md new file mode 100644 index 0000000..1b7593a --- /dev/null +++ b/skills/rudder-unity-sdk/reference/battlepass.md @@ -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 GetProgressAsync(CancellationToken = default)` +- `Task AddXpAsync(string source, long amount, CancellationToken = default)` +- `Task ClaimRewardAsync(int level, string track, CancellationToken = default)` +- `Task PurchasePremiumAsync(CancellationToken = default)` +- `LevelUp()` / `LevelUpAsync()`, `MaxLevel()` / `MaxLevelAsync()`, + `Complete()` / `CompleteAsync()` — resolve the scenario node. +- `Get(key, fallback)`, `Id`, `Context` like other sessions. + +`BattlePassLevelSession` (from `OnBattlePassLevel`) has `Claim()` / +`ClaimAsync()` and a `Level` property. + +## BattlePassService (direct) + +- `Task GetProgressAsync(string scenarioId, string nodeId, CancellationToken = default)` +- `Task AddXpAsync(AddBattlePassXpRequest request, CancellationToken = default)` +- `Task ClaimRewardAsync(ClaimBattlePassRewardRequest request, CancellationToken = default)` +- `Task 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`: `Level`, + `Track`). +- `AddBattlePassXpResponse` — `Xp` (`long?`), `Level` (`int?`), + `LeveledUp` (`bool?`), `MaxLevel` (`bool?`), `Plan` (`ExecutionPlan`). +- `ClaimBattlePassRewardResponse` — `Success` (`bool?`), + `AlreadyClaimed` (`bool?`), `Granted` (`List`), `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. diff --git a/skills/rudder-unity-sdk/reference/bootstrap.md b/skills/rudder-unity-sdk/reference/bootstrap.md new file mode 100644 index 0000000..da62161 --- /dev/null +++ b/skills/rudder-unity-sdk/reference/bootstrap.md @@ -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 Ready` — completes with the client after `Initialize`, + faults on initialization error. Prefer `Initialize()` in game code. +- `event Action 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`). diff --git a/skills/rudder-unity-sdk/reference/leaderboards.md b/skills/rudder-unity-sdk/reference/leaderboards.md new file mode 100644 index 0000000..e096b69 --- /dev/null +++ b/skills/rudder-unity-sdk/reference/leaderboards.md @@ -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`. +- `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). diff --git a/skills/rudder-unity-sdk/reference/quests.md b/skills/rudder-unity-sdk/reference/quests.md new file mode 100644 index 0000000..cb58ad1 --- /dev/null +++ b/skills/rudder-unity-sdk/reference/quests.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> ListAsync(CancellationToken = default)` +- `Task ClaimAsync(string questId, CancellationToken = default)` +- `Task> ReportProgressAsync(string metric, long amount, CancellationToken = default)` — returns the ids of quests completed by this report. + +Models: + +- `Quest` — `Id`, `Name`, `Status`, `Objectives` + (`List`), `Rewards` (`List`). +- `QuestObjectiveProgress` — `ObjectiveId`, `Metric`, `Current` (`long?`), + `Target` (`long?`), `Completed` (`bool?`). +- `Reward` — `Currency`, `ItemId`, `Amount` (`long?`). +- `ClaimQuestResponse` — `Success` (`bool?`), `AlreadyClaimed` (`bool?`), + `Granted` (`List`), `Error`. + +Store purchases already report metrics automatically; the metric strings are +built by `QuestMetrics.PurchaseOffer(offerId)` → `purchase.offer:` +and `QuestMetrics.PurchaseItem(itemId)` → `purchase.item:` +(`RudderSdk.Core.QuestMetrics`, static). diff --git a/skills/rudder-unity-sdk/reference/realtime.md b/skills/rudder-unity-sdk/reference/realtime.md new file mode 100644 index 0000000..b8f966d --- /dev/null +++ b/skills/rudder-unity-sdk/reference/realtime.md @@ -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 payload */ }; +await session.SendAsync(payload); +await client.Realtime.DisconnectAsync(); +``` + +API: + +- `Task ConnectAsync(TimeSpan? timeout = null, CancellationToken = default)` — connects to `RudderConfiguration.RealtimeUrl` (default `wss://api.rudder.build/api/realtime/ws`). +- `Task 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 Error`, + `event Action> MessageReceived`. + +Transport selection is automatic (`UnityRealtimeTransportFactory`): +WebGL builds use a JavaScript websocket via `RudderWebSocket.jslib` +(`JsWebSocketAdapter`); all other platforms use `NativeWebSocketAdapter`. diff --git a/skills/rudder-unity-sdk/reference/remote-config.md b/skills/rudder-unity-sdk/reference/remote-config.md new file mode 100644 index 0000000..49195a7 --- /dev/null +++ b/skills/rudder-unity-sdk/reference/remote-config.md @@ -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> LoadAsync(CancellationToken = default)` — fetches all configs into the cache. +- `T Get(string key, T defaultValue = default)` — reads the cache synchronously. Call `LoadAsync` first (or use `GetAsync`). +- `Task GetAsync(string key, T defaultValue = default, CancellationToken = default)` — loads on first use, then reads. +- `Task GetConfigAsync(string key, CancellationToken = default)` — full config entry. +- `bool IsLoaded { get; }`, `IReadOnlyDictionary 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. diff --git a/skills/rudder-unity-sdk/reference/scenarios.md b/skills/rudder-unity-sdk/reference/scenarios.md new file mode 100644 index 0000000..5ed4785 --- /dev/null +++ b/skills/rudder-unity-sdk/reference/scenarios.md @@ -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> 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 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(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. diff --git a/skills/rudder-unity-sdk/reference/storage.md b/skills/rudder-unity-sdk/reference/storage.md new file mode 100644 index 0000000..be477ce --- /dev/null +++ b/skills/rudder-unity-sdk/reference/storage.md @@ -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 GetAsync(string type = null, int limit = 100, string cursor = null, CancellationToken = default)` — pages items; response has `Items` (`List`) and `NextCursor`. +- `IAsyncEnumerable ListAllAsync(string type = null, int limit = 100, CancellationToken = default)` — follows cursors for you. +- `Task SaveAsync(IEnumerable 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 GetAsync(string type = null, int limit = 100, string cursor = null, CancellationToken = default)` — `Items` + `NextCursor`. +- `IAsyncEnumerable ListAllAsync(string type = null, int limit = 100, CancellationToken = default)` +- `Task SaveAsync(IEnumerable items, string idempotencyKey = null, CancellationToken = default)` + +`ProjectStorageItem`: `Type`, `Id`, `Data` (JSON string), `ReadPermission`, +`WritePermission`, `Version` (`long?`), `Size` (`long?`), `ExpiresAt`, +`UpdatedAt`. `ProjectStorageUpdateItem`: `{ Type, Data }`. diff --git a/skills/rudder-unity-sdk/reference/stores-inventory.md b/skills/rudder-unity-sdk/reference/stores-inventory.md new file mode 100644 index 0000000..b656043 --- /dev/null +++ b/skills/rudder-unity-sdk/reference/stores-inventory.md @@ -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> ListAsync(CancellationToken = default)` +- `Task GetAsync(string slug, CancellationToken = default)` +- `Task 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`), + `CreatedAt`, `UpdatedAt`. +- `Offer` — `Id`, `Name`, `Price` (`OfferPrice`), `Contents` + (`List`), `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:` / `purchase.item:` (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> GetAsync(CancellationToken = default)` +- `PlayerInventoryItem` — `Slug`, `Amount` (`long?`), `NameOverride`, + `PropertiesOverride` (`JToken`), `UpdatedAt`.