Docs: skills/README/CHANGELOG for effects client rewrite
- scenarios.md rewritten for server-side execution + effects API - realtime.md deleted; realtime/planstore references purged from docs - CHANGELOG Unreleased: major bump + migration table
This commit is contained in:
@@ -1,5 +1,51 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
Breaking change — major bump. The local scenario engine is replaced by a
|
||||||
|
server-driven effects client; the realtime websocket client is removed.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Local scenario runtime: `PlanRun`, `ExecutionPlan` walking, node sessions
|
||||||
|
(`NotificationSession`, `StoreOfferSession`, `LeaderboardSession`,
|
||||||
|
`WaitSession`, `QuestSession`, `BattlePassSession`,
|
||||||
|
`BattlePassLevelSession`, `ConfigChangedSession`), `RespondAsync` /
|
||||||
|
`Respond`, `RestoreAsync`, `Clear`, `ActiveRuns`, `IsRunning`,
|
||||||
|
`CurrentNodeId`, `OnConfigChanged`.
|
||||||
|
- `RudderClientOptions.PlanStateStore` / `IPlanStateStore` and
|
||||||
|
`IPlanScheduler` — no local plan persistence or delayed plan work.
|
||||||
|
- `RealtimeService` (`client.Realtime`), `RudderClientOptions.RealtimeUrl` /
|
||||||
|
`RealtimeTransportFactory`, `IRealtimeTransport` /
|
||||||
|
`IRealtimeTransportFactory`.
|
||||||
|
|
||||||
|
### Changed (breaking)
|
||||||
|
|
||||||
|
- `client.Scenario` is a trigger only: `TriggerAsync(eventName)` returns
|
||||||
|
`Task` (not started plans). Returned pending effects are ingested into
|
||||||
|
`client.Effects`.
|
||||||
|
- Subscribe to `client.Effects.On*` instead of `client.Scenario.On*`.
|
||||||
|
Sessions are now effect objects (`NotificationEffect`, `StoreOfferEffect`,
|
||||||
|
…). Complete them with the methods on the effect (`DoneAsync`,
|
||||||
|
`PurchaseAsync`/`DeclineAsync`, `EndAsync`/`ClaimAsync`, …).
|
||||||
|
- `client.Update(deltaTime)` is still required every frame — it now pumps
|
||||||
|
the effects client (30s `GET /sdk/v1/scenarios/pending` heartbeat +
|
||||||
|
wait-deadline checks), not a local DAG or a websocket.
|
||||||
|
- After login (or when constructed with a stored access token), the next
|
||||||
|
`Update` fetches pending effects. There is no `loginEvent` option; trigger
|
||||||
|
login-gated scenarios yourself.
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
| Before | After |
|
||||||
|
|---|---|
|
||||||
|
| `client.Scenario.OnNotification` (and other `On*`) | `client.Effects.OnNotification` (same event names) |
|
||||||
|
| `NotificationSession` / `StoreOfferSession` / … | `NotificationEffect` / `StoreOfferEffect` / … |
|
||||||
|
| `session.CompleteAsync()` / `RespondAsync("output")` | `effect.DoneAsync()` (and the matching method on each effect) |
|
||||||
|
| `RudderClientOptions.PlanStateStore` + `RestoreAsync` | gone — server owns run state; pending fetch after login + heartbeat |
|
||||||
|
| `client.Realtime` | gone |
|
||||||
|
| `client.Update(deltaTime)` | still required (effects pump) |
|
||||||
|
|
||||||
## 0.4.0
|
## 0.4.0
|
||||||
|
|
||||||
- Generated DTOs live under `Models/` (`RudderSdk.Core.Models`). Wire fields
|
- Generated DTOs live under `Models/` (`RudderSdk.Core.Models`). Wire fields
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.NET client SDK for the Rudder LiveOps platform: authentication, player
|
.NET client SDK for the Rudder LiveOps platform: authentication, player
|
||||||
profile, stores, battle pass, quests, leaderboards, inventory, remote config,
|
profile, stores, battle pass, quests, leaderboards, inventory, remote config,
|
||||||
scenarios, storage and realtime.
|
scenarios (server-driven effects), and storage.
|
||||||
|
|
||||||
- Target framework: `netstandard2.1` (works in Unity, .NET, Xamarin).
|
- Target framework: `netstandard2.1` (works in Unity, .NET, Xamarin).
|
||||||
- JSON: Newtonsoft.Json.
|
- JSON: Newtonsoft.Json.
|
||||||
@@ -36,7 +36,7 @@ Console.WriteLine(profile.Player.Id);
|
|||||||
// Buy an offer (idempotency key is generated when omitted).
|
// Buy an offer (idempotency key is generated when omitted).
|
||||||
var purchase = await client.Stores.PurchaseAsync("main-store", "offer-1");
|
var purchase = await client.Stores.PurchaseAsync("main-store", "offer-1");
|
||||||
|
|
||||||
// Pump time-dependent services (scenarios, realtime) every frame.
|
// Pump the effects client (30s pending heartbeat + wait deadlines) every frame.
|
||||||
client.Update(deltaTime);
|
client.Update(deltaTime);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -58,15 +58,15 @@ stack.
|
|||||||
| `Inventory` | `InventoryService` | `GetAsync` |
|
| `Inventory` | `InventoryService` | `GetAsync` |
|
||||||
| `Leaderboards` | `LeaderboardsService` | `FindBySlug(slug)` → handle: `SubmitAsync`, `ListAsync` |
|
| `Leaderboards` | `LeaderboardsService` | `FindBySlug(slug)` → handle: `SubmitAsync`, `ListAsync` |
|
||||||
| `RemoteConfig` | `RemoteConfigService` | `LoadAsync`, `Get<T>`, `GetAsync<T>` |
|
| `RemoteConfig` | `RemoteConfigService` | `LoadAsync`, `Get<T>`, `GetAsync<T>` |
|
||||||
| `Scenario` | `ScenarioService` | `TriggerAsync`, `RestoreAsync`, `On*` effect events |
|
| `Scenario` | `ScenarioService` | `TriggerAsync` |
|
||||||
|
| `Effects` | `EffectsService` | `On*` effect events; complete via methods on the effect objects |
|
||||||
| `Storage` | `StorageService` | `GetAsync`, `ListAllAsync`, `SaveAsync`, `DeleteAsync` |
|
| `Storage` | `StorageService` | `GetAsync`, `ListAllAsync`, `SaveAsync`, `DeleteAsync` |
|
||||||
| `Realtime` | `RealtimeService` | `ConnectAsync`, `DisconnectAsync` |
|
|
||||||
|
|
||||||
## Quests
|
## Quests
|
||||||
|
|
||||||
`client.Quests` covers the player's global quests — list with per-objective
|
`client.Quests` covers the player's global quests — list with per-objective
|
||||||
progress, claim, and metric reports. These are distinct from scenario quest
|
progress, claim, and metric reports. These are distinct from scenario quest
|
||||||
nodes, which advance through `QuestSession` (`Scenario.OnQuest`).
|
nodes, which advance through `QuestEffect` (`Effects.OnQuest`).
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var quests = await client.Quests.ListAsync();
|
var quests = await client.Quests.ListAsync();
|
||||||
@@ -108,11 +108,18 @@ All of them carry `StatusCode` (`int`), the machine-readable `Code`
|
|||||||
|
|
||||||
## Scenario effects
|
## Scenario effects
|
||||||
|
|
||||||
Subscribe to typed sessions on `client.Scenario`:
|
The server owns scenario execution. Trigger with `client.Scenario.TriggerAsync`,
|
||||||
|
subscribe to typed effects on `client.Effects`, and call `client.Update` every
|
||||||
|
frame:
|
||||||
|
|
||||||
`OnNotification`, `OnStoreOffer`, `OnLeaderboard`, `OnConfigChanged`,
|
`OnNotification`, `OnStoreOffer`, `OnLeaderboard`, `OnWait`, `OnQuest`,
|
||||||
`OnWait`, `OnQuest`, `OnBattlePass`, `OnBattlePassLevel`,
|
`OnBattlePass`, `OnBattlePassLevel`, `OnScenarioCompleted`,
|
||||||
`OnScenarioCompleted`, `OnScenarioFailed`.
|
`OnScenarioFailed`.
|
||||||
|
|
||||||
|
Complete an effect with the methods on the object (`DoneAsync`,
|
||||||
|
`PurchaseAsync`/`DeclineAsync`, `EndAsync`/`ClaimAsync`, …) or the run
|
||||||
|
stalls. `unknown_run` / `run_expired` drop the run and fire
|
||||||
|
`OnScenarioFailed`.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: rudder-csharp-sdk
|
name: rudder-csharp-sdk
|
||||||
description: Use when working with the Rudder C# SDK Rudder.Core (namespace RudderSdk.Core) — client init, transports, auth/session lifecycle, and the per-domain services (player, stores, inventory, battle pass, quests, leaderboards, remote config, scenarios, storage, realtime).
|
description: Use when working with the Rudder C# SDK Rudder.Core (namespace RudderSdk.Core) — client init, transports, auth/session lifecycle, and the per-domain services (player, stores, inventory, battle pass, quests, leaderboards, remote config, scenario trigger, scenario effects, storage).
|
||||||
---
|
---
|
||||||
|
|
||||||
# Rudder C# SDK (Rudder.Core)
|
# Rudder C# SDK (Rudder.Core)
|
||||||
@@ -43,8 +43,8 @@ full options surface and the abstraction interfaces):
|
|||||||
- `DeviceIdProvider` → `GuidDeviceIdProvider` (new GUID per run — plug a
|
- `DeviceIdProvider` → `GuidDeviceIdProvider` (new GUID per run — plug a
|
||||||
persistent `IDeviceIdProvider` for production)
|
persistent `IDeviceIdProvider` for production)
|
||||||
|
|
||||||
`RealtimeUrl` + `RealtimeTransportFactory` are required only for
|
`Clock` defaults to `DateTimeOffset.UtcNow`; override in tests. There is no
|
||||||
`client.Realtime`; there is no default websocket transport in Rudder.Core.
|
realtime websocket client in Rudder.Core.
|
||||||
|
|
||||||
## Request pipeline (applies to every service)
|
## Request pipeline (applies to every service)
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ full options surface and the abstraction interfaces):
|
|||||||
failures throw `RudderNetworkException` (`StatusCode == 0`). See
|
failures throw `RudderNetworkException` (`StatusCode == 0`). See
|
||||||
reference/errors.md.
|
reference/errors.md.
|
||||||
- `client.Update(deltaTime)` must be called every frame — it pumps the
|
- `client.Update(deltaTime)` must be called every frame — it pumps the
|
||||||
scenario runtime (wait-node deadlines) and the realtime transport.
|
effects client (30s pending heartbeat and wait-node deadline checks).
|
||||||
|
|
||||||
## Capability map
|
## Capability map
|
||||||
|
|
||||||
@@ -74,19 +74,19 @@ full options surface and the abstraction interfaces):
|
|||||||
| Stores and offer purchases | `client.Stores` | reference/stores.md |
|
| Stores and offer purchases | `client.Stores` | reference/stores.md |
|
||||||
| Leaderboards | `client.Leaderboards.FindBySlug(slug)` | reference/leaderboards.md |
|
| Leaderboards | `client.Leaderboards.FindBySlug(slug)` | reference/leaderboards.md |
|
||||||
| Inventory | `client.Inventory` | reference/inventory.md |
|
| Inventory | `client.Inventory` | reference/inventory.md |
|
||||||
| Battle pass | `client.BattlePass` (+ scenario session) | reference/battlepass.md |
|
| Battle pass | `client.BattlePass` (+ scenario effect) | reference/battlepass.md |
|
||||||
| Quests (global) | `client.Quests` | reference/quests.md |
|
| Quests (global) | `client.Quests` | reference/quests.md |
|
||||||
| Scenarios (event-driven plans, node sessions) | `client.Scenario` | reference/scenarios.md |
|
| Scenario trigger | `client.Scenario` | reference/scenarios.md |
|
||||||
| Realtime websocket | `client.Realtime` | reference/realtime.md |
|
| Scenario effects | `client.Effects.On*` | reference/scenarios.md |
|
||||||
|
|
||||||
Important cross-cutting facts:
|
Important cross-cutting facts:
|
||||||
|
|
||||||
- Battle pass state is tied to a scenario battle-pass node: every
|
- Battle pass state is tied to a scenario battle-pass node: every
|
||||||
`BattlePassService` call carries `ScenarioId`/`NodeId` (mutations also
|
`BattlePassService` call carries `ScenarioId`/`NodeId` (mutations also
|
||||||
`RunId`). During a scenario run, `BattlePassSession` supplies them — prefer
|
`RunId`). During a scenario run, `BattlePassEffect` supplies them — prefer
|
||||||
the session API inside a run.
|
the effect API inside a run.
|
||||||
- Global quests (`client.Quests`) are distinct from scenario quest nodes
|
- Global quests (`client.Quests`) are distinct from scenario quest nodes
|
||||||
(`Scenario.OnQuest` → `QuestSession`).
|
(`Effects.OnQuest` → `QuestEffect`).
|
||||||
- Mutations that take an idempotency key auto-generate a random GUID when the
|
- Mutations that take an idempotency key auto-generate a random GUID when the
|
||||||
key is omitted (`Stores.PurchaseAsync`, `ProjectStorage.SaveAsync`,
|
key is omitted (`Stores.PurchaseAsync`, `ProjectStorage.SaveAsync`,
|
||||||
`BattlePass.PurchasePremiumAsync`). Pass a stable key to make retries safe.
|
`BattlePass.PurchasePremiumAsync`). Pass a stable key to make retries safe.
|
||||||
@@ -96,10 +96,10 @@ Important cross-cutting facts:
|
|||||||
## Relationship to the Unity package (rudder.sdk)
|
## Relationship to the Unity package (rudder.sdk)
|
||||||
|
|
||||||
`rudder.sdk` (UPM, liveops-unity-sdk) wraps `Rudder.Core.dll` with Unity
|
`rudder.sdk` (UPM, liveops-unity-sdk) wraps `Rudder.Core.dll` with Unity
|
||||||
adapters: `UnityWebRequest` transport, `PlayerPrefs` token store, a websocket
|
adapters: `UnityWebRequest` transport, `PlayerPrefs` token store, and a
|
||||||
realtime transport, and a `Rudder` bootstrap component
|
`Rudder` bootstrap component (`RudderSdk.Unity` namespace). The domain
|
||||||
(`RudderSdk.Unity` namespace). The domain services and models are the same
|
services and models are the same ones documented here — for Unity games,
|
||||||
ones documented here — for Unity games, consume them through the package's
|
consume them through the package's adapters instead of wiring
|
||||||
adapters instead of wiring `RudderClientOptions` by hand. Unity requires
|
`RudderClientOptions` by hand. Unity requires
|
||||||
`com.unity.nuget.newtonsoft-json` since Rudder.Core serializes with
|
`com.unity.nuget.newtonsoft-json` since Rudder.Core serializes with
|
||||||
Newtonsoft.Json.
|
Newtonsoft.Json.
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ public class LoginViaDeviceResponse / LoginViaCustomResponse
|
|||||||
## Behavior notes
|
## Behavior notes
|
||||||
|
|
||||||
- A successful login stores the token pair in the configured `ITokenStore`
|
- A successful login stores the token pair in the configured `ITokenStore`
|
||||||
and fires `SignedIn`.
|
and fires `SignedIn`. `EffectsService` treats `SignedIn` as a pending-effects
|
||||||
|
refresh due: the next `client.Update` GETs `/sdk/v1/scenarios/pending`.
|
||||||
|
This SDK does not auto-trigger a login scenario event.
|
||||||
- If the server returns an incomplete session (missing tokens), login throws
|
- If the server returns an incomplete session (missing tokens), login throws
|
||||||
`InvalidOperationException`.
|
`InvalidOperationException`.
|
||||||
- Token refresh is automatic: any API call that gets a 401 triggers one
|
- Token refresh is automatic: any API call that gets a 401 triggers one
|
||||||
@@ -56,3 +58,5 @@ public class LoginViaDeviceResponse / LoginViaCustomResponse
|
|||||||
transparent retry. A failed refresh clears the tokens and fires
|
transparent retry. A failed refresh clears the tokens and fires
|
||||||
`SignedOut`; the original call then throws `RudderAuthException`.
|
`SignedOut`; the original call then throws `RudderAuthException`.
|
||||||
- `RefreshAsync()` shares the same single-flight path.
|
- `RefreshAsync()` shares the same single-flight path.
|
||||||
|
- Logout / `SignedOut` clears the effects client's seen-effect set and wait
|
||||||
|
deadlines.
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# Battle pass — `client.BattlePass` (`BattlePassService`)
|
# Battle pass — `client.BattlePass` (`BattlePassService`)
|
||||||
|
|
||||||
Source: `Services/BattlePassService.cs`,
|
Source: `Services/BattlePassService.cs`,
|
||||||
`Services/Scenarios/Sessions/BattlePassSession.cs`,
|
`Services/Effects/BattlePassEffect.cs`,
|
||||||
`Services/Scenarios/Sessions/BattlePassLevelSession.cs`, `BattlePass/*.cs`
|
`Services/Effects/BattlePassLevelEffect.cs`, `BattlePass/*.cs`
|
||||||
(generated DTOs).
|
(generated DTOs).
|
||||||
|
|
||||||
Battle pass state is tied to a scenario battle-pass node, so every call
|
Battle pass state is tied to a scenario battle-pass node, so every call
|
||||||
carries `ScenarioId`/`NodeId` (mutations also `RunId`). Inside a scenario run,
|
carries `ScenarioId`/`NodeId` (mutations also `RunId`). Inside a scenario run,
|
||||||
`Scenario.OnBattlePass` hands you a `BattlePassSession` that supplies these
|
`Effects.OnBattlePass` hands you a `BattlePassEffect` that supplies these
|
||||||
ids — prefer it over calling the service directly.
|
ids — prefer it over calling the service directly.
|
||||||
|
|
||||||
## BattlePassService
|
## BattlePassService
|
||||||
@@ -88,7 +88,6 @@ public class AddBattlePassXpResponse
|
|||||||
public int? Level { get; set; } // "level"
|
public int? Level { get; set; } // "level"
|
||||||
public bool? LeveledUp { get; set; } // "leveledUp"
|
public bool? LeveledUp { get; set; } // "leveledUp"
|
||||||
public bool? MaxLevel { get; set; } // "maxLevel"
|
public bool? MaxLevel { get; set; } // "maxLevel"
|
||||||
public ExecutionPlan? Plan { get; set; } // "plan" — scenario continuation
|
|
||||||
public long? Xp { get; set; } // "xp"
|
public long? Xp { get; set; } // "xp"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +102,6 @@ public class ClaimBattlePassRewardResponse
|
|||||||
public class PurchaseBattlePassPremiumResponse
|
public class PurchaseBattlePassPremiumResponse
|
||||||
{
|
{
|
||||||
public string? Error { get; set; } // "error"
|
public string? Error { get; set; } // "error"
|
||||||
public ExecutionPlan? Plan { get; set; } // "plan"
|
|
||||||
public bool? Success { get; set; } // "success"
|
public bool? Success { get; set; } // "success"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -112,36 +110,35 @@ public class PurchaseBattlePassPremiumResponse
|
|||||||
`ItemId` (string?). Responses carry business errors in `Error` — check
|
`ItemId` (string?). Responses carry business errors in `Error` — check
|
||||||
`Success`. Claim failures surface codes such as `RudderErrorCodes.LevelNotReached`.
|
`Success`. Claim failures surface codes such as `RudderErrorCodes.LevelNotReached`.
|
||||||
|
|
||||||
## BattlePassSession (scenario node session)
|
## `BattlePassEffect` (scenario battlepass node)
|
||||||
|
|
||||||
Raised via `Scenario.OnBattlePass`. Bound to the node's scenario/node/run ids:
|
Raised via `Effects.OnBattlePass`. Bound to the node's scenario/node/run ids:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public ScenarioNodeContext Context { get; }
|
public string RunId { get; }
|
||||||
public string Id { get; } // node id
|
public string ScenarioId { get; }
|
||||||
|
public string NodeId { get; }
|
||||||
public T Get<T>(string key, T defaultValue = default!);
|
public T Get<T>(string key, T defaultValue = default!);
|
||||||
|
|
||||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken ct = default);
|
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken ct = default);
|
||||||
public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken ct = default);
|
public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken ct = default);
|
||||||
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken ct = default);
|
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken ct = default);
|
||||||
|
|
||||||
// Purchases premium, then crosses the onPremiumPurchase handle on success.
|
// Purchases premium, then posts onPremiumPurchase when response.Success == true.
|
||||||
public Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken ct = default);
|
public Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
// Boundary crossings (async + fire-and-forget variants):
|
|
||||||
public Task LevelUpAsync(CancellationToken ct = default); public void LevelUp(); // onLevelUp
|
public Task LevelUpAsync(CancellationToken ct = default); public void LevelUp(); // onLevelUp
|
||||||
public Task MaxLevelAsync(CancellationToken ct = default); public void MaxLevel(); // onMaxLevel
|
public Task EndAsync(CancellationToken ct = default); public void End(); // onComplete
|
||||||
public Task CompleteAsync(CancellationToken ct = default); public void Complete(); // onComplete
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## BattlePassLevelSession (scenario battlepass_level node)
|
## `BattlePassLevelEffect` (scenario battlepass_level node)
|
||||||
|
|
||||||
Raised via `Scenario.OnBattlePassLevel`; a single claimable tier:
|
Raised via `Effects.OnBattlePassLevel`; a single claimable tier:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public int Level { get; } // reads the "levelNumber" node data key
|
public int Level { get; } // reads the "levelNumber" node data key
|
||||||
public Task ClaimAsync(CancellationToken ct = default); public void Claim();
|
public Task ClaimAsync(CancellationToken ct = default); public void Claim();
|
||||||
```
|
```
|
||||||
|
|
||||||
`ClaimAsync` crosses `onComplete`; the server accepts it only once the player
|
`ClaimAsync` posts `onComplete`; the server accepts it only once the player
|
||||||
has reached the node's configured level.
|
has reached the node's configured level.
|
||||||
|
|||||||
@@ -19,14 +19,13 @@ public sealed class RudderClient
|
|||||||
public BattlePassService BattlePass { get; }
|
public BattlePassService BattlePass { get; }
|
||||||
public QuestsService Quests { get; }
|
public QuestsService Quests { get; }
|
||||||
public ScenarioService Scenario { get; }
|
public ScenarioService Scenario { get; }
|
||||||
public RealtimeService Realtime { get; }
|
public EffectsService Effects { get; }
|
||||||
|
|
||||||
public string ProjectKey { get; }
|
public string ProjectKey { get; }
|
||||||
public string? RealtimeUrl { get; }
|
public IClock Clock { get; } // Options.Clock, else DateTimeOffset.UtcNow
|
||||||
public IClock Clock { get; } // defaults to DateTimeOffset.UtcNow
|
|
||||||
|
|
||||||
public RudderClient(RudderClientOptions options);
|
public RudderClient(RudderClientOptions options);
|
||||||
public void Update(float deltaTime); // pumps Scenario + Realtime; call every frame
|
public void Update(float deltaTime); // pumps Effects; call every frame
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,16 +39,12 @@ single-flight refresh and one transparent retry (see errors.md).
|
|||||||
public sealed class RudderClientOptions
|
public sealed class RudderClientOptions
|
||||||
{
|
{
|
||||||
public string? BaseUrl { get; set; } // required
|
public string? BaseUrl { get; set; } // required
|
||||||
public string? RealtimeUrl { get; set; } // required only for Realtime
|
|
||||||
public string? ProjectKey { get; set; } // required
|
public string? ProjectKey { get; set; } // required
|
||||||
public IRudderTransport? Transport { get; set; } // default HttpClientTransport
|
public IRudderTransport? Transport { get; set; } // default HttpClientTransport
|
||||||
public ITokenStore? TokenStore { get; set; } // default InMemoryTokenStore
|
public ITokenStore? TokenStore { get; set; } // default InMemoryTokenStore
|
||||||
public IDeviceIdProvider? DeviceIdProvider { get; set; }// default GuidDeviceIdProvider
|
public IDeviceIdProvider? DeviceIdProvider { get; set; }// default GuidDeviceIdProvider
|
||||||
public IRudderLogger? Logger { get; set; } // null = silent
|
public IRudderLogger? Logger { get; set; } // null = silent
|
||||||
public IClock? Clock { get; set; } // override in tests
|
public IClock? Clock { get; set; } // time source for the effects client; override in tests
|
||||||
public IPlanStateStore? PlanStateStore { get; set; } // scenario persistence between launches
|
|
||||||
public IPlanScheduler? Scheduler { get; set; } // optional delayed plan work
|
|
||||||
public IRealtimeTransportFactory? RealtimeTransportFactory { get; set; } // required only for Realtime
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,27 +84,6 @@ public interface IRudderLogger { void Log(RudderLogLevel level, string message);
|
|||||||
public enum RudderLogLevel { Debug, Info, Warning, Error }
|
public enum RudderLogLevel { Debug, Info, Warning, Error }
|
||||||
|
|
||||||
public interface IClock { DateTimeOffset UtcNow { get; } }
|
public interface IClock { DateTimeOffset UtcNow { get; } }
|
||||||
|
|
||||||
public interface IPlanStateStore { string? State { get; set; } } // serialized scenario-run blob
|
|
||||||
|
|
||||||
public interface IPlanScheduler
|
|
||||||
{
|
|
||||||
Task ScheduleAsync(TimeSpan delay, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IRealtimeTransport
|
|
||||||
{
|
|
||||||
event Action Closed;
|
|
||||||
event Action<ArraySegment<byte>> Received;
|
|
||||||
event Action<Exception> Error;
|
|
||||||
bool IsConnected { get; }
|
|
||||||
Task ConnectAsync(Uri uri, TimeSpan timeout, CancellationToken cancellationToken = default);
|
|
||||||
Task SendAsync(ArraySegment<byte> data, CancellationToken cancellationToken = default);
|
|
||||||
Task CloseAsync(CancellationToken cancellationToken = default);
|
|
||||||
void Update(float deltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IRealtimeTransportFactory { IRealtimeTransport Create(); }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Production guidance from the sources: provide a durable `ITokenStore`
|
Production guidance from the sources: provide a durable `ITokenStore`
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
# Errors, exceptions, error codes
|
# Errors, exceptions, error codes
|
||||||
|
|
||||||
Source: `Exceptions/*.cs`, `Models/RudderErrorCodes.cs`,
|
Source: `Exceptions/*.cs`, `Models/RudderErrorCodes.cs`,
|
||||||
`Models/ErrorResponse.cs`, `HttpClientTransport.cs` (status mapping),
|
`Models/ErrorResponse.cs`, `HttpClientTransport.cs` (status mapping).
|
||||||
`Services/Scenarios/ScenarioService.Runtime.cs` (`TransientBoundaryException`).
|
|
||||||
|
|
||||||
## Exception hierarchy
|
## Exception hierarchy
|
||||||
|
|
||||||
@@ -59,11 +58,11 @@ UnknownRun = "unknown_run"
|
|||||||
|
|
||||||
Match against `RudderApiException.Code`.
|
Match against `RudderApiException.Code`.
|
||||||
|
|
||||||
## Scenario boundary errors
|
## Scenario callback / counter errors
|
||||||
|
|
||||||
`TransientBoundaryException` (public sealed, wraps the transport error) is
|
On `POST /sdk/v1/scenarios/callback` and `POST /sdk/v1/scenarios/counter`,
|
||||||
used inside the scenario runtime for transient boundary-callback failures
|
`unknown_run`, `run_expired`, and HTTP 404 drop that run and fire
|
||||||
(timeout / `RudderNetworkException`): the run is NOT advanced, the node stays
|
`Effects.OnScenarioFailed`. Other callback errors propagate to the caller.
|
||||||
active and the handle stays pending for retry. A failed scenario counter
|
A failed counter update that is not one of those definitive rejections is
|
||||||
update never fails the run — it is logged via `IRudderLogger` and the node
|
logged via `IRudderLogger` and the effect stays active. Details:
|
||||||
stays active.
|
reference/scenarios.md.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Source: `Services/QuestsService.cs`, `Services/QuestMetrics.cs`,
|
|||||||
`Quests/*.cs` (generated DTOs).
|
`Quests/*.cs` (generated DTOs).
|
||||||
|
|
||||||
Global quests (list + claim + metric reports). Distinct from scenario quest
|
Global quests (list + claim + metric reports). Distinct from scenario quest
|
||||||
nodes, which advance through `QuestSession` (`Scenario.OnQuest`) — see
|
nodes, which advance through `QuestEffect` (`Effects.OnQuest`) — see
|
||||||
scenarios.md.
|
scenarios.md.
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
# Realtime — `client.Realtime` (`RealtimeService` + `RealtimeSession`)
|
|
||||||
|
|
||||||
Source: `Services/RealtimeService.cs`, `Abstractions/IRealtimeTransport.cs`,
|
|
||||||
`Abstractions/IRealtimeTransportFactory.cs`.
|
|
||||||
|
|
||||||
Websocket channel. Requires `RudderClientOptions.RealtimeUrl` and
|
|
||||||
`RudderClientOptions.RealtimeTransportFactory` — Rudder.Core ships no default
|
|
||||||
websocket transport (the Unity package `rudder.sdk` provides one). The current
|
|
||||||
access token authorizes the connection, so sign in first.
|
|
||||||
|
|
||||||
## RealtimeService
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public RealtimeSession? Session { get; } // null when not connected
|
|
||||||
public bool IsConnected { get; }
|
|
||||||
|
|
||||||
// Connects to the configured RealtimeUrl. Throws InvalidOperationException
|
|
||||||
// when RealtimeUrl is not configured.
|
|
||||||
public Task<RealtimeSession> ConnectAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
// Connects to an explicit URL. Throws InvalidOperationException when
|
|
||||||
// RealtimeTransportFactory is missing or no access token is stored.
|
|
||||||
public Task<RealtimeSession> ConnectAsync(Uri uri, TimeSpan? timeout = null, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
public Task DisconnectAsync(CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
public void Update(float deltaTime); // pumps the transport; call every frame (client.Update does this)
|
|
||||||
```
|
|
||||||
|
|
||||||
Default connect timeout: 10 seconds.
|
|
||||||
|
|
||||||
## RealtimeSession
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public string AccessToken { get; } // token the connection was authorized with
|
|
||||||
public bool IsConnected { get; }
|
|
||||||
|
|
||||||
public event Action? Closed;
|
|
||||||
public event Action<Exception>? Error;
|
|
||||||
public event Action<ArraySegment<byte>>? MessageReceived;
|
|
||||||
|
|
||||||
public Task SendAsync(byte[] payload, CancellationToken cancellationToken = default);
|
|
||||||
public Task DisconnectAsync(CancellationToken cancellationToken = default);
|
|
||||||
```
|
|
||||||
|
|
||||||
There is no message envelope or channel abstraction — payloads are raw bytes;
|
|
||||||
define the message shape with the server side of your project.
|
|
||||||
|
|
||||||
Note: the working tree contains an untracked `Services/RealtimeSession.cs`
|
|
||||||
with a stale `RealtimeSession` (no `AccessToken`). The committed definition in
|
|
||||||
`Services/RealtimeService.cs` documented above is authoritative.
|
|
||||||
@@ -48,5 +48,6 @@ public class RemoteConfig
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Scenario `remote_config_override` nodes apply patches directly into this
|
Scenario `remote_config_override` nodes are applied server-side and do not
|
||||||
cache before `Scenario.OnConfigChanged` fires (see scenarios.md).
|
reach the client. Call `LoadAsync` (or `GetAsync`) to observe the patched
|
||||||
|
value via `Get<T>`.
|
||||||
|
|||||||
@@ -1,149 +1,273 @@
|
|||||||
# Scenarios — `client.Scenario` (`ScenarioService`)
|
# Scenarios + effects — `client.Scenario` + `client.Effects`
|
||||||
|
|
||||||
Source: `Services/ScenarioService.cs`, `Services/Scenarios/ScenarioService.{Dispatch,NodeTypes,Persistence,Runtime}.cs`,
|
Source: `Services/ScenarioService.cs`, `Services/EffectsService.cs`,
|
||||||
`Services/Scenarios/Models/*.cs`, `Services/Scenarios/Sessions/*.cs`,
|
`Services/Effects/*.cs`, `Scenarios/*.cs` (generated DTOs).
|
||||||
`Scenarios/*.cs` (generated DTOs), `Models/*.cs`.
|
|
||||||
|
|
||||||
Scenario runtime for server-issued execution plans. `TriggerAsync(eventName)`
|
Scenarios are server-authored node graphs (configured in the dashboard) that
|
||||||
starts plans; active nodes surface as typed sessions through the `On*` events
|
run per player. The **server owns the graph**. This SDK is a thin effects
|
||||||
and are advanced by completing those sessions (which crosses server-validated
|
client: it sends trigger events, polls pending effects, raises typed
|
||||||
boundaries). Requires `Update(deltaTime)` every frame for wait-node deadlines.
|
`client.Effects.On*` events, and posts callbacks when the game completes an
|
||||||
|
effect. There is no local plan, no `IPlanStateStore`, and no restore.
|
||||||
|
|
||||||
## ScenarioService surface
|
## Lifecycle
|
||||||
|
|
||||||
|
- `client.Scenario.TriggerAsync(eventName)` POSTs
|
||||||
|
`/sdk/v1/scenarios/trigger` with `{event}`. Returned `PendingEffect`s are
|
||||||
|
ingested into `client.Effects` (they do **not** come back as a return
|
||||||
|
value).
|
||||||
|
- Drive `client.Update(deltaTime)` every frame. That pumps
|
||||||
|
`EffectsService.Update`: a 30s heartbeat (`GET /sdk/v1/scenarios/pending`)
|
||||||
|
plus an immediate pending fetch when any wait deadline is due. The pump
|
||||||
|
no-ops when there is no access token, and it will not start a second
|
||||||
|
fetch while one is in flight.
|
||||||
|
- After login (`AuthStateChanged` → `SignedIn`), or when the client is
|
||||||
|
constructed with an already-stored access token, a pending fetch is marked
|
||||||
|
due and runs on the next `Update`. Sign-out / logout clears the seen-effect
|
||||||
|
set and wait deadlines.
|
||||||
|
- This SDK does **not** fire a login scenario event (`loginEvent` /
|
||||||
|
`player_login` is a web-SDK option only). Trigger login-gated scenarios
|
||||||
|
yourself with `TriggerAsync`.
|
||||||
|
- An effect with an already-seen `(runId, nodeId)` is not re-emitted.
|
||||||
|
- There is no local plan persistence. Pending work lives on the server.
|
||||||
|
|
||||||
|
## `ScenarioService` (`client.Scenario`)
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public sealed partial class ScenarioService
|
public sealed class ScenarioService
|
||||||
{
|
{
|
||||||
// Node events
|
// POST /sdk/v1/scenarios/trigger — body {event}; returned effects are
|
||||||
public event Action<NotificationSession>? OnNotification;
|
// ingested into client.Effects. Returns when the HTTP call finishes.
|
||||||
public event Action<StoreOfferSession>? OnStoreOffer;
|
public Task TriggerAsync(string eventName, CancellationToken ct = default);
|
||||||
public event Action<LeaderboardSession>? OnLeaderboard;
|
|
||||||
public event Action<ConfigChangedSession>? OnConfigChanged; // patches already applied
|
|
||||||
public event Action<WaitSession>? OnWait;
|
|
||||||
public event Action<QuestSession>? OnQuest;
|
|
||||||
public event Action<BattlePassSession>? OnBattlePass;
|
|
||||||
public event Action<BattlePassLevelSession>? OnBattlePassLevel;
|
|
||||||
|
|
||||||
// Run lifecycle events
|
|
||||||
public event Action<PlanRun>? OnScenarioCompleted; // run finished all nodes
|
|
||||||
public event Action<ScenarioFailedEvent>? OnScenarioFailed; // run died on unrecoverable error
|
|
||||||
|
|
||||||
public bool IsRunning { get; } // at least one run active
|
|
||||||
public string? CurrentNodeId { get; } // first active node id across runs
|
|
||||||
public IReadOnlyList<PlanRun> ActiveRuns { get; } // snapshots
|
|
||||||
|
|
||||||
// POST /sdk/v1/scenarios/trigger — body {event}; returns the runs this call started
|
|
||||||
public Task<IReadOnlyList<PlanRun>> TriggerAsync(string eventName, CancellationToken ct = default);
|
|
||||||
|
|
||||||
// Restores persisted runs (needs RudderClientOptions.PlanStateStore),
|
|
||||||
// reconciles with the server, re-dispatches active nodes. Call once after login.
|
|
||||||
public Task RestoreAsync(CancellationToken ct = default);
|
|
||||||
|
|
||||||
public void Clear(); // drops all runs and the persisted state
|
|
||||||
|
|
||||||
public void Update(float deltaTime); // completes wait nodes past their deadline
|
|
||||||
|
|
||||||
// Completes the FIRST active node of the FIRST run with the given handle.
|
|
||||||
public Task RespondAsync(string handle, CancellationToken ct = default);
|
|
||||||
public void Respond(string handle); // fire-and-forget
|
|
||||||
|
|
||||||
// Adds progress to a counter of the FIRST active node of the FIRST run.
|
|
||||||
public Task UpdateProgressAsync(string counterKey, long amount, CancellationToken ct = default);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Prefer session methods over `RespondAsync`/`UpdateProgressAsync` — the
|
## `EffectsService` (`client.Effects`)
|
||||||
parameterless variants only address the first active node of the first run.
|
|
||||||
|
|
||||||
## Node types and session handles
|
```csharp
|
||||||
|
public sealed class EffectsService
|
||||||
|
{
|
||||||
|
public event Action<NotificationEffect>? OnNotification;
|
||||||
|
public event Action<StoreOfferEffect>? OnStoreOffer;
|
||||||
|
public event Action<LeaderboardEffect>? OnLeaderboard;
|
||||||
|
public event Action<WaitEffect>? OnWait;
|
||||||
|
public event Action<QuestEffect>? OnQuest;
|
||||||
|
public event Action<BattlePassEffect>? OnBattlePass;
|
||||||
|
public event Action<BattlePassLevelEffect>? OnBattlePassLevel;
|
||||||
|
public event Action<ScenarioCompletedEffect>? OnScenarioCompleted;
|
||||||
|
public event Action<ScenarioFailedEffect>? OnScenarioFailed;
|
||||||
|
|
||||||
| Node `type` | Event | Session completion handles |
|
public void Update(float deltaTime); // also called by client.Update
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Subscribe before login / trigger so you do not miss the first batch.
|
||||||
|
Handlers are `Action<T>` (not `Func<T, Task>`) — do not use `async`
|
||||||
|
lambdas on these events. Exceptions thrown inside handlers are logged via
|
||||||
|
`IRudderLogger` (Error) and do not fail the run. Always subscribe to
|
||||||
|
`OnScenarioFailed` — otherwise run failures surface only as logs.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
client.Effects.OnNotification += n =>
|
||||||
|
{
|
||||||
|
ShowToast(n.Title, n.Message);
|
||||||
|
n.Done(); // always resolve or the run stalls; use DoneAsync from a Task context
|
||||||
|
};
|
||||||
|
|
||||||
|
await client.Scenario.TriggerAsync("level_complete");
|
||||||
|
```
|
||||||
|
|
||||||
|
Every effect exposes `RunId`, `ScenarioId`, `NodeId`, `Data` (`JObject`),
|
||||||
|
and `Get<T>(key, defaultValue)` for reading node data. Completion methods
|
||||||
|
have an `Async` variant (awaitable) and a fire-and-forget variant.
|
||||||
|
|
||||||
|
## Effect types
|
||||||
|
|
||||||
|
| Node `type` | Event | How the node advances |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `notification` | `OnNotification` | `output` (`CompleteAsync`/`Complete`) |
|
| `notification` | `OnNotification` | `Done*` → handle `output` |
|
||||||
| `store` | `OnStoreOffer` | `onPurchase`, `onDecline` (`Purchase*`/`Decline*`, one-shot via `IsResolved`) |
|
| `store` | `OnStoreOffer` | `Purchase*` → `onPurchase`; `Decline*` → `onDecline` (one-shot via `IsResolved`) |
|
||||||
| `wait` | `OnWait` | auto-completes `onComplete` at `WaitSession.DeadlineUtc` |
|
| `leaderboard` | `OnLeaderboard` | `End*` → `onEnd`; `Claim*` → `onClaim` (one-shot via `IsResolved`) |
|
||||||
| `remote_config_override` | `OnConfigChanged` | auto-completes `output` after applying patches to `RemoteConfig` |
|
| `wait` | `OnWait` | none — server advances at `DeadlineUtc`; SDK polls pending then |
|
||||||
| `quest` | `OnQuest` | `onComplete`, `onFail`; `AddProgressAsync(counterKey, amount)` feeds counters |
|
| `quest` | `OnQuest` | `ReportProgress*` via counter; server auto-completes when all objectives are satisfied |
|
||||||
| `leaderboard` | `OnLeaderboard` | `onEnd` (`End*`), `onClaim` (`Claim*` / obsolete `RewardClaimed*`) |
|
| `battlepass` | `OnBattlePass` | `LevelUp*` → `onLevelUp`; `End*` → `onComplete`; successful `PurchasePremiumAsync` → `onPremiumPurchase` |
|
||||||
| `battlepass` | `OnBattlePass` | `onLevelUp`, `onMaxLevel`, `onComplete`, `onPremiumPurchase` (see battlepass.md) |
|
| `battlepass_level` | `OnBattlePassLevel` | `Claim*` → `onComplete` (server checks the configured level) |
|
||||||
| `battlepass_level` | `OnBattlePassLevel` | `onComplete` via `ClaimAsync` — server checks the configured level |
|
|
||||||
|
`remote_config_override` is applied server-side and is **not** delivered to
|
||||||
|
the client. There is no `OnConfigChanged`. Reload `client.RemoteConfig` to
|
||||||
|
observe patched values.
|
||||||
|
|
||||||
An unsupported node type fails the run (`OnScenarioFailed`) instead of
|
An unsupported node type fails the run (`OnScenarioFailed`) instead of
|
||||||
stalling. All sessions expose `Context` (`ScenarioNodeContext`), `Id` (node
|
stalling.
|
||||||
id) and `Get<T>(key, defaultValue)` for reading node data.
|
|
||||||
|
|
||||||
## ScenarioNodeContext
|
### `NotificationEffect`
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public PlanRun Run { get; }
|
public string Title { get; } // node data "title"
|
||||||
public ExecutionPlanNode Node { get; }
|
public string Message { get; } // node data "message"
|
||||||
public string RunId / PlanId / ScenarioId / NodeId / Type { get; }
|
|
||||||
public JObject Data { get; }
|
public Task DoneAsync(CancellationToken ct = default); // handle "output"
|
||||||
public T Get<T>(string key, T defaultValue = default!);
|
public void Done();
|
||||||
public T Get<T>(); // whole payload
|
|
||||||
public Dictionary<string, object> AsObjectDictionary();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## PlanRun and failure event
|
### `StoreOfferEffect`
|
||||||
|
|
||||||
|
Posts the scenario callback only — it does **not** call `Stores.PurchaseAsync`.
|
||||||
|
Purchase through `client.Stores` first if the offer should actually be bought,
|
||||||
|
then complete the effect.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public sealed class PlanRun
|
public string StoreSlug { get; } // node data "storeSlug"
|
||||||
|
public string? Message { get; } // node data "message", null if empty
|
||||||
|
public bool IsResolved { get; } // true after Purchase* or Decline* once
|
||||||
|
|
||||||
|
public Task PurchaseAsync(CancellationToken ct = default); // "onPurchase"
|
||||||
|
public void Purchase();
|
||||||
|
public Task DeclineAsync(CancellationToken ct = default); // "onDecline"
|
||||||
|
public void Decline();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `LeaderboardEffect`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public bool IsResolved { get; }
|
||||||
|
|
||||||
|
public Task EndAsync(CancellationToken ct = default); // "onEnd"
|
||||||
|
public void End();
|
||||||
|
public Task ClaimAsync(CancellationToken ct = default); // "onClaim" — server matches live rank to a place
|
||||||
|
public void Claim();
|
||||||
|
```
|
||||||
|
|
||||||
|
### `WaitEffect`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public DateTimeOffset DeadlineUtc { get; }
|
||||||
|
```
|
||||||
|
|
||||||
|
No completion method. When `Clock.UtcNow >= DeadlineUtc`, the next `Update`
|
||||||
|
fetches pending so the server-advanced successor can be dispatched.
|
||||||
|
|
||||||
|
### `QuestEffect`
|
||||||
|
|
||||||
|
Scenario quest node — distinct from global `client.Quests`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public string Name { get; } // node data "name"
|
||||||
|
public IReadOnlyList<JObject> Objectives { get; } // node data "objectives"
|
||||||
|
|
||||||
|
// POST /sdk/v1/scenarios/counter {scenarioId, nodeId, runId, counterKey, amount}
|
||||||
|
public Task ReportProgressAsync(string objectiveId, long amount = 1, CancellationToken ct = default);
|
||||||
|
public void ReportProgress(string objectiveId, long amount = 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
The node auto-completes server-side once every objective is satisfied; a
|
||||||
|
completed counter response may carry the next `PendingEffect`.
|
||||||
|
|
||||||
|
### `BattlePassEffect`
|
||||||
|
|
||||||
|
Bound to this node's `ScenarioId` / `NodeId` / `RunId` — prefer these over
|
||||||
|
calling `client.BattlePass` by hand. See also reference/battlepass.md.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken ct = default);
|
||||||
|
public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken ct = default);
|
||||||
|
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken ct = default);
|
||||||
|
|
||||||
|
// Purchases premium, then posts "onPremiumPurchase" when response.Success == true.
|
||||||
|
public Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
|
public Task LevelUpAsync(CancellationToken ct = default); public void LevelUp(); // "onLevelUp"
|
||||||
|
public Task EndAsync(CancellationToken ct = default); public void End(); // "onComplete"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `BattlePassLevelEffect` — a `battlepass_level` node (single claimable tier)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public int Level { get; } // node data "levelNumber"
|
||||||
|
|
||||||
|
public Task ClaimAsync(CancellationToken ct = default); // "onComplete"
|
||||||
|
public void Claim();
|
||||||
|
```
|
||||||
|
|
||||||
|
The server accepts `onComplete` only once the player has reached the node's
|
||||||
|
configured level.
|
||||||
|
|
||||||
|
### Run lifecycle effects
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class ScenarioCompletedEffect
|
||||||
{
|
{
|
||||||
public string RunId { get; }
|
public string RunId { get; }
|
||||||
public string PlanId { get; }
|
|
||||||
public string ScenarioId { get; }
|
public string ScenarioId { get; }
|
||||||
public string UserId { get; }
|
|
||||||
public IReadOnlyList<string> ActiveNodeIds { get; }
|
|
||||||
public ExecutionPlan Plan { get; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ScenarioFailedEvent
|
public sealed class ScenarioFailedEffect
|
||||||
{
|
{
|
||||||
public PlanRun Run { get; }
|
public string RunId { get; }
|
||||||
|
public string ScenarioId { get; }
|
||||||
public string NodeId { get; }
|
public string NodeId { get; }
|
||||||
public Exception Exception { get; }
|
public Exception Exception { get; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## ExecutionPlan (generated, `RudderSdk.Core.Models`)
|
`OnScenarioCompleted` fires when a callback / completed-counter response has
|
||||||
|
no next effect (the run finished all its nodes).
|
||||||
|
|
||||||
|
## Reliability
|
||||||
|
|
||||||
|
- Completion methods POST `/sdk/v1/scenarios/callback` with
|
||||||
|
`{scenarioId, runId, nodeId, handle}` using the handles `output`,
|
||||||
|
`onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`,
|
||||||
|
`onPremiumPurchase`. A non-null `effect` in the response is ingested as
|
||||||
|
the next node; a null/missing effect completes the run.
|
||||||
|
- `unknown_run`, `run_expired` (`RudderErrorCodes`), and HTTP 404
|
||||||
|
(`RudderNotFoundException`) on a callback or counter call **drop that
|
||||||
|
run** (forget its seen keys and wait deadlines) and fire
|
||||||
|
`OnScenarioFailed`.
|
||||||
|
- Other callback errors propagate to the `*Async` caller. Counter-update
|
||||||
|
failures that are not a definitive rejection are logged (Warning) and the
|
||||||
|
effect stays active.
|
||||||
|
- Pending-fetch failures are logged (Warning); the 30s heartbeat continues.
|
||||||
|
- Server scenario errors also include `run_not_active`, `node_not_active`,
|
||||||
|
`scenario_not_active`, `early_completion`, `objectives_incomplete`,
|
||||||
|
`level_not_reached`, `forbidden` (`RudderErrorCodes`).
|
||||||
|
|
||||||
|
## Wire DTOs (`RudderSdk.Core.Models.Scenarios`)
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public class ExecutionPlan
|
public class PendingEffect
|
||||||
{
|
{
|
||||||
public List<BoundaryNode>? BoundaryNodes { get; set; }
|
public JToken Data { get; set; }
|
||||||
public JToken? Context { get; set; }
|
public string NodeId { get; set; }
|
||||||
public List<PlanEdge>? Edges { get; set; }
|
public string RunId { get; set; }
|
||||||
public List<ExecutionPlanNode>? Nodes { get; set; }
|
public string ScenarioId { get; set; }
|
||||||
public string? PlanId { get; set; }
|
public string Type { get; set; }
|
||||||
|
public DateTimeOffset? WaitDeadline { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TriggerScenarioRequest { public string? Event { get; set; } }
|
||||||
|
public class TriggerScenarioResponse { public List<PendingEffect> Effects { get; set; } }
|
||||||
|
public class ListPendingScenarioEffectsResponse { public List<PendingEffect> Effects { get; set; } }
|
||||||
|
|
||||||
|
public class HandleScenarioCallbackRequest
|
||||||
|
{
|
||||||
|
public string? Handle { get; set; }
|
||||||
|
public string? NodeId { get; set; }
|
||||||
public string? RunId { get; set; }
|
public string? RunId { get; set; }
|
||||||
public string? ScenarioId { get; set; }
|
public string? ScenarioId { get; set; }
|
||||||
public string? StartNodeId { get; set; }
|
}
|
||||||
public string? UserId { get; set; }
|
public class HandleScenarioCallbackResponse { public JToken? Effect { get; set; } }
|
||||||
|
|
||||||
|
public class UpdateScenarioCounterRequest
|
||||||
|
{
|
||||||
|
public long? Amount { get; set; }
|
||||||
|
public string? CounterKey { get; set; }
|
||||||
|
public string? NodeId { get; set; }
|
||||||
|
public string? RunId { get; set; }
|
||||||
|
public string? ScenarioId { get; set; }
|
||||||
|
}
|
||||||
|
public class UpdateScenarioCounterResponse
|
||||||
|
{
|
||||||
|
public bool? Completed { get; set; }
|
||||||
|
public JToken? Effect { get; set; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`ExecutionPlanNode`: `Id`, `Type`, `Data` (`JToken`). `PlanEdge`: `Id`,
|
|
||||||
`Source`, `SourceHandle`, `Target`, `TargetHandle`. `BoundaryNode`:
|
|
||||||
`CallbackUrl`, `Enforcement`, `EnteredAt`, `NodeId`, `SourceHandle`,
|
|
||||||
`SourceNodeId`, `WaitDeadline`.
|
|
||||||
|
|
||||||
## Runtime semantics
|
|
||||||
|
|
||||||
- Boundaries: crossing a handle that has a server boundary calls
|
|
||||||
`POST /sdk/v1/scenarios/callback` first; the returned continuation plan
|
|
||||||
replaces/starts the run before the handle is marked complete. On failure the
|
|
||||||
runtime reconciles via `POST /sdk/v1/scenarios/run`; `unknown_run` /
|
|
||||||
`expired` statuses drop the run. Transient failures
|
|
||||||
(`TransientBoundaryException`: timeouts, `RudderNetworkException`) leave the
|
|
||||||
node active for retry; terminal errors fail the run. `rank_not_eligible` on
|
|
||||||
leaderboard Claim leaves the node active (retry Claim or End).
|
|
||||||
- Counter updates (`POST /sdk/v1/scenarios/counter`) never fail the run; when
|
|
||||||
the server reports the objective completed, the node crosses `onComplete`
|
|
||||||
automatically.
|
|
||||||
- Dedup/idempotency: a plan with an already-running `RunId` is not restarted;
|
|
||||||
completed (node, handle) pairs are not re-crossed.
|
|
||||||
- Wait deadlines prefer the server-stamped `BoundaryNode.WaitDeadline` over
|
|
||||||
locally computed `duration`+`unit` node data (`days`/`hours`/`minutes`/
|
|
||||||
`seconds` and abbreviations).
|
|
||||||
- Persistence: with `IPlanStateStore` configured, every state change is
|
|
||||||
serialized into `State`; call `RestoreAsync` once after startup/login.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user