Add agent skill (SKILL.md + per-domain reference)
This commit is contained in:
@@ -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`.
|
||||||
Reference in New Issue
Block a user