Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef5624ee4e | |||
| 1029f08fba | |||
| 6c590ca520 | |||
| 05dd30f31d | |||
| ea20ea6d51 | |||
| 3c590294c9 | |||
| d4bbafb314 |
@@ -3,3 +3,4 @@ obj/
|
||||
.vs/
|
||||
.idea/
|
||||
TestResults/
|
||||
.DS_Store
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
|
||||
namespace RudderSdk.Core.Abstractions;
|
||||
|
||||
/// <summary>Time source used by the scenario runtime; override in tests.</summary>
|
||||
/// <summary>Time source used by the effects client; override in tests.</summary>
|
||||
public interface IClock
|
||||
{
|
||||
/// <summary>Current UTC time.</summary>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RudderSdk.Core.Abstractions;
|
||||
|
||||
/// <summary>Optional scheduler abstraction for delayed plan work.</summary>
|
||||
public interface IPlanScheduler
|
||||
{
|
||||
/// <summary>Completes after the given delay.</summary>
|
||||
Task ScheduleAsync(TimeSpan delay, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace RudderSdk.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Persists serialized scenario-run state between app launches.
|
||||
/// Null by default (no persistence).
|
||||
/// </summary>
|
||||
public interface IPlanStateStore
|
||||
{
|
||||
/// <summary>Serialized state blob, or null when empty.</summary>
|
||||
string? State { get; set; }
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RudderSdk.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Low-level realtime (websocket) transport. There is no default implementation;
|
||||
/// provide one through <see cref="IRealtimeTransportFactory"/> (Unity ships its own).
|
||||
/// </summary>
|
||||
public interface IRealtimeTransport
|
||||
{
|
||||
/// <summary>Raised when the connection closes.</summary>
|
||||
event Action Closed;
|
||||
|
||||
/// <summary>Raised for every incoming message.</summary>
|
||||
event Action<ArraySegment<byte>> Received;
|
||||
|
||||
/// <summary>Raised on transport errors.</summary>
|
||||
event Action<Exception> Error;
|
||||
|
||||
/// <summary>True while the connection is open.</summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>Opens the connection, giving up after <paramref name="timeout"/>.</summary>
|
||||
Task ConnectAsync(Uri uri, TimeSpan timeout, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Sends one message.</summary>
|
||||
Task SendAsync(ArraySegment<byte> data, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Closes the connection.</summary>
|
||||
Task CloseAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Pumps time-dependent logic; call every frame.</summary>
|
||||
void Update(float deltaTime);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace RudderSdk.Core.Abstractions;
|
||||
|
||||
/// <summary>Creates realtime transports on demand (one per connection).</summary>
|
||||
public interface IRealtimeTransportFactory
|
||||
{
|
||||
/// <summary>Creates a new, unconnected transport.</summary>
|
||||
IRealtimeTransport Create();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace RudderSdk.Core.Models.Auth;
|
||||
|
||||
public class LoginViaCustomRequest
|
||||
{
|
||||
[JsonProperty("customData")]
|
||||
public JToken? CustomData { get; set; }
|
||||
|
||||
[JsonProperty("key")]
|
||||
public string? Key { get; set; }
|
||||
|
||||
[JsonProperty("language")]
|
||||
public string? Language { get; set; }
|
||||
|
||||
[JsonProperty("nickname")]
|
||||
public string? Nickname { get; set; }
|
||||
|
||||
[JsonProperty("region")]
|
||||
public string? Region { get; set; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace RudderSdk.Core.Models.Auth;
|
||||
|
||||
public class LoginViaCustomResponse
|
||||
{
|
||||
[JsonProperty("accessToken")]
|
||||
public string? AccessToken { get; set; }
|
||||
|
||||
[JsonProperty("refreshToken")]
|
||||
public string? RefreshToken { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,25 +1,23 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Auth;
|
||||
|
||||
public class LoginViaDeviceRequest
|
||||
{
|
||||
[JsonProperty("deviceId")]
|
||||
public string DeviceId { get; set; }
|
||||
public string? DeviceId { get; set; }
|
||||
|
||||
[JsonProperty("key")]
|
||||
public string Key { get; set; }
|
||||
public string? Key { get; set; }
|
||||
|
||||
[JsonProperty("language")]
|
||||
public string Language { get; set; }
|
||||
public string? Language { get; set; }
|
||||
|
||||
[JsonProperty("nickname")]
|
||||
public string Nickname { get; set; }
|
||||
public string? Nickname { get; set; }
|
||||
|
||||
[JsonProperty("region")]
|
||||
public string Region { get; set; }
|
||||
public string? Region { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Auth;
|
||||
|
||||
public class LoginViaDeviceResponse
|
||||
{
|
||||
[JsonProperty("accessToken")]
|
||||
public string AccessToken { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
|
||||
[JsonProperty("refreshToken")]
|
||||
public string RefreshToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Auth;
|
||||
|
||||
public class RefreshAccessTokenRequest
|
||||
{
|
||||
[JsonProperty("refreshToken")]
|
||||
public string RefreshToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Auth;
|
||||
|
||||
public class RefreshAccessTokenResponse
|
||||
{
|
||||
[JsonProperty("accessToken")]
|
||||
public string AccessToken { get; set; }
|
||||
public string? AccessToken { get; set; }
|
||||
|
||||
[JsonProperty("refreshToken")]
|
||||
public string RefreshToken { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
public class AddBattlePassXpRequest
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public long Amount { get; set; }
|
||||
public long? Amount { get; set; }
|
||||
|
||||
[JsonProperty("nodeId")]
|
||||
public string NodeId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; }
|
||||
public string? RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string ScenarioId { get; set; }
|
||||
public string? ScenarioId { get; set; }
|
||||
|
||||
[JsonProperty("source")]
|
||||
public string Source { get; set; }
|
||||
public string? Source { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
public class AddBattlePassXpResponse
|
||||
{
|
||||
[JsonProperty("level")]
|
||||
public int Level { get; set; }
|
||||
public int? Level { get; set; }
|
||||
|
||||
[JsonProperty("leveledUp")]
|
||||
public bool LeveledUp { get; set; }
|
||||
public bool? LeveledUp { get; set; }
|
||||
|
||||
[JsonProperty("maxLevel")]
|
||||
public bool MaxLevel { get; set; }
|
||||
|
||||
[JsonProperty("plan")]
|
||||
public ExecutionPlan Plan { get; set; }
|
||||
public bool? MaxLevel { get; set; }
|
||||
|
||||
[JsonProperty("xp")]
|
||||
public long Xp { get; set; }
|
||||
public long? Xp { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
public class BattlePassReward
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public long Amount { get; set; }
|
||||
|
||||
[JsonProperty("currency")]
|
||||
public string Currency { get; set; }
|
||||
|
||||
[JsonProperty("itemId")]
|
||||
public string ItemId { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,25 +1,23 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
public class ClaimBattlePassRewardRequest
|
||||
{
|
||||
[JsonProperty("level")]
|
||||
public int Level { get; set; }
|
||||
public int? Level { get; set; }
|
||||
|
||||
[JsonProperty("nodeId")]
|
||||
public string NodeId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; }
|
||||
public string? RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string ScenarioId { get; set; }
|
||||
public string? ScenarioId { get; set; }
|
||||
|
||||
[JsonProperty("track")]
|
||||
public string Track { get; set; }
|
||||
public string? Track { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
public class ClaimBattlePassRewardResponse
|
||||
{
|
||||
[JsonProperty("alreadyClaimed")]
|
||||
public bool AlreadyClaimed { get; set; }
|
||||
public bool? AlreadyClaimed { get; set; }
|
||||
|
||||
[JsonProperty("error")]
|
||||
public string Error { get; set; }
|
||||
public string? Error { get; set; }
|
||||
|
||||
[JsonProperty("granted")]
|
||||
public List<BattlePassReward> Granted { get; set; }
|
||||
public List<Reward>? Granted { get; set; }
|
||||
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
public bool? Success { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
public class ClaimedTier
|
||||
{
|
||||
[JsonProperty("level")]
|
||||
public int Level { get; set; }
|
||||
public int? Level { get; set; }
|
||||
|
||||
[JsonProperty("track")]
|
||||
public string Track { get; set; }
|
||||
public string? Track { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
public class GetBattlePassProgressRequest
|
||||
{
|
||||
[JsonProperty("nodeId")]
|
||||
public string NodeId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string ScenarioId { get; set; }
|
||||
public string? ScenarioId { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.BattlePass;
|
||||
@@ -8,15 +7,15 @@ namespace RudderSdk.Core.Models.BattlePass;
|
||||
public class GetBattlePassProgressResponse
|
||||
{
|
||||
[JsonProperty("claimedTiers")]
|
||||
public List<ClaimedTier> ClaimedTiers { get; set; }
|
||||
public List<ClaimedTier>? ClaimedTiers { get; set; }
|
||||
|
||||
[JsonProperty("level")]
|
||||
public int Level { get; set; }
|
||||
public int? Level { get; set; }
|
||||
|
||||
[JsonProperty("premiumOwned")]
|
||||
public bool PremiumOwned { get; set; }
|
||||
public bool? PremiumOwned { get; set; }
|
||||
|
||||
[JsonProperty("xp")]
|
||||
public long Xp { get; set; }
|
||||
public long? Xp { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
public class PurchaseBattlePassPremiumRequest
|
||||
{
|
||||
[JsonProperty("idempotencyKey")]
|
||||
public string IdempotencyKey { get; set; }
|
||||
public string? IdempotencyKey { get; set; }
|
||||
|
||||
[JsonProperty("nodeId")]
|
||||
public string NodeId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; }
|
||||
public string? RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string ScenarioId { get; set; }
|
||||
public string? ScenarioId { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
public class PurchaseBattlePassPremiumResponse
|
||||
{
|
||||
[JsonProperty("error")]
|
||||
public string Error { get; set; }
|
||||
|
||||
[JsonProperty("plan")]
|
||||
public ExecutionPlan Plan { get; set; }
|
||||
public string? Error { get; set; }
|
||||
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
public bool? Success { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models;
|
||||
|
||||
public class BoundaryNode
|
||||
{
|
||||
[JsonProperty("callbackUrl")]
|
||||
public string CallbackUrl { get; set; }
|
||||
|
||||
[JsonProperty("enforcement")]
|
||||
public string Enforcement { get; set; }
|
||||
|
||||
[JsonProperty("enteredAt")]
|
||||
public string EnteredAt { get; set; }
|
||||
|
||||
[JsonProperty("nodeId")]
|
||||
public string NodeId { get; set; }
|
||||
|
||||
[JsonProperty("sourceHandle")]
|
||||
public string SourceHandle { get; set; }
|
||||
|
||||
[JsonProperty("sourceNodeId")]
|
||||
public string SourceNodeId { get; set; }
|
||||
|
||||
[JsonProperty("waitDeadline")]
|
||||
public string WaitDeadline { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,5 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.0
|
||||
|
||||
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
|
||||
|
||||
- Generated DTOs live under `Models/` (`RudderSdk.Core.Models`). Wire fields
|
||||
are nullable. `ListQuestsRequest` is gone; `Quests.ListAsync` POSTs with
|
||||
no body. `BoundaryNode.WaitDeadline` is `DateTimeOffset?`.
|
||||
- `LeaderboardSession.ClaimAsync` / `Claim` complete the leaderboard node
|
||||
with handle `onClaim`. The server matches live rank to an authored place.
|
||||
- `RewardClaimedAsync` / `RewardClaimed` are obsolete aliases and will be
|
||||
removed in the next SDK version.
|
||||
- `rank_not_eligible` on Claim leaves the session open (retry Claim or End);
|
||||
it no longer fails the run.
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Removed
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Catalog;
|
||||
|
||||
public class CatalogItem
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("properties")]
|
||||
public JToken Properties { get; set; }
|
||||
|
||||
[JsonProperty("slug")]
|
||||
public string Slug { get; set; }
|
||||
|
||||
[JsonProperty("tags")]
|
||||
public List<string> Tags { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Catalog;
|
||||
|
||||
public class ListCatalogItemsResponse
|
||||
{
|
||||
[JsonProperty("items")]
|
||||
public List<CatalogItem> Items { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models;
|
||||
|
||||
public class ErrorResponse
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public string Code { get; set; }
|
||||
|
||||
[JsonProperty("error")]
|
||||
public string Error { get; set; }
|
||||
|
||||
[JsonProperty("requestId")]
|
||||
public string RequestId { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models;
|
||||
|
||||
public class ExecutionPlan
|
||||
{
|
||||
[JsonProperty("boundaryNodes")]
|
||||
public List<BoundaryNode> BoundaryNodes { get; set; }
|
||||
|
||||
[JsonProperty("context")]
|
||||
public JToken Context { get; set; }
|
||||
|
||||
[JsonProperty("edges")]
|
||||
public List<PlanEdge> Edges { get; set; }
|
||||
|
||||
[JsonProperty("nodes")]
|
||||
public List<ExecutionPlanNode> Nodes { get; set; }
|
||||
|
||||
[JsonProperty("planId")]
|
||||
public string PlanId { get; set; }
|
||||
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string ScenarioId { get; set; }
|
||||
|
||||
[JsonProperty("startNodeId")]
|
||||
public string StartNodeId { get; set; }
|
||||
|
||||
[JsonProperty("userId")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models;
|
||||
|
||||
public class ExecutionPlanNode
|
||||
{
|
||||
[JsonProperty("data")]
|
||||
public JToken Data { get; set; }
|
||||
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Inventory;
|
||||
@@ -8,6 +7,6 @@ namespace RudderSdk.Core.Models.Inventory;
|
||||
public class GetInventoryResponse
|
||||
{
|
||||
[JsonProperty("items")]
|
||||
public List<PlayerInventoryItem> Items { get; set; }
|
||||
public List<PlayerInventoryItem>? Items { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace RudderSdk.Core.Models.Inventory;
|
||||
|
||||
public class PlayerInventoryItem
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public long Amount { get; set; }
|
||||
public long? Amount { get; set; }
|
||||
|
||||
[JsonProperty("nameOverride")]
|
||||
public string NameOverride { get; set; }
|
||||
public string? NameOverride { get; set; }
|
||||
|
||||
[JsonProperty("propertiesOverride")]
|
||||
public JToken PropertiesOverride { get; set; }
|
||||
public JToken? PropertiesOverride { get; set; }
|
||||
|
||||
[JsonProperty("slug")]
|
||||
public string Slug { get; set; }
|
||||
public string? Slug { get; set; }
|
||||
|
||||
[JsonProperty("updatedAt")]
|
||||
public string UpdatedAt { get; set; }
|
||||
public DateTimeOffset? UpdatedAt { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Leaderboards;
|
||||
@@ -8,9 +7,9 @@ namespace RudderSdk.Core.Models.Leaderboards;
|
||||
public class GetRankingResponse
|
||||
{
|
||||
[JsonProperty("entries")]
|
||||
public List<RankEntry> Entries { get; set; }
|
||||
public List<RankEntry>? Entries { get; set; }
|
||||
|
||||
[JsonProperty("total")]
|
||||
public long Total { get; set; }
|
||||
public long? Total { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Leaderboards;
|
||||
|
||||
public class RankEntry
|
||||
{
|
||||
[JsonProperty("playerId")]
|
||||
public string PlayerId { get; set; }
|
||||
public string? PlayerId { get; set; }
|
||||
|
||||
[JsonProperty("playerName")]
|
||||
public string PlayerName { get; set; }
|
||||
public string? PlayerName { get; set; }
|
||||
|
||||
[JsonProperty("rank")]
|
||||
public long Rank { get; set; }
|
||||
public long? Rank { get; set; }
|
||||
|
||||
[JsonProperty("score")]
|
||||
public double Score { get; set; }
|
||||
public double? Score { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Leaderboards;
|
||||
|
||||
public class SubmitScoreRequest
|
||||
{
|
||||
[JsonProperty("score")]
|
||||
public double Score { get; set; }
|
||||
public double? Score { get; set; }
|
||||
|
||||
[JsonProperty("slug")]
|
||||
public string Slug { get; set; }
|
||||
public string? Slug { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace RudderSdk.Core.Models;
|
||||
|
||||
public class ErrorResponse
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public string? Code { get; set; }
|
||||
|
||||
[JsonProperty("error")]
|
||||
public string? Error { get; set; }
|
||||
|
||||
[JsonProperty("index")]
|
||||
public int? Index { get; set; }
|
||||
|
||||
[JsonProperty("playerId")]
|
||||
public string? PlayerId { get; set; }
|
||||
|
||||
[JsonProperty("requestId")]
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace RudderSdk.Core.Models;
|
||||
|
||||
public class Reward
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public long? Amount { get; set; }
|
||||
|
||||
[JsonProperty("currency")]
|
||||
public string? Currency { get; set; }
|
||||
|
||||
[JsonProperty("itemId")]
|
||||
public string? ItemId { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
namespace RudderSdk.Core.Models;
|
||||
|
||||
public static class RudderErrorCodes
|
||||
{
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models;
|
||||
|
||||
public class PlanEdge
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty("source")]
|
||||
public string Source { get; set; }
|
||||
|
||||
[JsonProperty("sourceHandle")]
|
||||
public string SourceHandle { get; set; }
|
||||
|
||||
[JsonProperty("target")]
|
||||
public string Target { get; set; }
|
||||
|
||||
[JsonProperty("targetHandle")]
|
||||
public string TargetHandle { get; set; }
|
||||
|
||||
}
|
||||
+10
-8
@@ -1,28 +1,30 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace RudderSdk.Core.Models.Player;
|
||||
|
||||
public class Player
|
||||
{
|
||||
[JsonProperty("avatarUrl")]
|
||||
public string? AvatarUrl { get; set; }
|
||||
|
||||
[JsonProperty("createdAt")]
|
||||
public string CreatedAt { get; set; }
|
||||
public DateTimeOffset? CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
public string? Id { get; set; }
|
||||
|
||||
[JsonProperty("language")]
|
||||
public string Language { get; set; }
|
||||
public string? Language { get; set; }
|
||||
|
||||
[JsonProperty("nickname")]
|
||||
public string Nickname { get; set; }
|
||||
public string? Nickname { get; set; }
|
||||
|
||||
[JsonProperty("projectId")]
|
||||
public string ProjectId { get; set; }
|
||||
public string? ProjectId { get; set; }
|
||||
|
||||
[JsonProperty("region")]
|
||||
public string Region { get; set; }
|
||||
public string? Region { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Player;
|
||||
@@ -8,9 +7,9 @@ namespace RudderSdk.Core.Models.Player;
|
||||
public class PlayerProfile
|
||||
{
|
||||
[JsonProperty("player")]
|
||||
public Player Player { get; set; }
|
||||
public Player? Player { get; set; }
|
||||
|
||||
[JsonProperty("wallets")]
|
||||
public List<Wallet> Wallets { get; set; }
|
||||
public List<Wallet>? Wallets { get; set; }
|
||||
|
||||
}
|
||||
|
||||
+2
-4
@@ -1,16 +1,14 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Player;
|
||||
|
||||
public class Wallet
|
||||
{
|
||||
[JsonProperty("balance")]
|
||||
public long Balance { get; set; }
|
||||
public long? Balance { get; set; }
|
||||
|
||||
[JsonProperty("currency")]
|
||||
public string Currency { get; set; }
|
||||
public string? Currency { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.ProjectStorage;
|
||||
@@ -8,9 +7,9 @@ namespace RudderSdk.Core.Models.ProjectStorage;
|
||||
public class GetProjectStorageResponse
|
||||
{
|
||||
[JsonProperty("items")]
|
||||
public List<ProjectStorageItem> Items { get; set; }
|
||||
public List<ProjectStorageItem>? Items { get; set; }
|
||||
|
||||
[JsonProperty("nextCursor")]
|
||||
public string NextCursor { get; set; }
|
||||
public string? NextCursor { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace RudderSdk.Core.Models.ProjectStorage;
|
||||
|
||||
public class ProjectStorageItem
|
||||
{
|
||||
[JsonProperty("data")]
|
||||
public string Data { get; set; }
|
||||
public string? Data { get; set; }
|
||||
|
||||
[JsonProperty("expiresAt")]
|
||||
public string ExpiresAt { get; set; }
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
public string? Id { get; set; }
|
||||
|
||||
[JsonProperty("readPermission")]
|
||||
public string ReadPermission { get; set; }
|
||||
public string? ReadPermission { get; set; }
|
||||
|
||||
[JsonProperty("size")]
|
||||
public long Size { get; set; }
|
||||
public long? Size { get; set; }
|
||||
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
public string? Type { get; set; }
|
||||
|
||||
[JsonProperty("updatedAt")]
|
||||
public string UpdatedAt { get; set; }
|
||||
public DateTimeOffset? UpdatedAt { get; set; }
|
||||
|
||||
[JsonProperty("version")]
|
||||
public long Version { get; set; }
|
||||
public long? Version { get; set; }
|
||||
|
||||
[JsonProperty("writePermission")]
|
||||
public string WritePermission { get; set; }
|
||||
public string? WritePermission { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.ProjectStorage;
|
||||
|
||||
public class ProjectStorageUpdateItem
|
||||
{
|
||||
[JsonProperty("data")]
|
||||
public string Data { get; set; }
|
||||
public string? Data { get; set; }
|
||||
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
public string? Type { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.ProjectStorage;
|
||||
@@ -8,9 +7,9 @@ namespace RudderSdk.Core.Models.ProjectStorage;
|
||||
public class UpdateProjectStorageRequest
|
||||
{
|
||||
[JsonProperty("idempotencyKey")]
|
||||
public string IdempotencyKey { get; set; }
|
||||
public string? IdempotencyKey { get; set; }
|
||||
|
||||
[JsonProperty("items")]
|
||||
public List<ProjectStorageUpdateItem> Items { get; set; }
|
||||
public List<ProjectStorageUpdateItem>? Items { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Quests;
|
||||
|
||||
public class ClaimQuestRequest
|
||||
{
|
||||
[JsonProperty("questId")]
|
||||
public string QuestId { get; set; }
|
||||
public string? QuestId { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core.Models.Quests;
|
||||
|
||||
public class ClaimQuestResponse
|
||||
{
|
||||
[JsonProperty("alreadyClaimed")]
|
||||
public bool AlreadyClaimed { get; set; }
|
||||
public bool? AlreadyClaimed { get; set; }
|
||||
|
||||
[JsonProperty("error")]
|
||||
public string Error { get; set; }
|
||||
public string? Error { get; set; }
|
||||
|
||||
[JsonProperty("granted")]
|
||||
public List<QuestReward> Granted { get; set; }
|
||||
public List<Reward>? Granted { get; set; }
|
||||
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
public bool? Success { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Quests;
|
||||
|
||||
public class ListQuestsRequest
|
||||
{
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Quests;
|
||||
@@ -8,6 +7,6 @@ namespace RudderSdk.Core.Models.Quests;
|
||||
public class ListQuestsResponse
|
||||
{
|
||||
[JsonProperty("quests")]
|
||||
public List<Quest> Quests { get; set; }
|
||||
public List<Quest>? Quests { get; set; }
|
||||
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,25 +1,25 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core.Models.Quests;
|
||||
|
||||
public class Quest
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
public string? Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonProperty("objectives")]
|
||||
public List<QuestObjectiveProgress> Objectives { get; set; }
|
||||
public List<QuestObjectiveProgress>? Objectives { get; set; }
|
||||
|
||||
[JsonProperty("rewards")]
|
||||
public List<QuestReward> Rewards { get; set; }
|
||||
public List<Reward>? Rewards { get; set; }
|
||||
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
public string? Status { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Quests;
|
||||
|
||||
public class QuestObjectiveProgress
|
||||
{
|
||||
[JsonProperty("completed")]
|
||||
public bool Completed { get; set; }
|
||||
public bool? Completed { get; set; }
|
||||
|
||||
[JsonProperty("current")]
|
||||
public long Current { get; set; }
|
||||
public long? Current { get; set; }
|
||||
|
||||
[JsonProperty("metric")]
|
||||
public string Metric { get; set; }
|
||||
public string? Metric { get; set; }
|
||||
|
||||
[JsonProperty("objectiveId")]
|
||||
public string ObjectiveId { get; set; }
|
||||
public string? ObjectiveId { get; set; }
|
||||
|
||||
[JsonProperty("target")]
|
||||
public long Target { get; set; }
|
||||
public long? Target { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Quests;
|
||||
|
||||
public class QuestReward
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public long Amount { get; set; }
|
||||
|
||||
[JsonProperty("currency")]
|
||||
public string Currency { get; set; }
|
||||
|
||||
[JsonProperty("itemId")]
|
||||
public string ItemId { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Quests;
|
||||
|
||||
public class ReportQuestProgressRequest
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public long Amount { get; set; }
|
||||
public long? Amount { get; set; }
|
||||
|
||||
[JsonProperty("metric")]
|
||||
public string Metric { get; set; }
|
||||
public string? Metric { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Quests;
|
||||
@@ -8,6 +7,6 @@ namespace RudderSdk.Core.Models.Quests;
|
||||
public class ReportQuestProgressResponse
|
||||
{
|
||||
[JsonProperty("completedQuestIds")]
|
||||
public List<string> CompletedQuestIds { get; set; }
|
||||
public List<string>? CompletedQuestIds { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
.NET client SDK for the Rudder LiveOps platform: authentication, player
|
||||
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).
|
||||
- JSON: Newtonsoft.Json.
|
||||
@@ -36,7 +36,7 @@ Console.WriteLine(profile.Player.Id);
|
||||
// Buy an offer (idempotency key is generated when omitted).
|
||||
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);
|
||||
```
|
||||
|
||||
@@ -53,14 +53,38 @@ stack.
|
||||
| `Auth` | `AuthService` | `LoginWithDeviceAsync`, `RefreshAsync`, `Logout`, `AuthStateChanged` |
|
||||
| `Player` | `PlayerService` | `GetProfileAsync` |
|
||||
| `BattlePass` | `BattlePassService` | `GetProgressAsync`, `AddXpAsync`, `ClaimRewardAsync`, `PurchasePremiumAsync` |
|
||||
| `Quests` | `QuestsService` | `ListAsync`, `ClaimAsync` |
|
||||
| `Quests` | `QuestsService` | `ListAsync`, `ClaimAsync`, `ReportProgressAsync` |
|
||||
| `Stores` | `StoresService` | `ListAsync`, `GetAsync`, `PurchaseAsync` |
|
||||
| `Inventory` | `InventoryService` | `GetAsync` |
|
||||
| `Leaderboards` | `LeaderboardsService` | `FindBySlug(slug)` → handle: `SubmitAsync`, `ListAsync` |
|
||||
| `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` |
|
||||
| `Realtime` | `RealtimeService` | `ConnectAsync`, `DisconnectAsync` |
|
||||
|
||||
## Quests
|
||||
|
||||
`client.Quests` covers the player's global quests — list with per-objective
|
||||
progress, claim, and metric reports. These are distinct from scenario quest
|
||||
nodes, which advance through `QuestEffect` (`Effects.OnQuest`).
|
||||
|
||||
```csharp
|
||||
var quests = await client.Quests.ListAsync();
|
||||
foreach (var quest in quests)
|
||||
{
|
||||
if (quest.Status == "completed")
|
||||
await client.Quests.ClaimAsync(quest.Id);
|
||||
}
|
||||
|
||||
// Custom metrics advance matching objectives server-side; the call returns
|
||||
// the ids of quests completed by this report.
|
||||
var completedIds = await client.Quests.ReportProgressAsync("kills", 1);
|
||||
```
|
||||
|
||||
Purchase metrics are reported automatically by store purchases;
|
||||
`QuestMetrics.PurchaseOffer(offerId)` / `QuestMetrics.PurchaseItem(itemId)`
|
||||
name the format (`purchase.offer:<offerId>`, `purchase.item:<itemId>`) so
|
||||
quest configs and client code agree on it.
|
||||
|
||||
## Sessions
|
||||
|
||||
@@ -84,11 +108,18 @@ All of them carry `StatusCode` (`int`), the machine-readable `Code`
|
||||
|
||||
## 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`,
|
||||
`OnWait`, `OnQuest`, `OnBattlePass`, `OnBattlePassLevel`,
|
||||
`OnScenarioCompleted`, `OnScenarioFailed`.
|
||||
`OnNotification`, `OnStoreOffer`, `OnLeaderboard`, `OnWait`, `OnQuest`,
|
||||
`OnBattlePass`, `OnBattlePassLevel`, `OnScenarioCompleted`,
|
||||
`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
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.RemoteConfig;
|
||||
@@ -8,6 +7,6 @@ namespace RudderSdk.Core.Models.RemoteConfig;
|
||||
public class ListRemoteConfigsResponse
|
||||
{
|
||||
[JsonProperty("configs")]
|
||||
public Dictionary<string, RemoteConfig> Configs { get; set; }
|
||||
public Dictionary<string, RemoteConfig>? Configs { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace RudderSdk.Core.Models.RemoteConfig;
|
||||
|
||||
public class RemoteConfig
|
||||
{
|
||||
[JsonProperty("active")]
|
||||
public bool Active { get; set; }
|
||||
public bool? Active { get; set; }
|
||||
|
||||
[JsonProperty("createdAt")]
|
||||
public string CreatedAt { get; set; }
|
||||
public DateTimeOffset? CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; }
|
||||
public string? Description { get; set; }
|
||||
|
||||
[JsonProperty("environment")]
|
||||
public string Environment { get; set; }
|
||||
public string? Environment { get; set; }
|
||||
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
public string? Id { get; set; }
|
||||
|
||||
[JsonProperty("key")]
|
||||
public string Key { get; set; }
|
||||
public string? Key { get; set; }
|
||||
|
||||
[JsonProperty("projectId")]
|
||||
public string ProjectId { get; set; }
|
||||
public string? ProjectId { get; set; }
|
||||
|
||||
[JsonProperty("updatedAt")]
|
||||
public string UpdatedAt { get; set; }
|
||||
public DateTimeOffset? UpdatedAt { get; set; }
|
||||
|
||||
[JsonProperty("value")]
|
||||
public string Value { get; set; }
|
||||
public string? Value { get; set; }
|
||||
|
||||
[JsonProperty("valueType")]
|
||||
public string ValueType { get; set; }
|
||||
public string? ValueType { get; set; }
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,9 +6,9 @@
|
||||
<AssemblyName>Rudder.Core</AssemblyName>
|
||||
<RootNamespace>RudderSdk.Core</RootNamespace>
|
||||
<PackageId>Rudder.Core</PackageId>
|
||||
<Version>0.3.0</Version>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Rudder</Authors>
|
||||
<Description>Rudder LiveOps client SDK for .NET: auth, player, stores, battle pass, quests, leaderboards, inventory, remote config, scenarios, storage and realtime.</Description>
|
||||
<Description>Rudder LiveOps client SDK for .NET: auth, player, stores, battle pass, quests, leaderboards, inventory, remote config, scenarios and storage.</Description>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
+7
-13
@@ -47,11 +47,11 @@ public sealed class RudderClient
|
||||
/// <summary>Global quests.</summary>
|
||||
public QuestsService Quests { get; }
|
||||
|
||||
/// <summary>Scenario runtime: triggers, node sessions, persistence.</summary>
|
||||
/// <summary>Scenario trigger. Execution lives on the server.</summary>
|
||||
public ScenarioService Scenario { get; }
|
||||
|
||||
/// <summary>Realtime websocket channel.</summary>
|
||||
public RealtimeService Realtime { get; }
|
||||
/// <summary>Pending scenario effects: subscriptions and completion callbacks.</summary>
|
||||
public EffectsService Effects { get; }
|
||||
|
||||
internal RudderClientOptions Options { get; }
|
||||
internal IRudderTransport Transport => Options.Transport!;
|
||||
@@ -61,10 +61,7 @@ public sealed class RudderClient
|
||||
/// <summary>Project key from <see cref="RudderClientOptions.ProjectKey"/>.</summary>
|
||||
public string ProjectKey => Options.ProjectKey!;
|
||||
|
||||
/// <summary>Realtime URL from <see cref="RudderClientOptions.RealtimeUrl"/>.</summary>
|
||||
public string? RealtimeUrl => Options.RealtimeUrl;
|
||||
|
||||
/// <summary>Time source used by the scenario runtime.</summary>
|
||||
/// <summary>Time source used by the effects client.</summary>
|
||||
public IClock Clock => Options.Clock ?? SystemClock.Instance;
|
||||
|
||||
/// <summary>
|
||||
@@ -90,17 +87,16 @@ public sealed class RudderClient
|
||||
Stores = new StoresService(this);
|
||||
Leaderboards = new LeaderboardsService(this);
|
||||
Inventory = new InventoryService(this);
|
||||
Scenario = new ScenarioService(this);
|
||||
BattlePass = new BattlePassService(this);
|
||||
Quests = new QuestsService(this);
|
||||
Realtime = new RealtimeService(this);
|
||||
Effects = new EffectsService(this);
|
||||
Scenario = new ScenarioService(this);
|
||||
}
|
||||
|
||||
/// <summary>Pumps time-dependent services; call every frame.</summary>
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
Scenario.Update(deltaTime);
|
||||
Realtime.Update(deltaTime);
|
||||
Effects.Update(deltaTime);
|
||||
}
|
||||
|
||||
internal Task<TResponse> SendAsync<TRequest, TResponse>(
|
||||
@@ -151,7 +147,6 @@ public sealed class RudderClient
|
||||
}
|
||||
catch (RudderAuthException)
|
||||
{
|
||||
// Session rejected — refresh once (single-flight) and retry the call once.
|
||||
if (!await RefreshTokensAsync().ConfigureAwait(false))
|
||||
throw;
|
||||
|
||||
@@ -218,7 +213,6 @@ public sealed class RudderClient
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Transport-level failure during refresh — session is over.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-13
@@ -12,9 +12,6 @@ public sealed class RudderClientOptions
|
||||
/// <summary>API base URL, e.g. https://api.example.com. Required.</summary>
|
||||
public string? BaseUrl { get; set; }
|
||||
|
||||
/// <summary>Realtime websocket URL. Required only for <see cref="RealtimeService"/>.</summary>
|
||||
public string? RealtimeUrl { get; set; }
|
||||
|
||||
/// <summary>Project key issued in the admin panel. Required.</summary>
|
||||
public string? ProjectKey { get; set; }
|
||||
|
||||
@@ -30,15 +27,6 @@ public sealed class RudderClientOptions
|
||||
/// <summary>Diagnostic sink. Null by default (silent).</summary>
|
||||
public IRudderLogger? Logger { get; set; }
|
||||
|
||||
/// <summary>Time source for the scenario runtime; override in tests.</summary>
|
||||
/// <summary>Time source for the effects client; override in tests.</summary>
|
||||
public IClock? Clock { get; set; }
|
||||
|
||||
/// <summary>Scenario-run persistence between app launches. Optional.</summary>
|
||||
public IPlanStateStore? PlanStateStore { get; set; }
|
||||
|
||||
/// <summary>Optional scheduler for delayed plan work.</summary>
|
||||
public IPlanScheduler? Scheduler { get; set; }
|
||||
|
||||
/// <summary>Realtime transport factory. Required only for <see cref="RealtimeService"/>.</summary>
|
||||
public IRealtimeTransportFactory? RealtimeTransportFactory { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
public class GetScenarioRunResponse
|
||||
{
|
||||
[JsonProperty("plan")]
|
||||
public ExecutionPlan Plan { get; set; }
|
||||
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; }
|
||||
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
public class HandleScenarioCallbackRequest
|
||||
{
|
||||
[JsonProperty("handle")]
|
||||
public string Handle { get; set; }
|
||||
public string? Handle { get; set; }
|
||||
|
||||
[JsonProperty("nodeId")]
|
||||
public string NodeId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; }
|
||||
public string? RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string ScenarioId { get; set; }
|
||||
public string? ScenarioId { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
public class HandleScenarioCallbackResponse
|
||||
{
|
||||
[JsonProperty("plan")]
|
||||
public ExecutionPlan Plan { get; set; }
|
||||
[JsonProperty("effect")]
|
||||
public JToken? Effect { get; set; }
|
||||
|
||||
}
|
||||
|
||||
+3
-4
@@ -1,13 +1,12 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
public class GetScenarioRunRequest
|
||||
public class ListPendingScenarioEffectsResponse
|
||||
{
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; }
|
||||
[JsonProperty("effects")]
|
||||
public List<PendingEffect> Effects { get; set; } = null!;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
|
||||
namespace RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
public class PendingEffect
|
||||
{
|
||||
[JsonProperty("data")]
|
||||
public JToken Data { get; set; } = null!;
|
||||
|
||||
[JsonProperty("nodeId")]
|
||||
public string NodeId { get; set; } = null!;
|
||||
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; } = null!;
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string ScenarioId { get; set; } = null!;
|
||||
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; } = null!;
|
||||
|
||||
[JsonProperty("waitDeadline")]
|
||||
public DateTimeOffset? WaitDeadline { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
public class TriggerScenarioRequest
|
||||
{
|
||||
[JsonProperty("event")]
|
||||
public string Event { get; set; }
|
||||
public string? Event { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
public class TriggerScenarioResponse
|
||||
{
|
||||
[JsonProperty("plans")]
|
||||
public List<ExecutionPlan> Plans { get; set; }
|
||||
[JsonProperty("effects")]
|
||||
public List<PendingEffect> Effects { get; set; } = null!;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
public class UpdateScenarioCounterRequest
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public long Amount { get; set; }
|
||||
public long? Amount { get; set; }
|
||||
|
||||
[JsonProperty("counterKey")]
|
||||
public string CounterKey { get; set; }
|
||||
public string? CounterKey { get; set; }
|
||||
|
||||
[JsonProperty("nodeId")]
|
||||
public string NodeId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
|
||||
[JsonProperty("runId")]
|
||||
public string RunId { get; set; }
|
||||
public string? RunId { get; set; }
|
||||
|
||||
[JsonProperty("scenarioId")]
|
||||
public string ScenarioId { get; set; }
|
||||
public string? ScenarioId { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
public class UpdateScenarioCounterResponse
|
||||
{
|
||||
[JsonProperty("completed")]
|
||||
public bool Completed { get; set; }
|
||||
public bool? Completed { get; set; }
|
||||
|
||||
[JsonProperty("plan")]
|
||||
public ExecutionPlan Plan { get; set; }
|
||||
[JsonProperty("effect")]
|
||||
public JToken? Effect { get; set; }
|
||||
|
||||
}
|
||||
|
||||
+34
-1
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using RudderSdk.Core.Models.Auth;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
@@ -41,7 +42,39 @@ public sealed class AuthService
|
||||
DeviceId = _client.DeviceIdProvider.DeviceId,
|
||||
Region = region,
|
||||
Language = language,
|
||||
Nickname = nickname!
|
||||
Nickname = nickname
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response == null || string.IsNullOrEmpty(response.AccessToken) || string.IsNullOrEmpty(response.RefreshToken))
|
||||
throw new InvalidOperationException("The server returned an incomplete session.");
|
||||
|
||||
_client.TokenStore.SaveTokens(response.AccessToken, response.RefreshToken);
|
||||
NotifySignedIn();
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signs the player in through the project's custom authorization webhook.
|
||||
/// The returned token pair is stored in the configured token store.
|
||||
/// </summary>
|
||||
public async Task<LoginViaCustomResponse> LoginWithCustomAsync(
|
||||
JToken customData,
|
||||
string region,
|
||||
string language,
|
||||
string? nickname = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _client.SendAsync<LoginViaCustomRequest, LoginViaCustomResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/authorization/custom",
|
||||
new LoginViaCustomRequest
|
||||
{
|
||||
Key = _client.ProjectKey,
|
||||
CustomData = customData,
|
||||
Region = region,
|
||||
Language = language,
|
||||
Nickname = nickname
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace RudderSdk.Core;
|
||||
/// <summary>
|
||||
/// Battle pass progress and rewards. Battle pass state is tied to a scenario
|
||||
/// battle pass node, so every call carries the scenario/node ids (and a run id
|
||||
/// for the mutating calls) — <see cref="BattlePassSession"/> supplies them
|
||||
/// for the mutating calls) — <see cref="BattlePassEffect"/> supplies them
|
||||
/// during scenario runs.
|
||||
/// </summary>
|
||||
public sealed class BattlePassService
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A scenario battle-pass node. Battle-pass operations are bound to this
|
||||
/// node's scenario/node/run ids; <see cref="LevelUpAsync"/>, <see cref="EndAsync"/>
|
||||
/// and a successful <see cref="PurchasePremiumAsync"/> post callbacks.
|
||||
/// </summary>
|
||||
public sealed class BattlePassEffect
|
||||
{
|
||||
private readonly EffectHandle _handle;
|
||||
private readonly BattlePassService _battlePass;
|
||||
|
||||
internal BattlePassEffect(EffectHandle handle, BattlePassService battlePass)
|
||||
{
|
||||
_handle = handle;
|
||||
_battlePass = battlePass;
|
||||
}
|
||||
|
||||
/// <summary>Run id.</summary>
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
/// <summary>Node data payload.</summary>
|
||||
public JObject Data => _handle.Data;
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => _handle.Get(key, defaultValue);
|
||||
|
||||
/// <summary>Reads current progress (xp, level, premium ownership, claimed tiers) for this node.</summary>
|
||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken cancellationToken = default)
|
||||
=> _battlePass.GetProgressAsync(_handle.ScenarioId, _handle.NodeId, cancellationToken);
|
||||
|
||||
/// <summary>Credits xp from a configured source.</summary>
|
||||
public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken cancellationToken = default)
|
||||
=> _battlePass.AddXpAsync(new AddBattlePassXpRequest
|
||||
{
|
||||
ScenarioId = _handle.ScenarioId,
|
||||
NodeId = _handle.NodeId,
|
||||
RunId = _handle.RunId,
|
||||
Source = source,
|
||||
Amount = amount
|
||||
}, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Claims a tier reward at a reached level.
|
||||
/// <paramref name="track"/> is <see cref="BattlePassService.TrackFree"/> or <see cref="BattlePassService.TrackPremium"/>.
|
||||
/// </summary>
|
||||
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken cancellationToken = default)
|
||||
=> _battlePass.ClaimRewardAsync(new ClaimBattlePassRewardRequest
|
||||
{
|
||||
ScenarioId = _handle.ScenarioId,
|
||||
NodeId = _handle.NodeId,
|
||||
RunId = _handle.RunId,
|
||||
Level = level,
|
||||
Track = track
|
||||
}, cancellationToken);
|
||||
|
||||
/// <summary>Purchases the premium track, then posts <c>onPremiumPurchase</c> on success.</summary>
|
||||
public async Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _battlePass.PurchasePremiumAsync(new PurchaseBattlePassPremiumRequest
|
||||
{
|
||||
ScenarioId = _handle.ScenarioId,
|
||||
NodeId = _handle.NodeId,
|
||||
RunId = _handle.RunId,
|
||||
IdempotencyKey = Guid.NewGuid().ToString()
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response != null && response.Success == true)
|
||||
await _handle.CompleteAsync("onPremiumPurchase", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response!;
|
||||
}
|
||||
|
||||
/// <summary>Posts the <c>onLevelUp</c> callback.</summary>
|
||||
public Task LevelUpAsync(CancellationToken cancellationToken = default) => _handle.CompleteAsync("onLevelUp", cancellationToken);
|
||||
|
||||
/// <summary>Posts the <c>onLevelUp</c> callback (fire-and-forget).</summary>
|
||||
public void LevelUp() => _handle.Complete("onLevelUp");
|
||||
|
||||
/// <summary>Posts the <c>onComplete</c> callback.</summary>
|
||||
public Task EndAsync(CancellationToken cancellationToken = default) => _handle.CompleteAsync("onComplete", cancellationToken);
|
||||
|
||||
/// <summary>Posts the <c>onComplete</c> callback (fire-and-forget).</summary>
|
||||
public void End() => _handle.Complete("onComplete");
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A scenario battle-pass-level node — a single claimable tier.
|
||||
/// <see cref="ClaimAsync"/> posts <c>onComplete</c>, which the server accepts
|
||||
/// only once the player has reached the node's configured level.
|
||||
/// </summary>
|
||||
public sealed class BattlePassLevelEffect
|
||||
{
|
||||
private readonly EffectHandle _handle;
|
||||
|
||||
internal BattlePassLevelEffect(EffectHandle handle) => _handle = handle;
|
||||
|
||||
/// <summary>Run id.</summary>
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
/// <summary>The tier level this node claims.</summary>
|
||||
public int Level => _handle.Get("levelNumber", 0);
|
||||
|
||||
/// <summary>Node data payload.</summary>
|
||||
public JObject Data => _handle.Data;
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => _handle.Get(key, defaultValue);
|
||||
|
||||
/// <summary>Claims this tier; posts <c>onComplete</c>.</summary>
|
||||
public Task ClaimAsync(CancellationToken cancellationToken = default) => _handle.CompleteAsync("onComplete", cancellationToken);
|
||||
|
||||
/// <summary>Claims this tier (fire-and-forget).</summary>
|
||||
public void Claim() => _handle.Complete("onComplete");
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>A scenario leaderboard node. End it with <see cref="EndAsync"/> or claim with <see cref="ClaimAsync"/>.</summary>
|
||||
public sealed class LeaderboardEffect
|
||||
{
|
||||
private readonly EffectHandle _handle;
|
||||
|
||||
internal LeaderboardEffect(EffectHandle handle) => _handle = handle;
|
||||
|
||||
/// <summary>Run id.</summary>
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
/// <summary>Node data payload.</summary>
|
||||
public JObject Data => _handle.Data;
|
||||
|
||||
/// <summary>True after the effect was resolved once.</summary>
|
||||
public bool IsResolved { get; private set; }
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => _handle.Get(key, defaultValue);
|
||||
|
||||
/// <summary>Posts the <c>onEnd</c> callback.</summary>
|
||||
public Task EndAsync(CancellationToken cancellationToken = default) => ResolveAsync("onEnd", cancellationToken);
|
||||
|
||||
/// <summary>Posts the <c>onEnd</c> callback (fire-and-forget).</summary>
|
||||
public void End() => Resolve("onEnd");
|
||||
|
||||
/// <summary>Posts the <c>onClaim</c> callback. The server matches live rank to a place.</summary>
|
||||
public Task ClaimAsync(CancellationToken cancellationToken = default) => ResolveAsync("onClaim", cancellationToken);
|
||||
|
||||
/// <summary>Posts the <c>onClaim</c> callback (fire-and-forget).</summary>
|
||||
public void Claim() => Resolve("onClaim");
|
||||
|
||||
private async Task ResolveAsync(string handle, CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsResolved)
|
||||
return;
|
||||
|
||||
IsResolved = true;
|
||||
await _handle.CompleteAsync(handle, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void Resolve(string handle)
|
||||
{
|
||||
if (IsResolved)
|
||||
return;
|
||||
|
||||
IsResolved = true;
|
||||
_handle.Complete(handle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>A scenario notification node. <see cref="DoneAsync"/> posts the <c>output</c> callback.</summary>
|
||||
public sealed class NotificationEffect
|
||||
{
|
||||
private readonly EffectHandle _handle;
|
||||
|
||||
internal NotificationEffect(EffectHandle handle) => _handle = handle;
|
||||
|
||||
/// <summary>Run id.</summary>
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
/// <summary>Notification title.</summary>
|
||||
public string Title => _handle.Get("title", string.Empty);
|
||||
|
||||
/// <summary>Notification message.</summary>
|
||||
public string Message => _handle.Get("message", string.Empty);
|
||||
|
||||
/// <summary>Node data payload.</summary>
|
||||
public JObject Data => _handle.Data;
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => _handle.Get(key, defaultValue);
|
||||
|
||||
/// <summary>Posts the <c>output</c> callback.</summary>
|
||||
public Task DoneAsync(CancellationToken cancellationToken = default) => _handle.CompleteAsync("output", cancellationToken);
|
||||
|
||||
/// <summary>Posts the <c>output</c> callback (fire-and-forget).</summary>
|
||||
public void Done() => _handle.Complete("output");
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A scenario quest node. Report objective progress; the server auto-completes
|
||||
/// the node once every objective is satisfied.
|
||||
/// </summary>
|
||||
public sealed class QuestEffect
|
||||
{
|
||||
private readonly EffectHandle _handle;
|
||||
|
||||
internal QuestEffect(EffectHandle handle) => _handle = handle;
|
||||
|
||||
/// <summary>Run id.</summary>
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
/// <summary>Quest name from the node data.</summary>
|
||||
public string Name => _handle.Get("name", string.Empty);
|
||||
|
||||
/// <summary>Objective definitions from the node data.</summary>
|
||||
public IReadOnlyList<JObject> Objectives
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_handle.Data["objectives"] is not JArray array)
|
||||
return Array.Empty<JObject>();
|
||||
|
||||
var list = new List<JObject>(array.Count);
|
||||
foreach (var item in array)
|
||||
{
|
||||
if (item is JObject obj)
|
||||
list.Add(obj);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Node data payload.</summary>
|
||||
public JObject Data => _handle.Data;
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => _handle.Get(key, defaultValue);
|
||||
|
||||
/// <summary>Reports progress toward an objective via <c>POST /sdk/v1/scenarios/counter</c>.</summary>
|
||||
public Task ReportProgressAsync(string objectiveId, long amount = 1, CancellationToken cancellationToken = default)
|
||||
=> _handle.ReportProgressAsync(objectiveId, amount, cancellationToken);
|
||||
|
||||
/// <summary>Reports progress toward an objective (fire-and-forget).</summary>
|
||||
public void ReportProgress(string objectiveId, long amount = 1) => _handle.ReportProgress(objectiveId, amount);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>Payload of <see cref="EffectsService.OnScenarioCompleted"/>.</summary>
|
||||
public sealed class ScenarioCompletedEffect
|
||||
{
|
||||
internal ScenarioCompletedEffect(string runId, string scenarioId)
|
||||
{
|
||||
RunId = runId;
|
||||
ScenarioId = scenarioId;
|
||||
}
|
||||
|
||||
/// <summary>Server-issued run id.</summary>
|
||||
public string RunId { get; }
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId { get; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>Payload of <see cref="EffectsService.OnScenarioFailed"/>.</summary>
|
||||
public sealed class ScenarioFailedEffect
|
||||
{
|
||||
internal ScenarioFailedEffect(string runId, string scenarioId, string nodeId, Exception exception)
|
||||
{
|
||||
RunId = runId;
|
||||
ScenarioId = scenarioId;
|
||||
NodeId = nodeId;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
/// <summary>Server-issued run id.</summary>
|
||||
public string RunId { get; }
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId { get; }
|
||||
|
||||
/// <summary>Node the failure happened at.</summary>
|
||||
public string NodeId { get; }
|
||||
|
||||
/// <summary>The error that failed the run.</summary>
|
||||
public Exception Exception { get; }
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>A scenario store-offer node. Resolve it with <see cref="PurchaseAsync"/> or <see cref="DeclineAsync"/>.</summary>
|
||||
public sealed class StoreOfferEffect
|
||||
{
|
||||
private readonly EffectHandle _handle;
|
||||
|
||||
internal StoreOfferEffect(EffectHandle handle) => _handle = handle;
|
||||
|
||||
/// <summary>Run id.</summary>
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
/// <summary>Store slug from the node data, if present.</summary>
|
||||
public string StoreSlug => _handle.Get("storeSlug", string.Empty);
|
||||
|
||||
/// <summary>Optional message from the node data.</summary>
|
||||
public string? Message
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = _handle.Get<string?>("message", null);
|
||||
return string.IsNullOrEmpty(value) ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Node data payload.</summary>
|
||||
public JObject Data => _handle.Data;
|
||||
|
||||
/// <summary>True after the offer was resolved once.</summary>
|
||||
public bool IsResolved { get; private set; }
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => _handle.Get(key, defaultValue);
|
||||
|
||||
/// <summary>Posts the <c>onPurchase</c> callback.</summary>
|
||||
public Task PurchaseAsync(CancellationToken cancellationToken = default) => ResolveAsync("onPurchase", cancellationToken);
|
||||
|
||||
/// <summary>Posts the <c>onPurchase</c> callback (fire-and-forget).</summary>
|
||||
public void Purchase() => Resolve("onPurchase");
|
||||
|
||||
/// <summary>Posts the <c>onDecline</c> callback.</summary>
|
||||
public Task DeclineAsync(CancellationToken cancellationToken = default) => ResolveAsync("onDecline", cancellationToken);
|
||||
|
||||
/// <summary>Posts the <c>onDecline</c> callback (fire-and-forget).</summary>
|
||||
public void Decline() => Resolve("onDecline");
|
||||
|
||||
private async Task ResolveAsync(string handle, CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsResolved)
|
||||
return;
|
||||
|
||||
IsResolved = true;
|
||||
await _handle.CompleteAsync(handle, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void Resolve(string handle)
|
||||
{
|
||||
if (IsResolved)
|
||||
return;
|
||||
|
||||
IsResolved = true;
|
||||
_handle.Complete(handle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>A scenario wait node. The run resumes when the server advances it at <see cref="DeadlineUtc"/>.</summary>
|
||||
public sealed class WaitEffect
|
||||
{
|
||||
private readonly EffectHandle _handle;
|
||||
|
||||
internal WaitEffect(EffectHandle handle, DateTimeOffset deadlineUtc)
|
||||
{
|
||||
_handle = handle;
|
||||
DeadlineUtc = deadlineUtc;
|
||||
}
|
||||
|
||||
/// <summary>Run id.</summary>
|
||||
public string RunId => _handle.RunId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => _handle.ScenarioId;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => _handle.NodeId;
|
||||
|
||||
/// <summary>When the wait ends (UTC).</summary>
|
||||
public DateTimeOffset DeadlineUtc { get; }
|
||||
|
||||
/// <summary>Node data payload.</summary>
|
||||
public JObject Data => _handle.Data;
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => _handle.Get(key, defaultValue);
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using RudderSdk.Core.Abstractions;
|
||||
using RudderSdk.Core.Models;
|
||||
using RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Thin effects client. The server owns scenario execution; this service
|
||||
/// surfaces pending effects and posts callbacks. Drive
|
||||
/// <see cref="RudderClient.Update"/> every frame for the 30s heartbeat and
|
||||
/// wait-deadline checks.
|
||||
/// </summary>
|
||||
public sealed class EffectsService
|
||||
{
|
||||
private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(30);
|
||||
|
||||
private readonly RudderClient _client;
|
||||
private readonly object _gate = new();
|
||||
private readonly HashSet<(string RunId, string NodeId)> _seen = new();
|
||||
private readonly Dictionary<(string RunId, string NodeId), DateTimeOffset> _waitDeadlines = new();
|
||||
|
||||
private bool _refreshDue;
|
||||
private bool _inFlight;
|
||||
private DateTimeOffset _nextHeartbeat;
|
||||
|
||||
/// <summary>Raised for a notification effect.</summary>
|
||||
public event Action<NotificationEffect>? OnNotification;
|
||||
|
||||
/// <summary>Raised for a store-offer effect.</summary>
|
||||
public event Action<StoreOfferEffect>? OnStoreOffer;
|
||||
|
||||
/// <summary>Raised for a leaderboard effect.</summary>
|
||||
public event Action<LeaderboardEffect>? OnLeaderboard;
|
||||
|
||||
/// <summary>Raised for a wait effect.</summary>
|
||||
public event Action<WaitEffect>? OnWait;
|
||||
|
||||
/// <summary>Raised for a quest effect.</summary>
|
||||
public event Action<QuestEffect>? OnQuest;
|
||||
|
||||
/// <summary>Raised for a battle-pass effect.</summary>
|
||||
public event Action<BattlePassEffect>? OnBattlePass;
|
||||
|
||||
/// <summary>Raised for a battle-pass-level effect.</summary>
|
||||
public event Action<BattlePassLevelEffect>? OnBattlePassLevel;
|
||||
|
||||
/// <summary>Raised when a run finishes all its nodes.</summary>
|
||||
public event Action<ScenarioCompletedEffect>? OnScenarioCompleted;
|
||||
|
||||
/// <summary>Raised when a run is dropped after a definitive server rejection or an unsupported effect type.</summary>
|
||||
public event Action<ScenarioFailedEffect>? OnScenarioFailed;
|
||||
|
||||
internal EffectsService(RudderClient client)
|
||||
{
|
||||
_client = client;
|
||||
_nextHeartbeat = _client.Clock.UtcNow + HeartbeatInterval;
|
||||
if (!string.IsNullOrEmpty(_client.TokenStore.GetAccessToken()))
|
||||
_refreshDue = true;
|
||||
|
||||
_client.Auth.AuthStateChanged += OnAuthStateChanged;
|
||||
}
|
||||
|
||||
/// <summary>Pumps heartbeat and wait-deadline checks; call every frame.</summary>
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_client.TokenStore.GetAccessToken()))
|
||||
return;
|
||||
|
||||
var now = _client.Clock.UtcNow;
|
||||
lock (_gate)
|
||||
{
|
||||
if (_inFlight)
|
||||
return;
|
||||
if (!_refreshDue && now < _nextHeartbeat && !HasDueWaitUnlocked(now))
|
||||
return;
|
||||
_refreshDue = false;
|
||||
_inFlight = true;
|
||||
}
|
||||
|
||||
_ = RefreshPendingAsync();
|
||||
}
|
||||
|
||||
internal void Ingest(IEnumerable<PendingEffect>? effects)
|
||||
{
|
||||
if (effects == null)
|
||||
return;
|
||||
|
||||
var batch = new List<PendingEffect>();
|
||||
lock (_gate)
|
||||
{
|
||||
foreach (var effect in effects)
|
||||
{
|
||||
if (effect == null || string.IsNullOrEmpty(effect.RunId) || string.IsNullOrEmpty(effect.NodeId))
|
||||
continue;
|
||||
|
||||
var key = (effect.RunId, effect.NodeId);
|
||||
if (!_seen.Add(key))
|
||||
continue;
|
||||
|
||||
if (string.Equals(effect.Type, EffectTypes.Wait, StringComparison.Ordinal))
|
||||
{
|
||||
var deadline = effect.WaitDeadline ?? _client.Clock.UtcNow;
|
||||
_waitDeadlines[key] = deadline;
|
||||
}
|
||||
|
||||
batch.Add(effect);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var effect in batch)
|
||||
Dispatch(effect);
|
||||
}
|
||||
|
||||
internal async Task CompleteAsync(PendingEffect source, string handle, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.SendAsync<HandleScenarioCallbackRequest, HandleScenarioCallbackResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/scenarios/callback",
|
||||
new HandleScenarioCallbackRequest
|
||||
{
|
||||
ScenarioId = source.ScenarioId,
|
||||
NodeId = source.NodeId,
|
||||
Handle = handle,
|
||||
RunId = source.RunId
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
ForgetWait(source.RunId, source.NodeId);
|
||||
var next = ReadEffect(response?.Effect);
|
||||
if (next == null)
|
||||
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioId));
|
||||
else
|
||||
Ingest(new[] { next });
|
||||
}
|
||||
catch (Exception ex) when (IsDefinitiveRejection(ex))
|
||||
{
|
||||
DropRun(source.RunId, source.ScenarioId, source.NodeId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task ReportProgressAsync(
|
||||
PendingEffect source,
|
||||
string counterKey,
|
||||
long amount,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.SendAsync<UpdateScenarioCounterRequest, UpdateScenarioCounterResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/scenarios/counter",
|
||||
new UpdateScenarioCounterRequest
|
||||
{
|
||||
ScenarioId = source.ScenarioId,
|
||||
NodeId = source.NodeId,
|
||||
CounterKey = counterKey,
|
||||
Amount = amount,
|
||||
RunId = source.RunId
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response == null || response.Completed != true)
|
||||
return;
|
||||
|
||||
ForgetWait(source.RunId, source.NodeId);
|
||||
var next = ReadEffect(response.Effect);
|
||||
if (next == null)
|
||||
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioId));
|
||||
else
|
||||
Ingest(new[] { next });
|
||||
}
|
||||
catch (Exception ex) when (IsDefinitiveRejection(ex))
|
||||
{
|
||||
DropRun(source.RunId, source.ScenarioId, source.NodeId, ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Options.Logger?.Log(
|
||||
RudderLogLevel.Warning,
|
||||
$"[Rudder] Scenario counter update failed at node {source.NodeId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAuthStateChanged(RudderAuthState state)
|
||||
{
|
||||
if (state == RudderAuthState.SignedIn)
|
||||
{
|
||||
lock (_gate)
|
||||
_refreshDue = true;
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_seen.Clear();
|
||||
_waitDeadlines.Clear();
|
||||
_refreshDue = false;
|
||||
_nextHeartbeat = _client.Clock.UtcNow + HeartbeatInterval;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshPendingAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.SendAsync<ListPendingScenarioEffectsResponse>(
|
||||
"GET",
|
||||
"/sdk/v1/scenarios/pending",
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
Ingest(response?.Effects);
|
||||
lock (_gate)
|
||||
_nextHeartbeat = _client.Clock.UtcNow + HeartbeatInterval;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Options.Logger?.Log(
|
||||
RudderLogLevel.Warning,
|
||||
"[Rudder] Failed to refresh pending scenario effects. " + ex.Message);
|
||||
lock (_gate)
|
||||
_nextHeartbeat = _client.Clock.UtcNow + HeartbeatInterval;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_gate)
|
||||
_inFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Dispatch(PendingEffect effect)
|
||||
{
|
||||
var handle = new EffectHandle(this, effect);
|
||||
switch (effect.Type ?? string.Empty)
|
||||
{
|
||||
case EffectTypes.Notification:
|
||||
Emit(OnNotification, new NotificationEffect(handle));
|
||||
break;
|
||||
case EffectTypes.Store:
|
||||
Emit(OnStoreOffer, new StoreOfferEffect(handle));
|
||||
break;
|
||||
case EffectTypes.Leaderboard:
|
||||
Emit(OnLeaderboard, new LeaderboardEffect(handle));
|
||||
break;
|
||||
case EffectTypes.Wait:
|
||||
Emit(OnWait, new WaitEffect(handle, effect.WaitDeadline ?? _client.Clock.UtcNow));
|
||||
break;
|
||||
case EffectTypes.Quest:
|
||||
Emit(OnQuest, new QuestEffect(handle));
|
||||
break;
|
||||
case EffectTypes.BattlePass:
|
||||
Emit(OnBattlePass, new BattlePassEffect(handle, _client.BattlePass));
|
||||
break;
|
||||
case EffectTypes.BattlePassLevel:
|
||||
Emit(OnBattlePassLevel, new BattlePassLevelEffect(handle));
|
||||
break;
|
||||
default:
|
||||
_client.Options.Logger?.Log(
|
||||
RudderLogLevel.Warning,
|
||||
$"[Rudder] Unsupported scenario node type '{effect.Type}' ({effect.NodeId}).");
|
||||
Emit(
|
||||
OnScenarioFailed,
|
||||
new ScenarioFailedEffect(
|
||||
effect.RunId,
|
||||
effect.ScenarioId,
|
||||
effect.NodeId,
|
||||
new Exception($"Unsupported scenario node type '{effect.Type}'")));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DropRun(string runId, string scenarioId, string nodeId, Exception exception)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var toRemove = new List<(string RunId, string NodeId)>();
|
||||
foreach (var key in _seen)
|
||||
{
|
||||
if (key.RunId == runId)
|
||||
toRemove.Add(key);
|
||||
}
|
||||
|
||||
foreach (var key in toRemove)
|
||||
{
|
||||
_seen.Remove(key);
|
||||
_waitDeadlines.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
Emit(OnScenarioFailed, new ScenarioFailedEffect(runId, scenarioId, nodeId, exception));
|
||||
}
|
||||
|
||||
private void ForgetWait(string runId, string nodeId)
|
||||
{
|
||||
lock (_gate)
|
||||
_waitDeadlines.Remove((runId, nodeId));
|
||||
}
|
||||
|
||||
private bool HasDueWaitUnlocked(DateTimeOffset now)
|
||||
{
|
||||
foreach (var deadline in _waitDeadlines.Values)
|
||||
{
|
||||
if (now >= deadline)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Emit<T>(Action<T>? handlers, T effect)
|
||||
{
|
||||
if (handlers == null)
|
||||
return;
|
||||
|
||||
foreach (var subscriber in handlers.GetInvocationList())
|
||||
{
|
||||
try
|
||||
{
|
||||
((Action<T>)subscriber).Invoke(effect);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Options.Logger?.Log(
|
||||
RudderLogLevel.Error,
|
||||
"[Rudder] Effect handler failed. " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PendingEffect? ReadEffect(JToken? token)
|
||||
{
|
||||
if (token == null || token.Type == JTokenType.Null)
|
||||
return null;
|
||||
|
||||
return token.ToObject<PendingEffect>();
|
||||
}
|
||||
|
||||
private static bool IsDefinitiveRejection(Exception ex)
|
||||
{
|
||||
if (ex is RudderNotFoundException)
|
||||
return true;
|
||||
|
||||
return ex is RudderApiException api
|
||||
&& (api.Code == RudderErrorCodes.UnknownRun || api.Code == RudderErrorCodes.RunExpired);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class EffectTypes
|
||||
{
|
||||
public const string Notification = "notification";
|
||||
public const string Store = "store";
|
||||
public const string Leaderboard = "leaderboard";
|
||||
public const string Wait = "wait";
|
||||
public const string Quest = "quest";
|
||||
public const string BattlePass = "battlepass";
|
||||
public const string BattlePassLevel = "battlepass_level";
|
||||
}
|
||||
|
||||
internal sealed class EffectHandle
|
||||
{
|
||||
private readonly EffectsService _service;
|
||||
private readonly PendingEffect _effect;
|
||||
|
||||
public EffectHandle(EffectsService service, PendingEffect effect)
|
||||
{
|
||||
_service = service;
|
||||
_effect = effect;
|
||||
}
|
||||
|
||||
public PendingEffect Source => _effect;
|
||||
|
||||
public string RunId => _effect.RunId;
|
||||
|
||||
public string ScenarioId => _effect.ScenarioId;
|
||||
|
||||
public string NodeId => _effect.NodeId;
|
||||
|
||||
public JObject Data => _effect.Data as JObject ?? new JObject();
|
||||
|
||||
public T Get<T>(string key, T defaultValue = default!)
|
||||
{
|
||||
if (!Data.TryGetValue(key, out var value) || value == null || value.Type == JTokenType.Null)
|
||||
return defaultValue;
|
||||
|
||||
try
|
||||
{
|
||||
return value.ToObject<T>() ?? defaultValue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public Task CompleteAsync(string handle, CancellationToken cancellationToken = default)
|
||||
=> _service.CompleteAsync(_effect, handle, cancellationToken);
|
||||
|
||||
public void Complete(string handle) => _ = CompleteAsync(handle);
|
||||
|
||||
public Task ReportProgressAsync(string counterKey, long amount, CancellationToken cancellationToken = default)
|
||||
=> _service.ReportProgressAsync(_effect, counterKey, amount, cancellationToken);
|
||||
|
||||
public void ReportProgress(string counterKey, long amount) => _ = ReportProgressAsync(counterKey, amount);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Builders for the quest metric strings known to the platform. Purchase
|
||||
/// metrics are reported automatically by the shop purchase fan-out; any other
|
||||
/// metric is a custom string reported via
|
||||
/// <see cref="QuestsService.ReportProgressAsync"/>.
|
||||
/// </summary>
|
||||
public static class QuestMetrics
|
||||
{
|
||||
/// <summary>Metric for purchasing a store offer; auto-reported on purchase.</summary>
|
||||
public static string PurchaseOffer(string offerId) => $"purchase.offer:{offerId}";
|
||||
|
||||
/// <summary>Metric for purchasing a catalog item; auto-reported on purchase.</summary>
|
||||
public static string PurchaseItem(string itemId) => $"purchase.item:{itemId}";
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Global quests (list + claim), distinct from scenario quest nodes which
|
||||
/// advance through <see cref="QuestSession"/>.
|
||||
/// advance through <see cref="QuestEffect"/>.
|
||||
/// </summary>
|
||||
public sealed class QuestsService
|
||||
{
|
||||
@@ -18,10 +18,9 @@ public sealed class QuestsService
|
||||
/// <summary>Lists the player's quests with per-objective progress and rewards.</summary>
|
||||
public async Task<IReadOnlyList<Quest>> ListAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _client.SendAsync<ListQuestsRequest, ListQuestsResponse>(
|
||||
var response = await _client.SendAsync<ListQuestsResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/quests/list",
|
||||
new ListQuestsRequest(),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response?.Quests ?? new List<Quest>();
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RudderSdk.Core.Abstractions;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Realtime websocket channel. Requires
|
||||
/// <see cref="RudderClientOptions.RealtimeUrl"/> and
|
||||
/// <see cref="RudderClientOptions.RealtimeTransportFactory"/>.
|
||||
/// </summary>
|
||||
public sealed class RealtimeService
|
||||
{
|
||||
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
private readonly RudderClient _client;
|
||||
private RealtimeSession? _session;
|
||||
|
||||
internal RealtimeService(RudderClient client) => _client = client;
|
||||
|
||||
/// <summary>The current session, or null when not connected.</summary>
|
||||
public RealtimeSession? Session => _session;
|
||||
|
||||
/// <summary>True while a session is connected.</summary>
|
||||
public bool IsConnected => _session?.IsConnected == true;
|
||||
|
||||
/// <summary>Connects to the configured realtime URL.</summary>
|
||||
public Task<RealtimeSession> ConnectAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_client.RealtimeUrl))
|
||||
throw new InvalidOperationException("RealtimeUrl is not configured.");
|
||||
|
||||
return ConnectAsync(new Uri(_client.RealtimeUrl), timeout, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Connects to an explicit realtime URL.</summary>
|
||||
public async Task<RealtimeSession> ConnectAsync(Uri uri, TimeSpan? timeout = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var factory = _client.Options.RealtimeTransportFactory
|
||||
?? throw new InvalidOperationException("RealtimeTransportFactory is not configured.");
|
||||
|
||||
var token = _client.TokenStore.GetAccessToken();
|
||||
if (string.IsNullOrEmpty(token))
|
||||
throw new InvalidOperationException("LiveOps access token is required for realtime authorization.");
|
||||
|
||||
var transport = factory.Create();
|
||||
await transport.ConnectAsync(uri, timeout ?? DefaultTimeout, cancellationToken).ConfigureAwait(false);
|
||||
_session = new RealtimeSession(transport, token);
|
||||
return _session;
|
||||
}
|
||||
|
||||
/// <summary>Closes the current session, if any.</summary>
|
||||
public Task DisconnectAsync(CancellationToken cancellationToken = default)
|
||||
=> _session?.DisconnectAsync(cancellationToken) ?? Task.CompletedTask;
|
||||
|
||||
/// <summary>Pumps the underlying transport; call every frame.</summary>
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
_session?.Update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>An open realtime connection.</summary>
|
||||
public sealed class RealtimeSession
|
||||
{
|
||||
private readonly IRealtimeTransport _transport;
|
||||
|
||||
internal RealtimeSession(IRealtimeTransport transport, string accessToken)
|
||||
{
|
||||
_transport = transport;
|
||||
AccessToken = accessToken;
|
||||
_transport.Closed += () => Closed?.Invoke();
|
||||
_transport.Error += ex => Error?.Invoke(ex);
|
||||
_transport.Received += data => MessageReceived?.Invoke(data);
|
||||
}
|
||||
|
||||
/// <summary>Access token the connection was authorized with.</summary>
|
||||
public string AccessToken { get; }
|
||||
|
||||
/// <summary>True while the connection is open.</summary>
|
||||
public bool IsConnected => _transport.IsConnected;
|
||||
|
||||
/// <summary>Raised when the connection closes.</summary>
|
||||
public event Action? Closed;
|
||||
|
||||
/// <summary>Raised on transport errors.</summary>
|
||||
public event Action<Exception>? Error;
|
||||
|
||||
/// <summary>Raised for every incoming message.</summary>
|
||||
public event Action<ArraySegment<byte>>? MessageReceived;
|
||||
|
||||
/// <summary>Sends one message.</summary>
|
||||
public Task SendAsync(byte[] payload, CancellationToken cancellationToken = default)
|
||||
=> _transport.SendAsync(new ArraySegment<byte>(payload ?? Array.Empty<byte>()), cancellationToken);
|
||||
|
||||
/// <summary>Closes the connection.</summary>
|
||||
public Task DisconnectAsync(CancellationToken cancellationToken = default)
|
||||
=> _transport.CloseAsync(cancellationToken);
|
||||
|
||||
internal void Update(float deltaTime)
|
||||
{
|
||||
_transport.Update(deltaTime);
|
||||
}
|
||||
}
|
||||
+7
-218
@@ -1,71 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using RudderSdk.Core.Abstractions;
|
||||
using RudderSdk.Core.Models;
|
||||
using RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Scenario runtime. <see cref="TriggerAsync"/> starts server-issued plans;
|
||||
/// active nodes surface as typed sessions through the On* events and are
|
||||
/// advanced by completing those sessions.
|
||||
/// Scenario trigger. Execution lives on the server; resulting effects surface
|
||||
/// through <see cref="RudderClient.Effects"/>.
|
||||
/// </summary>
|
||||
public sealed partial class ScenarioService
|
||||
public sealed class ScenarioService
|
||||
{
|
||||
private readonly RudderClient _client;
|
||||
private readonly Dictionary<string, RuntimeRun> _runs = new();
|
||||
|
||||
/// <summary>Raised for a notification node.</summary>
|
||||
public event Action<NotificationSession>? OnNotification;
|
||||
|
||||
/// <summary>Raised for a store-offer node.</summary>
|
||||
public event Action<StoreOfferSession>? OnStoreOffer;
|
||||
|
||||
/// <summary>Raised for a leaderboard node.</summary>
|
||||
public event Action<LeaderboardSession>? OnLeaderboard;
|
||||
|
||||
/// <summary>Raised for a remote-config-override node, after the patches were applied.</summary>
|
||||
public event Action<ConfigChangedSession>? OnConfigChanged;
|
||||
|
||||
/// <summary>Raised for a wait node.</summary>
|
||||
public event Action<WaitSession>? OnWait;
|
||||
|
||||
/// <summary>Raised for a quest node.</summary>
|
||||
public event Action<QuestSession>? OnQuest;
|
||||
|
||||
/// <summary>Raised for a battle-pass node.</summary>
|
||||
public event Action<BattlePassSession>? OnBattlePass;
|
||||
|
||||
/// <summary>Raised for a battle-pass-level node.</summary>
|
||||
public event Action<BattlePassLevelSession>? OnBattlePassLevel;
|
||||
|
||||
/// <summary>Raised when a run finishes all its nodes.</summary>
|
||||
public event Action<PlanRun>? OnScenarioCompleted;
|
||||
|
||||
/// <summary>Raised when a run dies on an unrecoverable error.</summary>
|
||||
public event Action<ScenarioFailedEvent>? OnScenarioFailed;
|
||||
|
||||
internal ScenarioService(RudderClient client) => _client = client;
|
||||
|
||||
/// <summary>True while at least one run is active.</summary>
|
||||
public bool IsRunning => _runs.Count > 0;
|
||||
|
||||
/// <summary>First active node id across the runs, or null.</summary>
|
||||
public string? CurrentNodeId => _runs.Values.FirstOrDefault()?.ActiveNodes.Keys.FirstOrDefault();
|
||||
|
||||
/// <summary>Snapshots of the active runs.</summary>
|
||||
public IReadOnlyList<PlanRun> ActiveRuns => _runs.Values.Select(ToPlanRun).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Triggers scenarios by event name and starts the plans the server
|
||||
/// returns. Returns the runs this call started.
|
||||
/// Triggers scenarios by event name. Returned pending effects are ingested
|
||||
/// into <see cref="RudderClient.Effects"/>.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<PlanRun>> TriggerAsync(string eventName, CancellationToken cancellationToken = default)
|
||||
public async Task TriggerAsync(string eventName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _client.SendAsync<TriggerScenarioRequest, TriggerScenarioResponse>(
|
||||
"POST",
|
||||
@@ -73,170 +26,6 @@ public sealed partial class ScenarioService
|
||||
new TriggerScenarioRequest { Event = eventName },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return StartPlans(response?.Plans);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores persisted runs, reconciles them with the server and re-dispatches
|
||||
/// active nodes. Call once after startup, after login.
|
||||
/// </summary>
|
||||
public Task RestoreAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return RestoreCoreAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task RestoreCoreAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var store = _client.Options.PlanStateStore;
|
||||
if (store == null || string.IsNullOrEmpty(store.State))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var state = JsonConvert.DeserializeObject<PersistedScenarioState>(store.State);
|
||||
_runs.Clear();
|
||||
if (state?.Runs != null)
|
||||
{
|
||||
foreach (var savedRun in state.Runs)
|
||||
{
|
||||
if (savedRun?.Plan == null)
|
||||
continue;
|
||||
|
||||
// Reconcile with server
|
||||
try
|
||||
{
|
||||
var response = await _client.SendAsync<GetScenarioRunRequest, GetScenarioRunResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/scenarios/run",
|
||||
new GetScenarioRunRequest { RunId = savedRun.RunId },
|
||||
cancellationToken);
|
||||
|
||||
if (response?.Status == "unknown_run" || response?.Status == "expired")
|
||||
continue;
|
||||
|
||||
if (response?.Plan != null)
|
||||
{
|
||||
savedRun.Plan = response.Plan;
|
||||
savedRun.ActiveNodes = null;
|
||||
savedRun.CompletedHandles = null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Network error — keep local state as fallback
|
||||
}
|
||||
|
||||
var run = RuntimeRun.FromPersisted(savedRun);
|
||||
_runs[run.RunId] = run;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var run in _runs.Values.ToList())
|
||||
{
|
||||
if (run.ActiveNodes.Count > 0)
|
||||
{
|
||||
foreach (var nodeState in run.ActiveNodes.Values.ToList())
|
||||
DispatchActiveNode(run, nodeState, restored: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rebuilt from server — activate start node
|
||||
var startNode = FindNode(run.Plan, run.Plan.StartNodeId) ?? run.Plan.Nodes[0];
|
||||
ActivateNode(run, startNode.Id);
|
||||
}
|
||||
}
|
||||
|
||||
Persist();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_client.Options.Logger?.Log(RudderLogLevel.Error, "[Rudder] Failed to restore scenario state. Clearing persisted state. " + ex.Message);
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Drops all runs and the persisted state.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
_runs.Clear();
|
||||
Persist();
|
||||
}
|
||||
|
||||
/// <summary>Completes wait nodes whose deadline passed; call every frame.</summary>
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
var now = _client.Clock.UtcNow;
|
||||
foreach (var run in _runs.Values.ToList())
|
||||
{
|
||||
foreach (var node in run.ActiveNodes.Values.ToList())
|
||||
{
|
||||
if (node.WaitDeadlineUtc.HasValue && now >= node.WaitDeadlineUtc.Value)
|
||||
_ = CompleteNodeAsync(run.RunId, node.NodeId, "onComplete");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Completes the first active node with the given handle.</summary>
|
||||
public Task RespondAsync(string handle, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var run = _runs.Values.FirstOrDefault();
|
||||
var node = run?.ActiveNodes.Values.FirstOrDefault();
|
||||
return run == null || node == null
|
||||
? Task.CompletedTask
|
||||
: CompleteNodeAsync(run.RunId, node.NodeId, handle, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Completes the first active node with the given handle (fire-and-forget).</summary>
|
||||
public void Respond(string handle)
|
||||
{
|
||||
_ = RespondAsync(handle);
|
||||
}
|
||||
|
||||
/// <summary>Adds progress to a counter of the first active node.</summary>
|
||||
public Task UpdateProgressAsync(string counterKey, long amount, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var run = _runs.Values.FirstOrDefault();
|
||||
var node = run?.ActiveNodes.Values.FirstOrDefault();
|
||||
return run == null || node == null
|
||||
? Task.CompletedTask
|
||||
: UpdateProgressAsync(run.RunId, node.NodeId, counterKey, amount, cancellationToken);
|
||||
}
|
||||
|
||||
internal async Task UpdateProgressAsync(
|
||||
string runId,
|
||||
string nodeId,
|
||||
string counterKey,
|
||||
long amount,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_runs.TryGetValue(runId, out var run) || !run.ActiveNodes.ContainsKey(nodeId))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _client.SendAsync<UpdateScenarioCounterRequest, UpdateScenarioCounterResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/scenarios/counter",
|
||||
new UpdateScenarioCounterRequest
|
||||
{
|
||||
ScenarioId = run.Plan.ScenarioId,
|
||||
NodeId = nodeId,
|
||||
CounterKey = counterKey,
|
||||
Amount = amount,
|
||||
RunId = run.RunId
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// The server reports objective completion; it no longer returns a plan from the
|
||||
// counter endpoint. On completion, cross the node's onComplete handle (which
|
||||
// advances the run) — idempotent if the consumer also completes the session.
|
||||
if (response != null && response.Completed)
|
||||
await CompleteNodeAsync(runId, nodeId, "onComplete", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Counter update failure does not fail the run.
|
||||
_client.Options.Logger?.Log(RudderLogLevel.Warning, $"[Rudder] Scenario counter update failed at node {nodeId}: {ex.Message}");
|
||||
}
|
||||
_client.Effects.Ingest(response?.Effects);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>Snapshot of one running scenario plan.</summary>
|
||||
public sealed class PlanRun
|
||||
{
|
||||
internal PlanRun(
|
||||
string runId,
|
||||
string planId,
|
||||
string scenarioId,
|
||||
string userId,
|
||||
IReadOnlyList<string> activeNodeIds,
|
||||
ExecutionPlan plan)
|
||||
{
|
||||
RunId = runId;
|
||||
PlanId = planId;
|
||||
ScenarioId = scenarioId;
|
||||
UserId = userId;
|
||||
ActiveNodeIds = activeNodeIds;
|
||||
Plan = plan;
|
||||
}
|
||||
|
||||
/// <summary>Server-issued run id.</summary>
|
||||
public string RunId { get; }
|
||||
|
||||
/// <summary>Plan id.</summary>
|
||||
public string PlanId { get; }
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId { get; }
|
||||
|
||||
/// <summary>Player the run belongs to.</summary>
|
||||
public string UserId { get; }
|
||||
|
||||
/// <summary>Ids of the currently active nodes.</summary>
|
||||
public IReadOnlyList<string> ActiveNodeIds { get; }
|
||||
|
||||
/// <summary>The execution plan being run.</summary>
|
||||
public ExecutionPlan Plan { get; }
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>Payload of <see cref="ScenarioService.OnScenarioFailed"/>.</summary>
|
||||
public sealed class ScenarioFailedEvent
|
||||
{
|
||||
internal ScenarioFailedEvent(PlanRun run, string nodeId, Exception exception)
|
||||
{
|
||||
Run = run;
|
||||
NodeId = nodeId;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
/// <summary>The failed run.</summary>
|
||||
public PlanRun Run { get; }
|
||||
|
||||
/// <summary>Node the failure happened at.</summary>
|
||||
public string NodeId { get; }
|
||||
|
||||
/// <summary>The error that failed the run.</summary>
|
||||
public Exception Exception { get; }
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>Context of the scenario node a session was created for.</summary>
|
||||
public class ScenarioNodeContext
|
||||
{
|
||||
private readonly ScenarioService _service;
|
||||
|
||||
internal ScenarioNodeContext(ScenarioService service, PlanRun run, ExecutionPlanNode node)
|
||||
{
|
||||
_service = service;
|
||||
Run = run;
|
||||
Node = node;
|
||||
Data = node?.Data as JObject ?? new JObject();
|
||||
}
|
||||
|
||||
/// <summary>The run this node belongs to.</summary>
|
||||
public PlanRun Run { get; }
|
||||
|
||||
/// <summary>The plan node.</summary>
|
||||
public ExecutionPlanNode Node { get; }
|
||||
|
||||
/// <summary>Run id.</summary>
|
||||
public string RunId => Run.RunId;
|
||||
|
||||
/// <summary>Plan id.</summary>
|
||||
public string PlanId => Run.PlanId;
|
||||
|
||||
/// <summary>Scenario id.</summary>
|
||||
public string ScenarioId => Run.ScenarioId;
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string NodeId => Node.Id;
|
||||
|
||||
/// <summary>Node type.</summary>
|
||||
public string Type => Node.Type;
|
||||
|
||||
/// <summary>Node data payload.</summary>
|
||||
public JObject Data { get; }
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!)
|
||||
{
|
||||
if (Data == null || !Data.TryGetValue(key, out var value))
|
||||
return defaultValue;
|
||||
try { return value.ToObject<T>() ?? defaultValue; }
|
||||
catch { return defaultValue; }
|
||||
}
|
||||
|
||||
/// <summary>Deserializes the whole node data payload.</summary>
|
||||
public T Get<T>()
|
||||
{
|
||||
try { return Data == null ? default! : Data.ToObject<T>() ?? default!; }
|
||||
catch { return default!; }
|
||||
}
|
||||
|
||||
/// <summary>Returns the node data as a plain dictionary.</summary>
|
||||
public Dictionary<string, object> AsObjectDictionary()
|
||||
{
|
||||
return Data?.ToObject<Dictionary<string, object>>() ?? new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
internal Task CompleteAsync(string handle, CancellationToken cancellationToken = default)
|
||||
=> _service.CompleteNodeAsync(RunId, NodeId, handle, cancellationToken);
|
||||
|
||||
internal void Complete(string handle)
|
||||
{
|
||||
_ = CompleteAsync(handle);
|
||||
}
|
||||
|
||||
internal Task AddProgressAsync(string counterKey, long amount, CancellationToken cancellationToken = default)
|
||||
=> _service.UpdateProgressAsync(RunId, NodeId, counterKey, amount, cancellationToken);
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using RudderSdk.Core.Abstractions;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
public sealed partial class ScenarioService
|
||||
{
|
||||
private void DispatchActiveNode(RuntimeRun run, ActiveNodeState state, bool restored)
|
||||
{
|
||||
var node = FindNode(run.Plan, state.NodeId);
|
||||
if (node == null)
|
||||
{
|
||||
run.ActiveNodes.Remove(state.NodeId);
|
||||
CheckRunCompleted(run);
|
||||
return;
|
||||
}
|
||||
|
||||
var context = new ScenarioNodeContext(this, ToPlanRun(run), node);
|
||||
switch (node.Type ?? string.Empty)
|
||||
{
|
||||
case WaitNode:
|
||||
DispatchWait(run, state, context);
|
||||
break;
|
||||
case RemoteConfigOverrideNode:
|
||||
DispatchRemoteConfigOverride(run, state, context);
|
||||
break;
|
||||
case NotificationNode:
|
||||
EmitNotification(context);
|
||||
break;
|
||||
case StoreNode:
|
||||
EmitStoreOffer(context);
|
||||
break;
|
||||
case QuestNode:
|
||||
EmitQuest(context);
|
||||
break;
|
||||
case LeaderboardNode:
|
||||
EmitLeaderboard(context);
|
||||
break;
|
||||
case BattlePassNode:
|
||||
EmitBattlePass(context);
|
||||
break;
|
||||
case BattlePassLevelNode:
|
||||
EmitBattlePassLevel(context);
|
||||
break;
|
||||
default:
|
||||
// Unsupported node type — fail the run (surfaced via OnScenarioFailed)
|
||||
// instead of leaving it stalled on a node no handler will complete.
|
||||
_client.Options.Logger?.Log(RudderLogLevel.Warning, $"[Rudder] Unsupported scenario node type '{node.Type}' ({node.Id}).");
|
||||
FailRun(run, state.NodeId, new Exception($"Unsupported scenario node type '{node.Type}'"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DispatchWait(RuntimeRun run, ActiveNodeState state, ScenarioNodeContext context)
|
||||
{
|
||||
if (!state.WaitDeadlineUtc.HasValue)
|
||||
{
|
||||
// Prefer server-provided WaitDeadline from the plan boundary over local calculation.
|
||||
// The server stamps WaitDeadline on server-enforced wait boundaries (see StampBoundaries).
|
||||
var boundary = run.Plan.BoundaryNodes?.FirstOrDefault(
|
||||
b => b.SourceNodeId == state.NodeId && !string.IsNullOrEmpty(b.WaitDeadline)
|
||||
);
|
||||
if (boundary != null && DateTimeOffset.TryParse(boundary.WaitDeadline, null, DateTimeStyles.RoundtripKind, out var parsed))
|
||||
{
|
||||
state.WaitDeadlineUtc = parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
var delay = GetWaitDelay(context.Data);
|
||||
state.WaitDeadlineUtc = _client.Clock.UtcNow.Add(delay);
|
||||
}
|
||||
Persist();
|
||||
}
|
||||
|
||||
var session = new WaitSession(context, state.WaitDeadlineUtc.Value);
|
||||
OnWait?.Invoke(session);
|
||||
|
||||
if (_client.Clock.UtcNow >= state.WaitDeadlineUtc.Value)
|
||||
_ = CompleteNodeAsync(run.RunId, state.NodeId, "onComplete");
|
||||
}
|
||||
|
||||
private void DispatchRemoteConfigOverride(RuntimeRun run, ActiveNodeState state, ScenarioNodeContext context)
|
||||
{
|
||||
var patches = context.Data["patches"] as JArray;
|
||||
if (patches != null)
|
||||
{
|
||||
foreach (var patchToken in patches.OfType<JObject>())
|
||||
{
|
||||
var key = patchToken.Value<string>("path");
|
||||
if (string.IsNullOrEmpty(key))
|
||||
continue;
|
||||
|
||||
var valueType = patchToken.Value<string>("valueType") ?? "json";
|
||||
var value = SerializeRemoteConfigValue(patchToken["value"], valueType);
|
||||
_client.RemoteConfig.ApplyOverride(key, value, valueType);
|
||||
}
|
||||
}
|
||||
|
||||
var session = new ConfigChangedSession(context);
|
||||
OnConfigChanged?.Invoke(session);
|
||||
_ = CompleteNodeAsync(run.RunId, state.NodeId, "output");
|
||||
}
|
||||
|
||||
private void EmitNotification(ScenarioNodeContext context)
|
||||
{
|
||||
OnNotification?.Invoke(new NotificationSession(context));
|
||||
}
|
||||
|
||||
private void EmitStoreOffer(ScenarioNodeContext context)
|
||||
{
|
||||
OnStoreOffer?.Invoke(new StoreOfferSession(context));
|
||||
}
|
||||
|
||||
private void EmitQuest(ScenarioNodeContext context)
|
||||
{
|
||||
OnQuest?.Invoke(new QuestSession(context));
|
||||
}
|
||||
|
||||
private void EmitLeaderboard(ScenarioNodeContext context)
|
||||
{
|
||||
OnLeaderboard?.Invoke(new LeaderboardSession(context));
|
||||
}
|
||||
|
||||
private void EmitBattlePass(ScenarioNodeContext context)
|
||||
{
|
||||
OnBattlePass?.Invoke(new BattlePassSession(context, _client.BattlePass));
|
||||
}
|
||||
|
||||
private void EmitBattlePassLevel(ScenarioNodeContext context)
|
||||
{
|
||||
OnBattlePassLevel?.Invoke(new BattlePassLevelSession(context));
|
||||
}
|
||||
|
||||
private static TimeSpan GetWaitDelay(JObject data)
|
||||
{
|
||||
var duration = data.Value<double?>("duration") ?? 0;
|
||||
var unit = data.Value<string>("unit") ?? "seconds";
|
||||
if (duration <= 0)
|
||||
return TimeSpan.Zero;
|
||||
|
||||
switch (unit)
|
||||
{
|
||||
case "days":
|
||||
case "day":
|
||||
case "d":
|
||||
return TimeSpan.FromDays(duration);
|
||||
case "hours":
|
||||
case "hour":
|
||||
case "hr":
|
||||
case "h":
|
||||
return TimeSpan.FromHours(duration);
|
||||
case "minutes":
|
||||
case "minute":
|
||||
case "min":
|
||||
case "m":
|
||||
return TimeSpan.FromMinutes(duration);
|
||||
case "seconds":
|
||||
case "second":
|
||||
case "sec":
|
||||
case "s":
|
||||
return TimeSpan.FromSeconds(duration);
|
||||
default:
|
||||
return TimeSpan.FromSeconds(duration);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? SerializeRemoteConfigValue(JToken? token, string valueType)
|
||||
{
|
||||
if (token == null || token.Type == JTokenType.Null)
|
||||
return null;
|
||||
|
||||
switch ((valueType ?? string.Empty).ToLowerInvariant())
|
||||
{
|
||||
case "string":
|
||||
return token.Type == JTokenType.String ? token.Value<string>() : token.ToString(Formatting.None);
|
||||
case "bool":
|
||||
case "boolean":
|
||||
return token.Value<bool>().ToString().ToLowerInvariant();
|
||||
case "int":
|
||||
case "integer":
|
||||
return token.Value<long>().ToString(CultureInfo.InvariantCulture);
|
||||
case "float":
|
||||
case "double":
|
||||
return token.Value<double>().ToString(CultureInfo.InvariantCulture);
|
||||
default:
|
||||
return token.Type == JTokenType.String ? token.Value<string>() : token.ToString(Formatting.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
public sealed partial class ScenarioService
|
||||
{
|
||||
private const string NotificationNode = "notification";
|
||||
private const string StoreNode = "store";
|
||||
private const string WaitNode = "wait";
|
||||
private const string RemoteConfigOverrideNode = "remote_config_override";
|
||||
private const string QuestNode = "quest";
|
||||
private const string LeaderboardNode = "leaderboard";
|
||||
private const string BattlePassNode = "battlepass";
|
||||
private const string BattlePassLevelNode = "battlepass_level";
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using RudderSdk.Core.Models;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
public sealed partial class ScenarioService
|
||||
{
|
||||
private void Persist()
|
||||
{
|
||||
var store = _client.Options.PlanStateStore;
|
||||
if (store == null)
|
||||
return;
|
||||
|
||||
if (_runs.Count == 0)
|
||||
{
|
||||
store.State = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var state = new PersistedScenarioState
|
||||
{
|
||||
Runs = _runs.Values.Select(run => run.ToPersisted()).ToList()
|
||||
};
|
||||
store.State = JsonConvert.SerializeObject(state);
|
||||
}
|
||||
|
||||
private sealed class RuntimeRun
|
||||
{
|
||||
public RuntimeRun(string runId, ExecutionPlan plan)
|
||||
{
|
||||
RunId = runId;
|
||||
Plan = plan;
|
||||
}
|
||||
|
||||
public string RunId { get; }
|
||||
public ExecutionPlan Plan { get; }
|
||||
public Dictionary<string, ActiveNodeState> ActiveNodes { get; } = new();
|
||||
public HashSet<string> CompletedHandles { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
public PersistedRun ToPersisted()
|
||||
{
|
||||
return new PersistedRun
|
||||
{
|
||||
RunId = RunId,
|
||||
Plan = Plan,
|
||||
ActiveNodes = ActiveNodes.Values.ToList(),
|
||||
CompletedHandles = CompletedHandles.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public static RuntimeRun FromPersisted(PersistedRun saved)
|
||||
{
|
||||
var run = new RuntimeRun(saved.RunId, saved.Plan);
|
||||
if (saved.ActiveNodes != null)
|
||||
{
|
||||
foreach (var node in saved.ActiveNodes)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(node?.NodeId))
|
||||
run.ActiveNodes[node.NodeId] = node;
|
||||
}
|
||||
}
|
||||
if (saved.CompletedHandles != null)
|
||||
{
|
||||
foreach (var handle in saved.CompletedHandles)
|
||||
run.CompletedHandles.Add(handle);
|
||||
}
|
||||
return run;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PersistedScenarioState
|
||||
{
|
||||
public List<PersistedRun>? Runs { get; set; }
|
||||
}
|
||||
|
||||
private sealed class PersistedRun
|
||||
{
|
||||
public string RunId { get; set; } = string.Empty;
|
||||
public ExecutionPlan Plan { get; set; } = null!;
|
||||
public List<ActiveNodeState>? ActiveNodes { get; set; }
|
||||
public List<string>? CompletedHandles { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ActiveNodeState
|
||||
{
|
||||
public string NodeId { get; set; } = string.Empty;
|
||||
public DateTimeOffset? WaitDeadlineUtc { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RudderSdk.Core.Abstractions;
|
||||
using RudderSdk.Core.Models;
|
||||
using RudderSdk.Core.Models.Scenarios;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when a boundary HTTP call fails with a transient error
|
||||
/// (network failure, timeout, or server 5xx). The caller should NOT advance
|
||||
/// the run; the node stays active and the handle stays pending for retry.
|
||||
/// </summary>
|
||||
public sealed class TransientBoundaryException : Exception
|
||||
{
|
||||
/// <summary>Creates the exception.</summary>
|
||||
public TransientBoundaryException(string message, Exception inner)
|
||||
: base(message, inner)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Creates the exception wrapping the transport error.</summary>
|
||||
public TransientBoundaryException(Exception inner)
|
||||
: base($"Transient boundary error: {inner.Message}", inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class ScenarioService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true for exceptions that may succeed on retry
|
||||
/// (timeout / network failures).
|
||||
/// </summary>
|
||||
private static bool IsTransientException(Exception ex)
|
||||
{
|
||||
return ex is OperationCanceledException || ex is RudderNetworkException;
|
||||
}
|
||||
|
||||
private IReadOnlyList<PlanRun> StartPlans(IEnumerable<ExecutionPlan>? plans)
|
||||
{
|
||||
var started = new List<PlanRun>();
|
||||
if (plans == null)
|
||||
return started;
|
||||
|
||||
foreach (var plan in plans)
|
||||
{
|
||||
var run = StartPlan(plan);
|
||||
if (run != null)
|
||||
started.Add(ToPlanRun(run));
|
||||
}
|
||||
|
||||
return started;
|
||||
}
|
||||
|
||||
private RuntimeRun? StartPlan(ExecutionPlan? plan)
|
||||
{
|
||||
if (plan?.Nodes == null || plan.Nodes.Count == 0)
|
||||
return null;
|
||||
|
||||
// Dedup: server returned same runId — skip without restarting.
|
||||
if (!string.IsNullOrEmpty(plan.RunId) && _runs.ContainsKey(plan.RunId))
|
||||
return null;
|
||||
|
||||
if (plan.BoundaryNodes?.Count > 0 && string.IsNullOrEmpty(plan.RunId))
|
||||
throw new InvalidOperationException("ExecutionPlan has boundaryNodes but missing RunId.");
|
||||
|
||||
var runId = plan.RunId ?? Guid.NewGuid().ToString("N");
|
||||
var startNode = FindNode(plan, plan.StartNodeId) ?? plan.Nodes[0];
|
||||
var run = new RuntimeRun(runId, plan);
|
||||
_runs[run.RunId] = run;
|
||||
ActivateNode(run, startNode.Id);
|
||||
Persist();
|
||||
return run;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces an existing run's plan with a server-provided continuation (same RunId):
|
||||
/// the previous segment is done, the new segment's start node becomes active.
|
||||
/// Idempotent: if the continuation was already applied, nothing is re-dispatched.
|
||||
/// </summary>
|
||||
private void ReplaceRun(ExecutionPlan? plan, string? fallbackRunId = null)
|
||||
{
|
||||
if (plan?.Nodes == null || plan.Nodes.Count == 0)
|
||||
return;
|
||||
|
||||
var runId = plan.RunId ?? fallbackRunId;
|
||||
if (string.IsNullOrEmpty(runId))
|
||||
return;
|
||||
|
||||
var startNode = FindNode(plan, plan.StartNodeId) ?? plan.Nodes[0];
|
||||
if (startNode == null)
|
||||
return;
|
||||
|
||||
if (_runs.TryGetValue(runId, out var existing) && existing.ActiveNodes.ContainsKey(startNode.Id))
|
||||
return; // continuation already applied (idempotent callback echo)
|
||||
|
||||
var run = new RuntimeRun(runId, plan);
|
||||
_runs[runId] = run;
|
||||
ActivateNode(run, startNode.Id);
|
||||
Persist();
|
||||
}
|
||||
|
||||
private void ActivateNode(RuntimeRun run, string nodeId, ActiveNodeState? restoredState = null)
|
||||
{
|
||||
var node = FindNode(run.Plan, nodeId);
|
||||
if (node == null)
|
||||
return;
|
||||
|
||||
var state = restoredState ?? new ActiveNodeState { NodeId = nodeId };
|
||||
run.ActiveNodes[nodeId] = state;
|
||||
DispatchActiveNode(run, state, restored: restoredState != null);
|
||||
}
|
||||
|
||||
internal Task CompleteNodeAsync(
|
||||
string runId,
|
||||
string nodeId,
|
||||
string handle,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return CompleteNodeAsync(runId, nodeId, handle, continueOnBoundary: true, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task CompleteNodeAsync(
|
||||
string runId,
|
||||
string nodeId,
|
||||
string handle,
|
||||
bool continueOnBoundary,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_runs.TryGetValue(runId, out var run))
|
||||
return;
|
||||
|
||||
var key = CompletedHandleKey(nodeId, handle);
|
||||
if (run.CompletedHandles.Contains(key))
|
||||
return; // idempotent
|
||||
|
||||
try
|
||||
{
|
||||
var continuedOnBoundary = false;
|
||||
if (continueOnBoundary)
|
||||
{
|
||||
try
|
||||
{
|
||||
continuedOnBoundary = await ContinueBoundaryAsync(
|
||||
run, nodeId, handle, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (TransientBoundaryException)
|
||||
{
|
||||
// Transient error — don't advance the run.
|
||||
// Node stays active, handle stays pending for retry on reconnect.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (continuedOnBoundary)
|
||||
{
|
||||
// The boundary may have replaced or removed the run (continuation plan
|
||||
// or reconcile). The transferred run owns the state — touching the stale
|
||||
// object here would complete or delete the new run.
|
||||
if (!_runs.TryGetValue(runId, out var currentRun) || !ReferenceEquals(currentRun, run))
|
||||
return;
|
||||
}
|
||||
|
||||
// Only now — after the server has confirmed — mark the handle and node.
|
||||
run.CompletedHandles.Add(key);
|
||||
run.ActiveNodes.Remove(nodeId);
|
||||
Persist();
|
||||
|
||||
if (!continuedOnBoundary)
|
||||
{
|
||||
foreach (var edge in MatchingEdges(run.Plan, nodeId, handle))
|
||||
ActivateNode(run, edge.Target);
|
||||
}
|
||||
|
||||
CheckRunCompleted(run);
|
||||
Persist();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FailRun(run, nodeId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ContinueBoundaryAsync(
|
||||
RuntimeRun run,
|
||||
string nodeId,
|
||||
string handle,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var boundaries = MatchingBoundaryNodes(run.Plan, nodeId, handle).ToList();
|
||||
if (boundaries.Count == 0)
|
||||
return false;
|
||||
|
||||
foreach (var boundary in boundaries)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _client.SendAsync<HandleScenarioCallbackRequest, HandleScenarioCallbackResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/scenarios/callback",
|
||||
new HandleScenarioCallbackRequest
|
||||
{
|
||||
ScenarioId = run.Plan.ScenarioId,
|
||||
NodeId = boundary.SourceNodeId,
|
||||
Handle = boundary.SourceHandle,
|
||||
RunId = run.RunId
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response?.Plan != null)
|
||||
{
|
||||
// Continuation of the current run (server keeps the RunId) replaces
|
||||
// the run's plan; fresh/terminal plans start as new runs.
|
||||
if (_runs.ContainsKey(response.Plan.RunId ?? string.Empty))
|
||||
ReplaceRun(response.Plan);
|
||||
else
|
||||
StartPlan(response.Plan);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Boundary call failed — try to reconcile with server.
|
||||
var reconciled = false;
|
||||
try
|
||||
{
|
||||
var reconcile = await _client.SendAsync<GetScenarioRunRequest, GetScenarioRunResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/scenarios/run",
|
||||
new GetScenarioRunRequest { RunId = run.RunId },
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (reconcile?.Status == "unknown_run" || reconcile?.Status == "expired")
|
||||
{
|
||||
_runs.Remove(run.RunId);
|
||||
Persist();
|
||||
reconciled = true;
|
||||
}
|
||||
|
||||
if (reconcile?.Plan != null)
|
||||
{
|
||||
ReplaceRun(reconcile.Plan, run.RunId);
|
||||
reconciled = true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Reconciliation also failed.
|
||||
}
|
||||
|
||||
if (!reconciled)
|
||||
{
|
||||
// Reconcile did not resolve — distinguish transient from terminal.
|
||||
if (IsTransientException(ex))
|
||||
throw new TransientBoundaryException(ex);
|
||||
// Terminal error — let the caller fail the run.
|
||||
throw;
|
||||
}
|
||||
// If reconciled, the boundary was handled (run corrected or removed).
|
||||
// Fall through to continue to the next boundary.
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CheckRunCompleted(RuntimeRun run)
|
||||
{
|
||||
if (run.ActiveNodes.Count > 0)
|
||||
return;
|
||||
|
||||
_runs.Remove(run.RunId);
|
||||
Persist();
|
||||
OnScenarioCompleted?.Invoke(ToPlanRun(run));
|
||||
}
|
||||
|
||||
private void FailRun(RuntimeRun run, string nodeId, Exception ex)
|
||||
{
|
||||
_client.Options.Logger?.Log(RudderLogLevel.Error, $"[Rudder] Scenario run {run.RunId} failed at node {nodeId}: {ex.Message}");
|
||||
_runs.Remove(run.RunId);
|
||||
Persist();
|
||||
OnScenarioFailed?.Invoke(new ScenarioFailedEvent(ToPlanRun(run), nodeId, ex));
|
||||
}
|
||||
|
||||
private static PlanRun ToPlanRun(RuntimeRun run)
|
||||
{
|
||||
return new PlanRun(
|
||||
run.RunId,
|
||||
run.Plan.PlanId,
|
||||
run.Plan.ScenarioId,
|
||||
run.Plan.UserId,
|
||||
run.ActiveNodes.Keys.ToList(),
|
||||
run.Plan);
|
||||
}
|
||||
|
||||
private static ExecutionPlanNode? FindNode(ExecutionPlan? plan, string? nodeId)
|
||||
{
|
||||
if (plan?.Nodes == null || plan.Nodes.Count == 0)
|
||||
return null;
|
||||
|
||||
if (!string.IsNullOrEmpty(nodeId))
|
||||
{
|
||||
foreach (var node in plan.Nodes)
|
||||
{
|
||||
if (node.Id == nodeId)
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<PlanEdge> MatchingEdges(ExecutionPlan plan, string sourceNodeId, string sourceHandle)
|
||||
{
|
||||
if (plan?.Edges == null)
|
||||
yield break;
|
||||
|
||||
foreach (var edge in plan.Edges)
|
||||
{
|
||||
if (edge.Source == sourceNodeId && string.Equals(edge.SourceHandle ?? string.Empty, sourceHandle ?? string.Empty, StringComparison.Ordinal))
|
||||
yield return edge;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<BoundaryNode> MatchingBoundaryNodes(ExecutionPlan plan, string sourceNodeId, string sourceHandle)
|
||||
{
|
||||
if (plan?.BoundaryNodes == null)
|
||||
yield break;
|
||||
|
||||
foreach (var boundary in plan.BoundaryNodes)
|
||||
{
|
||||
if (boundary.SourceNodeId == sourceNodeId && string.Equals(boundary.SourceHandle ?? string.Empty, sourceHandle ?? string.Empty, StringComparison.Ordinal))
|
||||
yield return boundary;
|
||||
}
|
||||
}
|
||||
|
||||
private static string CompletedHandleKey(string nodeId, string? handle) => nodeId + ":" + (handle ?? string.Empty);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Session of a scenario battle-pass-level node — a single claimable tier.
|
||||
/// <see cref="ClaimAsync"/> crosses onComplete, which the server accepts only
|
||||
/// once the player has reached the node's configured level.
|
||||
/// </summary>
|
||||
public sealed class BattlePassLevelSession
|
||||
{
|
||||
internal BattlePassLevelSession(ScenarioNodeContext context) => Context = context;
|
||||
|
||||
/// <summary>Underlying node context.</summary>
|
||||
public ScenarioNodeContext Context { get; }
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string Id => Context.NodeId;
|
||||
|
||||
/// <summary>The tier level this node claims.</summary>
|
||||
public int Level => Context.Get("levelNumber", 0);
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => Context.Get(key, defaultValue);
|
||||
|
||||
/// <summary>Claims this tier; crosses onComplete (the server checks the level was reached).</summary>
|
||||
public Task ClaimAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onComplete", cancellationToken);
|
||||
|
||||
/// <summary>Claims this tier (fire-and-forget).</summary>
|
||||
public void Claim() => Context.Complete("onComplete");
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Session of a scenario battle-pass node. Exposes the battle pass operations
|
||||
/// bound to this node's scenario/node/run ids, plus explicit boundary crossings
|
||||
/// the game drives from its UI; the server validates each crossing.
|
||||
/// </summary>
|
||||
public sealed class BattlePassSession
|
||||
{
|
||||
private readonly BattlePassService _battlePass;
|
||||
|
||||
internal BattlePassSession(ScenarioNodeContext context, BattlePassService battlePass)
|
||||
{
|
||||
Context = context;
|
||||
_battlePass = battlePass;
|
||||
}
|
||||
|
||||
/// <summary>Underlying node context.</summary>
|
||||
public ScenarioNodeContext Context { get; }
|
||||
|
||||
/// <summary>Node id.</summary>
|
||||
public string Id => Context.NodeId;
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => Context.Get(key, defaultValue);
|
||||
|
||||
/// <summary>Reads current progress (xp, level, premium ownership, claimed tiers) for this node.</summary>
|
||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken cancellationToken = default)
|
||||
=> _battlePass.GetProgressAsync(Context.ScenarioId, Context.NodeId, cancellationToken);
|
||||
|
||||
/// <summary>Credits xp from a configured source.</summary>
|
||||
public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken cancellationToken = default)
|
||||
=> _battlePass.AddXpAsync(new AddBattlePassXpRequest
|
||||
{
|
||||
ScenarioId = Context.ScenarioId,
|
||||
NodeId = Context.NodeId,
|
||||
RunId = Context.RunId,
|
||||
Source = source,
|
||||
Amount = amount
|
||||
}, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Claims a tier reward at a reached level.
|
||||
/// <paramref name="track"/> is <see cref="BattlePassService.TrackFree"/> or <see cref="BattlePassService.TrackPremium"/>.
|
||||
/// </summary>
|
||||
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken cancellationToken = default)
|
||||
=> _battlePass.ClaimRewardAsync(new ClaimBattlePassRewardRequest
|
||||
{
|
||||
ScenarioId = Context.ScenarioId,
|
||||
NodeId = Context.NodeId,
|
||||
RunId = Context.RunId,
|
||||
Level = level,
|
||||
Track = track
|
||||
}, cancellationToken);
|
||||
|
||||
/// <summary>Purchases the premium track, then crosses onPremiumPurchase on success.</summary>
|
||||
public async Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _battlePass.PurchasePremiumAsync(new PurchaseBattlePassPremiumRequest
|
||||
{
|
||||
ScenarioId = Context.ScenarioId,
|
||||
NodeId = Context.NodeId,
|
||||
RunId = Context.RunId,
|
||||
IdempotencyKey = Guid.NewGuid().ToString()
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response != null && response.Success)
|
||||
await Context.CompleteAsync("onPremiumPurchase", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response!;
|
||||
}
|
||||
|
||||
/// <summary>Advances the run through the onLevelUp handle.</summary>
|
||||
public Task LevelUpAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onLevelUp", cancellationToken);
|
||||
|
||||
/// <summary>Advances the run through the onLevelUp handle (fire-and-forget).</summary>
|
||||
public void LevelUp() => Context.Complete("onLevelUp");
|
||||
|
||||
/// <summary>Advances the run through the onMaxLevel handle.</summary>
|
||||
public Task MaxLevelAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onMaxLevel", cancellationToken);
|
||||
|
||||
/// <summary>Advances the run through the onMaxLevel handle (fire-and-forget).</summary>
|
||||
public void MaxLevel() => Context.Complete("onMaxLevel");
|
||||
|
||||
/// <summary>Advances the run through the onComplete handle.</summary>
|
||||
public Task CompleteAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onComplete", cancellationToken);
|
||||
|
||||
/// <summary>Advances the run through the onComplete handle (fire-and-forget).</summary>
|
||||
public void Complete() => Context.Complete("onComplete");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user