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
This commit is contained in:
edmand46
2026-09-04 14:23:38 +03:00
parent 22805173eb
commit 6d115f158e
71 changed files with 3338 additions and 1034 deletions
+7 -10
View File
@@ -1,6 +1,6 @@
---
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.
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.
---
# Rudder Unity SDK (`rudder.sdk`)
@@ -13,7 +13,7 @@ adapters in `RudderSdk.Unity`. Public API surface:
`RudderState`, platform adapters.
- `RudderSdk.Core``RudderClient` and feature services (`Auth`, `Player`,
`RemoteConfig`, `Storage`, `ProjectStorage`, `Stores`, `Inventory`,
`Leaderboards`, `Quests`, `BattlePass`, `Scenario`, `Realtime`).
`Leaderboards`, `Quests`, `BattlePass`, `Scenario`, `Effects`).
- `RudderSdk.Core.Models.*` — DTOs (`PlayerProfile`, `Wallet`, `Offer`,
`Store`, `StorageItem`, `RankEntry`, `Quest`, `Reward`, ...).
@@ -47,7 +47,7 @@ the package does not bundle Newtonsoft.Json):
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.
`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
@@ -58,7 +58,7 @@ the package does not bundle Newtonsoft.Json):
using RudderSdk.Unity;
var client = Rudder.Initialize(); // synchronous, idempotent
await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player");
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
```
`Rudder.Initialize()` throws if the component or configuration is missing.
@@ -79,9 +79,7 @@ you with `PlayerPrefsUnityKeyValueStore` + `UnityDebugLoggerSink`:
| `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
@@ -90,15 +88,14 @@ 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) |
| Init, sign in, profile, wallets, errors | `Rudder.Initialize`, `client.Auth.LoginWithDeviceAsync`, `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) |
| LiveOps scenarios | `Scenario.TriggerAsync`, `Effects.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) |
| Battle pass | `BattlePass.*` with scenario/node ids, `BattlePassEffect` | [reference/battlepass.md](reference/battlepass.md) |
## Hard rules
@@ -7,16 +7,16 @@ and `Samples~/Authentication/AuthenticationSample.cs`.
```csharp
var client = Rudder.Initialize();
await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player");
await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player");
```
`RudderClient.AuthorizeWithDeviceAsync(string region, string language,
`AuthService.LoginWithDeviceAsync(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.
returns `LoginViaDeviceResponse` (`AccessToken`, `RefreshToken`). Tokens are
saved to PlayerPrefs by the Unity adapter. Pending scenario effects are
fetched after sign-in by the effects heartbeat.
Lower-level alternative on `client.Auth` (login only, no scenario restore):
Auth service:
- `Task<LoginViaDeviceResponse> LoginWithDeviceAsync(string region, string language, string nickname = null, CancellationToken = default)`
- `Task<bool> RefreshAsync()` — explicit token refresh.
+15 -16
View File
@@ -3,31 +3,31 @@
`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`/
Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect`
delivered by `Effects.OnBattlePass` — it binds `scenarioId`/`nodeId`/
`runId` for you. Direct `BattlePassService` calls need those ids manually.
## BattlePassSession (preferred)
## BattlePassEffect (preferred)
```csharp
client.Scenario.OnBattlePass += async session =>
client.Effects.OnBattlePass += async effect =>
{
var progress = await session.GetProgressAsync();
await session.AddXpAsync("kills", 100);
var claim = await session.ClaimRewardAsync(level, BattlePassService.TrackFree);
var progress = await effect.GetProgressAsync();
await effect.AddXpAsync("kills", 100);
var claim = await effect.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.
- `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken = default)` — posts `onPremiumPurchase` on success
- `LevelUp()` / `LevelUpAsync()` — posts `onLevelUp`
- `End()` / `EndAsync()`posts `onComplete`
- `Get<T>(key, fallback)`, `RunId`, `ScenarioId`, `NodeId`, `Data` like other effects
`BattlePassLevelSession` (from `OnBattlePassLevel`) has `Claim()` /
`ClaimAsync()` and a `Level` property.
`BattlePassLevelEffect` (from `OnBattlePassLevel`) has `Claim()` /
`ClaimAsync()` (posts `onComplete`) and a `Level` property.
## BattlePassService (direct)
@@ -46,11 +46,10 @@ Request models carry `ScenarioId`, `NodeId`, and (except progress)
`PremiumOwned` (`bool?`), `ClaimedTiers` (`List<ClaimedTier>`: `Level`,
`Track`).
- `AddBattlePassXpResponse``Xp` (`long?`), `Level` (`int?`),
`LeveledUp` (`bool?`), `MaxLevel` (`bool?`), `Plan` (`ExecutionPlan`).
`LeveledUp` (`bool?`), `MaxLevel` (`bool?`).
- `ClaimBattlePassRewardResponse``Success` (`bool?`),
`AlreadyClaimed` (`bool?`), `Granted` (`List<Reward>`), `Error`.
- `PurchaseBattlePassPremiumResponse``Success` (`bool?`), `Error`,
`Plan` (`ExecutionPlan`).
- `PurchaseBattlePassPremiumResponse``Success` (`bool?`), `Error`.
Business failures come back in the response (`Success != true`, `Error`)
with codes from `RudderErrorCodes` (`LevelNotReached`,
+13 -11
View File
@@ -12,7 +12,6 @@ Create via `Assets > Create > Rudder > Configuration`.
| --- | --- | --- | --- |
| `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
@@ -23,8 +22,8 @@ Keep the asset with the real key out of git (this repo uses the git-ignored
`[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.
`_client.Update(Time.deltaTime)` (effects heartbeat and wait-deadline checks
need it — do not call `client.Update` yourself).
Static API:
@@ -56,18 +55,21 @@ public class Bootstrap : MonoBehaviour
private async void Start()
{
var client = Rudder.Initialize();
await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player");
await client.Auth.LoginWithDeviceAsync("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
`LoginWithDeviceAsync` logs in with the SDK device id (a GUID persisted
in PlayerPrefs under `rudder_device_id`). 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.
Pending scenario effects are fetched after sign-in by the effects heartbeat
(`client.Update`, pumped by `Rudder`). Subscribe to `client.Effects.On*`
before `Scenario.TriggerAsync`.
## Errors
All API errors are `RudderApiException` subclasses from `RudderSdk.Core`:
@@ -85,5 +87,5 @@ token refresh — game code does not implement that retry. Catch and surface
Removed APIs (do not use): `Rudder.Initialize(configuration)`,
`Rudder.Auth`-style static service shortcuts, `client.Scenarios`,
`BuyAsync`, `LoginViaDeviceAsync`, `Scenario.RestoreAsync` (folded into
`AuthorizeWithDeviceAsync`).
`BuyAsync`, `LoginViaDeviceAsync`, `AuthorizeWithDeviceAsync`,
`Scenario.RestoreAsync`.
+1 -1
View File
@@ -1,7 +1,7 @@
# Quests (global)
`client.Quests` (`QuestsService`) — the player's global quest list, distinct
from scenario `OnQuest` nodes (`QuestSession`, see
from scenario `OnQuest` nodes (`QuestEffect`, see
[scenarios.md](scenarios.md)). Verified against the bundled
`Rudder.Core.dll`. There is no sample scene for this feature.
@@ -1,36 +0,0 @@
# 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`.
@@ -21,6 +21,5 @@ API:
`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.
A missing key returns the fallback; the key must exist in the dashboard to
have a server value.
+25 -30
View File
@@ -1,52 +1,47 @@
# Scenarios
`client.Scenario` (`ScenarioService`) — the LiveOps plan runtime. Verified
against the bundled `Rudder.Core.dll` and
Scenario graphs execute on the server. `client.Scenario` (`ScenarioService`)
only triggers an event; pending nodes surface through `client.Effects`
(`EffectsService`). Verified against the bundled `Rudder.Core.dll` and
`Samples~/Scenarios/ScenariosSample.cs`.
## Flow
Subscribe to `On*` events **before** triggering, then trigger an event name
Subscribe to `Effects.On*` **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");
client.Effects.OnNotification += effect => { /* show UI, then effect.Done() */ };
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`.
- `Task ScenarioService.TriggerAsync(string eventName, CancellationToken = default)`posts the event and ingests any effects the response contains. Does not return runs.
- `void RudderClient.Update(float deltaTime)` — pumped by the `Rudder` component. Drives the 30s pending-effects heartbeat and wait-deadline checks. Do not call it from game code.
Run state persists in PlayerPrefs (`UnityPlanStateStore`), so sessions
survive restarts.
After `Auth.LoginWithDeviceAsync`, the heartbeat fetches pending effects for
the signed-in player. Effects that arrive later (or after a wait deadline)
are dispatched the same way.
## Events and sessions
## Events and effects
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()`.
All effect types expose `RunId`, `ScenarioId`, `NodeId`, `Data` (`JObject`),
and `T Get<T>(string key, T defaultValue = default)` for node data.
| Event | Session | Game must |
| Event | Effect | 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. |
| `OnNotification` | `NotificationEffect` | Show UI (`Title`, `Message`), then `Done()` / `DoneAsync()`. |
| `OnStoreOffer` | `StoreOfferEffect` | Show offer (`StoreSlug`, `Get("storeSlug", ...)`); on buy call `Stores.PurchaseAsync` then `Purchase()` / `PurchaseAsync()`, else `Decline()` / `DeclineAsync()`. `IsResolved` marks resolution. |
| `OnWait` | `WaitEffect` | Deadline only (`DeadlineUtc`); the server advances the run. |
| `OnQuest` | `QuestEffect` | Scenario quest node: `ReportProgress(objectiveId, amount)` / `ReportProgressAsync`. The server auto-completes the node once every objective is satisfied. Not `client.Quests`. |
| `OnLeaderboard` | `LeaderboardEffect` | `End()` / `EndAsync()`, `Claim()` / `ClaimAsync()`. `IsResolved` marks resolution. |
| `OnBattlePass` | `BattlePassEffect` | Drive the effect — see [battlepass.md](battlepass.md). |
| `OnBattlePassLevel` | `BattlePassLevelEffect` | `Claim()` / `ClaimAsync()` for the level reward; `Level` prop. |
| `OnScenarioCompleted` | `ScenarioCompletedEffect` | `RunId`, `ScenarioId`. Log / surface. |
| `OnScenarioFailed` | `ScenarioFailedEffect` | `RunId`, `ScenarioId`, `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.
configured in the dashboard — an unknown event simply produces no effects.