# Rudder Unity SDK — agent integration guide

This file is for coding agents adding `rudder.sdk` to a Unity game. Read it
before writing integration code. The public API is `Rudder` + `RudderClient`;
verify signatures in this package and in `Rudder.Core.dll` (`RudderSdk.Core`),
not from memory.

Human-facing install notes live in `README.md`. Copy-paste scenes live in
`Samples~/` (Package Manager → Rudder SDK → Feature Samples).

## Goal

Wire LiveOps into the existing game: initialize once, log in with the device,
then call the feature the user asked for. Do not build a second game, a
debug dashboard, or a wrapper layer around the SDK.

## Install

Add the scoped registry and both packages to `Packages/manifest.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"
  }
}
```

`com.unity.nuget.newtonsoft-json` is required. The package does not bundle
Newtonsoft. Unity `6000.0` or newer.

Namespaces:

- `RudderSdk.Unity` — `Rudder`, `RudderConfiguration`, `RudderState`
- `RudderSdk.Core` — `RudderClient` and services
- `RudderSdk.Core.Models.*` — `PlayerProfile`, `Offer`, `StorageItem`, `RankEntry`, …

## Bootstrap (do this once)

1. `Assets > Create > Rudder > Configuration`
2. Set `ProjectKey` (required). Defaults: `BaseUrl` `https://api.rudder.build`,
   timeout 10s.
3. Add the `Rudder` component to a startup scene and assign the asset.
4. Get the Core client, then authorize:

```csharp
using RudderSdk.Unity;

var client = Rudder.Initialize();
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
```

`Initialize()` is synchronous: it reads the serialized configuration and
creates `RudderClient`. It does not create a GameObject — the component must
already be in the scene. Calling it again returns the same client.

`Auth.LoginWithDeviceAsync` logs in with the device id. After that, all
features hang off `client` / `Rudder.Client`. Pending scenario effects are
fetched automatically (heartbeat after sign-in, and immediately from
`TriggerAsync`).

Login uses the SDK device id (PlayerPrefs). Do not pass
`SystemInfo.deviceUniqueIdentifier`. Tokens persist in PlayerPrefs; a second
launch can still call `LoginWithDeviceAsync` (it refreshes the session).

Do not call `client.Update`. The `Rudder` component pumps it every frame
(effects heartbeat and wait-deadline checks need that).

Subscribe to `client.Effects.On*` before `Scenario.TriggerAsync`.

## Feature map

Call sites hang off `Rudder.Client`. Match the user's request to one row.
Copy the matching scene under `Samples~/` if the shape is unclear.

| Need | API | Sample |
| --- | --- | --- |
| Sign in / profile / wallets | `Auth.LoginWithDeviceAsync`, `Auth.Logout`, `Player.GetProfileAsync` | `Samples~/Authentication` |
| Tunables | `RemoteConfig.LoadAsync`, `Get<T>(key, fallback)` | `Samples~/RemoteConfig` |
| Cloud save | `Storage.GetAsync` / `SaveAsync` / `DeleteAsync` | `Samples~/Storage` |
| Shop + inventory | `Stores.ListAsync`, `PurchaseAsync`, `Inventory.GetAsync` | `Samples~/StoreInventory` |
| High scores | `Leaderboards.FindBySlug(slug).SubmitAsync` / `ListAsync` | `Samples~/Leaderboards` |
| LiveOps plans | `Scenario.TriggerAsync`, `Effects.OnNotification`, `Effects.OnStoreOffer`, … | `Samples~/Scenarios` |
| Global quests | `Quests.ListAsync`, `ClaimAsync`, `ReportProgressAsync` | (no sample; see below) |
| Battle pass | `BattlePass.*` with scenario/node ids, or `BattlePassEffect` | (no sample; see below) |

### Remote config

```csharp
await client.RemoteConfig.LoadAsync();
var speed = client.RemoteConfig.Get("player_speed", 5f);
```

`Get` reads the cache. Call `LoadAsync` first (or `GetAsync`, which loads on
first use).

### Storage

`StorageItem` is `{ Type, Id, Data }` where `Data` is an opaque JSON string.
`GetAsync(type)` pages; `SaveAsync` upserts; `DeleteAsync(type)` deletes that
type. Prefer `JsonUtility` or Newtonsoft on `Data` — do not invent a second
save format in a wrapper type.

### Stores and inventory

```csharp
var stores = await client.Stores.ListAsync();
var result = await client.Stores.PurchaseAsync(storeSlug, offerId);
if (result != null && result.Success != true) { /* result.Error */ }
var items = await client.Inventory.GetAsync();
```

`PurchaseAsync` generates an idempotency key when omitted. After a purchase,
reload wallets (`Player.GetProfileAsync`) and inventory — there is no
`onChange` callback. Use `Offer`, `Store`, `PlayerInventoryItem` as-is.

### Leaderboards

```csharp
var board = client.Leaderboards.FindBySlug("my-board");
await board.SubmitAsync(score);
var top = await board.ListAsync(10);
```

### Scenarios

