Files
rudder-unity-sdk/Packages/rudder.sdk/AGENTS.md
T
edmand46 6d115f158e Server-side scenario execution: new Rudder.Core.dll, realtime/planstore glue removed
- Rudder.Core.dll rebuilt from csharp-sdk effects rewrite (96,768 bytes)
- Realtime/ adapters, UnityRealtimeTransportFactory, UnityPlanStateStore,
  UnityPlanScheduler, WebGL jslib and RealtimeUrl config deleted (with .meta)
- Scenarios sample rewired to client.Effects; samples login path updated
- AGENTS.md/README/CHANGELOG/package docs + agent skill updated; realtime.md skill doc removed
2026-09-04 14:23:38 +03:00

244 lines
9.4 KiB
Markdown

# 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.