Scenario graphs run on the server. The client triggers an event and
surfaces pending effects. Subscribe first, then trigger an event name the
project configured (examples: `player_login`, `demo_round_finished`).

```csharp
client.Effects.OnNotification += effect => { /* show UI, then effect.Done() */ };
await client.Scenario.TriggerAsync("player_login");
```

`Scenario.TriggerAsync(string eventName)` posts the event and ingests any
effects the server returns. The `Rudder` component's per-frame
`client.Update` also heartbeats `GET /sdk/v1/scenarios/pending` (every 30s
while signed in) so effects that land later still arrive.

| Event | Game must |
| --- | --- |
| `OnNotification` | Show UI, then `effect.Done()` / `DoneAsync()` |
| `OnStoreOffer` | Show offer; `Stores.PurchaseAsync` then `effect.Purchase()`, or `effect.Decline()` |
| `OnWait` | Deadline only (`DeadlineUtc`); the server advances the run |
| `OnQuest` | Scenario quest node (`QuestEffect.ReportProgress`), not `client.Quests` |
| `OnLeaderboard` | `effect.End()` / `effect.Claim()` |
| `OnBattlePass` / `OnBattlePassLevel` | Drive the effect (`Claim` on a level effect) |
| `OnScenarioCompleted` / `OnScenarioFailed` | Log / surface |

Unsubscribe on destroy. `TriggerAsync` does not return runs — follow
effects through the `On*` events.

### Quests (global)

`client.Quests` is the player's quest list, distinct from `OnQuest` nodes.

```csharp
var quests = await client.Quests.ListAsync();
await client.Quests.ClaimAsync(quest.Id);
var completedIds = await client.Quests.ReportProgressAsync("kills", 1);
```

Store purchases already report `purchase.offer:<id>` / `purchase.item:<id>`.

### Battle pass

Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect`
from `Effects.OnBattlePass` (it binds scenario/node/run ids). Direct
`BattlePass.GetProgressAsync(scenarioId, nodeId)` needs those ids. Tracks:
`BattlePassService.TrackFree` / `TrackPremium`.

## Errors

Catch `RudderApiException` (and subclasses) from `RudderSdk.Core`:

- `RudderAuthException` — 401; refresh failed → `AuthStateChanged(SignedOut)`
- `RudderNotFoundException` — 404
- `RudderRateLimitException` — 429
- `RudderNetworkException` — no response / timeout

Fields: `StatusCode`, `Code`, `RequestId`. Surface `exception.Message` in UI.
Do not swallow errors.

A 401 on an API call is retried once after refresh. Game code does not
implement that retry.

## Unity rules for this SDK

- Use the game's existing UI. Samples use IMGUI (`OnGUI`) only as a demo
  shell. Do not add UI Toolkit / `UIDocument` / `PanelRenderer` just to call
  Rudder.
- Call the SDK from a `MonoBehaviour` (or the game's existing service object).
  Do not add a second singleton next to `Rudder`.
- Use SDK model types in game code. Do not wrap `Offer` / `PlayerProfile` /
  `RankEntry` in project DTOs unless the user asks.
- Do not declare consumer-local interfaces (`IRudderStore`, …) over
  `RudderClient`.
- Do not edit `Runtime/Plugins/Rudder.Core.dll` or generated models. Change
  the C# SDK repo and copy the DLL if the API is wrong.
- Do not add `replace` / local project references to `Rudder.Core`.
- Game `await` must not use `ConfigureAwait(false)` so continuations return
  to the Unity sync context.
- `Rudder.Initialize()` is idempotent: if `State` is `Ready`, it returns
  `Rudder.Client`.
- Keep the project key out of git. Use a gitignored configuration asset
  (this repo uses `Assets/LiveOpsLocal.asset`).

## Do not

- Invent APIs (`Rudder.Auth`, `client.Scenarios`, `BuyAsync`,
  `LoginViaDeviceAsync`, `AuthorizeWithDeviceAsync` are gone). Current names:
  `Rudder.Initialize()`, `client.Auth.LoginWithDeviceAsync`,
  `client.Scenario.TriggerAsync`, `client.Effects`, `PurchaseAsync`.
- Port a Phaser/React store (event bus, phase machine, `Cozy*` types).
- Generate scenes from editor bake scripts.
- Call `Rudder.Initialize(configuration)` — that overload is gone. Put
  `Rudder` in a scene.
- Block the main thread on HTTP. Use `async`/`await` for `LoginWithDeviceAsync`
  and feature calls.
- Claim a feature works without a matching Rudder project entity (store
  slug, leaderboard slug, remote-config key, scenario event). Missing
  backend data is a 404, not an SDK bug.

## Checklist after integration

1. Configuration asset exists with a real `ProjectKey`.
2. `Rudder` is on a startup scene with that asset assigned.
3. `var client = Rudder.Initialize();` then
   `await client.Auth.LoginWithDeviceAsync(...)` before other calls.
4. The requested feature uses `client.*` / `Rudder.Client.*` with SDK types.
5. Failures hit `RudderApiException` and show in the game UI.
6. Play Mode: login succeeds and the feature call returns or shows a real
   API error (`LastError` / exception message), not a missing-component
   throw.
