2.0.0: slug-based quests, scenarios and offers; environment carried by the token; regenerated models
CI / check (push) Successful in 25s
CI / publish (push) Has been skipped

Claude-Session: https://claude.ai/code/session_01SMCvdwDmuxoaqGgvGBLk1V
This commit is contained in:
edmand46
2026-09-06 22:25:32 +03:00
parent 62025592ba
commit 372ab7cf7e
36 changed files with 138 additions and 90 deletions
+2 -2
View File
@@ -14,8 +14,8 @@ public class AddBattlePassXpRequest
[JsonProperty("runId")] [JsonProperty("runId")]
public string? RunId { get; set; } public string? RunId { get; set; }
[JsonProperty("scenarioId")] [JsonProperty("scenarioSlug")]
public string? ScenarioId { get; set; } public string? ScenarioSlug { get; set; }
[JsonProperty("source")] [JsonProperty("source")]
public string? Source { get; set; } public string? Source { get; set; }
+2 -2
View File
@@ -14,8 +14,8 @@ public class ClaimBattlePassRewardRequest
[JsonProperty("runId")] [JsonProperty("runId")]
public string? RunId { get; set; } public string? RunId { get; set; }
[JsonProperty("scenarioId")] [JsonProperty("scenarioSlug")]
public string? ScenarioId { get; set; } public string? ScenarioSlug { get; set; }
[JsonProperty("track")] [JsonProperty("track")]
public string? Track { get; set; } public string? Track { get; set; }
+2 -2
View File
@@ -8,7 +8,7 @@ public class GetBattlePassProgressRequest
[JsonProperty("nodeId")] [JsonProperty("nodeId")]
public string? NodeId { get; set; } public string? NodeId { get; set; }
[JsonProperty("scenarioId")] [JsonProperty("scenarioSlug")]
public string? ScenarioId { get; set; } public string? ScenarioSlug { get; set; }
} }
@@ -14,7 +14,7 @@ public class PurchaseBattlePassPremiumRequest
[JsonProperty("runId")] [JsonProperty("runId")]
public string? RunId { get; set; } public string? RunId { get; set; }
[JsonProperty("scenarioId")] [JsonProperty("scenarioSlug")]
public string? ScenarioId { get; set; } public string? ScenarioSlug { get; set; }
} }
+31
View File
@@ -1,5 +1,36 @@
# Changelog # Changelog
## 2.0.0
Breaking change — major bump, required by the backend environments release.
Every project now has exactly two environments, `staging` and `prod`, and the
SDK key you configure decides which one the player belongs to. The environment
never appears in the client API: it is resolved at login and carried inside the
access and refresh tokens. Tokens issued before this release have no
environment claim and are rejected with 401, so the first call after the
backend upgrade refreshes, fails, clears the stored tokens and raises
`AuthService.AuthStateChanged` with `RudderAuthState.SignedOut`. Log the player
in again with `Auth.LoginWithDeviceAsync` / `LoginWithCustomAsync`.
Quests, scenarios and offers are addressed by their slug instead of their id,
because ids differ between staging and prod while slugs are stable. Renamed
accordingly: `Quest.Id` is now `Quest.Slug`, `QuestsService.ClaimAsync` takes a
quest slug, `ReportProgressAsync` returns the slugs of the completed quests
(`ReportQuestProgressResponse.CompletedQuestSlugs`), `StoresService.PurchaseAsync`
takes an offer slug and `Offer` carries a `Slug`, `QuestMetrics.PurchaseOffer`
builds its metric from the offer slug, and every scenario-scoped type exposes
`ScenarioSlug` instead of `ScenarioId``PendingEffect`, the effect objects,
`ScenarioCompletedEffect`, `ScenarioFailedEffect`, the battle pass requests and
`BattlePassService.GetProgressAsync`. Leaderboards, items and stores already
used slugs and are unchanged.
Also shipped here, previously committed but never published: the effects client
reconciles against every `GET /sdk/v1/scenarios/pending` response, so a run that
disappears server-side (finished elsewhere, expired after a promote) now emits
`OnScenarioCompleted` instead of lingering; and `run_not_active` joins
`unknown_run` and `run_expired` as a terminal rejection that drops the run and
emits `OnScenarioFailed`.
## 1.0.0 ## 1.0.0
Breaking change — major bump. The local scenario engine is replaced by a Breaking change — major bump. The local scenario engine is replaced by a
+2 -2
View File
@@ -5,7 +5,7 @@ namespace RudderSdk.Core.Models.Quests;
public class ClaimQuestRequest public class ClaimQuestRequest
{ {
[JsonProperty("questId")] [JsonProperty("questSlug")]
public string? QuestId { get; set; } public string? QuestSlug { get; set; }
} }
+3 -3
View File
@@ -7,9 +7,6 @@ namespace RudderSdk.Core.Models.Quests;
public class Quest public class Quest
{ {
[JsonProperty("id")]
public string? Id { get; set; }
[JsonProperty("name")] [JsonProperty("name")]
public string? Name { get; set; } public string? Name { get; set; }
@@ -19,6 +16,9 @@ public class Quest
[JsonProperty("rewards")] [JsonProperty("rewards")]
public List<Reward>? Rewards { get; set; } public List<Reward>? Rewards { get; set; }
[JsonProperty("slug")]
public string? Slug { get; set; }
[JsonProperty("status")] [JsonProperty("status")]
public string? Status { get; set; } public string? Status { get; set; }
+2 -2
View File
@@ -6,7 +6,7 @@ namespace RudderSdk.Core.Models.Quests;
public class ReportQuestProgressResponse public class ReportQuestProgressResponse
{ {
[JsonProperty("completedQuestIds")] [JsonProperty("completedQuestSlugs")]
public List<string>? CompletedQuestIds { get; set; } public List<string>? CompletedQuestSlugs { get; set; }
} }
+3 -3
View File
@@ -73,7 +73,7 @@ var quests = await client.Quests.ListAsync();
foreach (var quest in quests) foreach (var quest in quests)
{ {
if (quest.Status == "completed") if (quest.Status == "completed")
await client.Quests.ClaimAsync(quest.Id); await client.Quests.ClaimAsync(quest.Slug);
} }
// Custom metrics advance matching objectives server-side; the call returns // Custom metrics advance matching objectives server-side; the call returns
@@ -82,8 +82,8 @@ var completedIds = await client.Quests.ReportProgressAsync("kills", 1);
``` ```
Purchase metrics are reported automatically by store purchases; Purchase metrics are reported automatically by store purchases;
`QuestMetrics.PurchaseOffer(offerId)` / `QuestMetrics.PurchaseItem(itemId)` `QuestMetrics.PurchaseOffer(offerSlug)` / `QuestMetrics.PurchaseItem(itemId)`
name the format (`purchase.offer:<offerId>`, `purchase.item:<itemId>`) so name the format (`purchase.offer:<offerSlug>`, `purchase.item:<itemId>`) so
quest configs and client code agree on it. quest configs and client code agree on it.
## Sessions ## Sessions
+1 -1
View File
@@ -6,7 +6,7 @@
<AssemblyName>Rudder.Core</AssemblyName> <AssemblyName>Rudder.Core</AssemblyName>
<RootNamespace>RudderSdk.Core</RootNamespace> <RootNamespace>RudderSdk.Core</RootNamespace>
<PackageId>Rudder.Core</PackageId> <PackageId>Rudder.Core</PackageId>
<Version>1.0.0</Version> <Version>2.0.0</Version>
<Authors>Rudder</Authors> <Authors>Rudder</Authors>
<Description>Rudder LiveOps client SDK for .NET: auth, player, stores, battle pass, quests, leaderboards, inventory, remote config, scenarios and storage.</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> <PackageLicenseExpression>MIT</PackageLicenseExpression>
+2 -2
View File
@@ -14,7 +14,7 @@ public class HandleScenarioCallbackRequest
[JsonProperty("runId")] [JsonProperty("runId")]
public string? RunId { get; set; } public string? RunId { get; set; }
[JsonProperty("scenarioId")] [JsonProperty("scenarioSlug")]
public string? ScenarioId { get; set; } public string? ScenarioSlug { get; set; }
} }
+2 -2
View File
@@ -16,8 +16,8 @@ public class PendingEffect
[JsonProperty("runId")] [JsonProperty("runId")]
public string RunId { get; set; } = null!; public string RunId { get; set; } = null!;
[JsonProperty("scenarioId")] [JsonProperty("scenarioSlug")]
public string ScenarioId { get; set; } = null!; public string ScenarioSlug { get; set; } = null!;
[JsonProperty("type")] [JsonProperty("type")]
public string Type { get; set; } = null!; public string Type { get; set; } = null!;
+2 -2
View File
@@ -17,7 +17,7 @@ public class UpdateScenarioCounterRequest
[JsonProperty("runId")] [JsonProperty("runId")]
public string? RunId { get; set; } public string? RunId { get; set; }
[JsonProperty("scenarioId")] [JsonProperty("scenarioSlug")]
public string? ScenarioId { get; set; } public string? ScenarioSlug { get; set; }
} }
+2 -2
View File
@@ -24,11 +24,11 @@ public sealed class BattlePassService
internal BattlePassService(RudderClient client) => _client = client; internal BattlePassService(RudderClient client) => _client = client;
/// <summary>Reads current progress: xp, level, premium ownership, claimed tiers.</summary> /// <summary>Reads current progress: xp, level, premium ownership, claimed tiers.</summary>
public Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioId, string nodeId, CancellationToken cancellationToken = default) public Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioSlug, string nodeId, CancellationToken cancellationToken = default)
=> _client.SendAsync<GetBattlePassProgressRequest, GetBattlePassProgressResponse>( => _client.SendAsync<GetBattlePassProgressRequest, GetBattlePassProgressResponse>(
"POST", "POST",
"/sdk/v1/battlepass/progress", "/sdk/v1/battlepass/progress",
new GetBattlePassProgressRequest { ScenarioId = scenarioId, NodeId = nodeId }, new GetBattlePassProgressRequest { ScenarioSlug = scenarioSlug, NodeId = nodeId },
cancellationToken); cancellationToken);
/// <summary>Credits xp and returns the new xp/level and level-up flags.</summary> /// <summary>Credits xp and returns the new xp/level and level-up flags.</summary>
+5 -5
View File
@@ -26,7 +26,7 @@ public sealed class BattlePassEffect
public string RunId => _handle.RunId; public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary> /// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId; public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary> /// <summary>Node id.</summary>
public string NodeId => _handle.NodeId; public string NodeId => _handle.NodeId;
@@ -39,13 +39,13 @@ public sealed class BattlePassEffect
/// <summary>Reads current progress (xp, level, premium ownership, claimed tiers) for this node.</summary> /// <summary>Reads current progress (xp, level, premium ownership, claimed tiers) for this node.</summary>
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken cancellationToken = default) public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken cancellationToken = default)
=> _battlePass.GetProgressAsync(_handle.ScenarioId, _handle.NodeId, cancellationToken); => _battlePass.GetProgressAsync(_handle.ScenarioSlug, _handle.NodeId, cancellationToken);
/// <summary>Credits xp from a configured source.</summary> /// <summary>Credits xp from a configured source.</summary>
public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken cancellationToken = default) public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken cancellationToken = default)
=> _battlePass.AddXpAsync(new AddBattlePassXpRequest => _battlePass.AddXpAsync(new AddBattlePassXpRequest
{ {
ScenarioId = _handle.ScenarioId, ScenarioSlug = _handle.ScenarioSlug,
NodeId = _handle.NodeId, NodeId = _handle.NodeId,
RunId = _handle.RunId, RunId = _handle.RunId,
Source = source, Source = source,
@@ -59,7 +59,7 @@ public sealed class BattlePassEffect
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken cancellationToken = default) public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken cancellationToken = default)
=> _battlePass.ClaimRewardAsync(new ClaimBattlePassRewardRequest => _battlePass.ClaimRewardAsync(new ClaimBattlePassRewardRequest
{ {
ScenarioId = _handle.ScenarioId, ScenarioSlug = _handle.ScenarioSlug,
NodeId = _handle.NodeId, NodeId = _handle.NodeId,
RunId = _handle.RunId, RunId = _handle.RunId,
Level = level, Level = level,
@@ -71,7 +71,7 @@ public sealed class BattlePassEffect
{ {
var response = await _battlePass.PurchasePremiumAsync(new PurchaseBattlePassPremiumRequest var response = await _battlePass.PurchasePremiumAsync(new PurchaseBattlePassPremiumRequest
{ {
ScenarioId = _handle.ScenarioId, ScenarioSlug = _handle.ScenarioSlug,
NodeId = _handle.NodeId, NodeId = _handle.NodeId,
RunId = _handle.RunId, RunId = _handle.RunId,
IdempotencyKey = Guid.NewGuid().ToString() IdempotencyKey = Guid.NewGuid().ToString()
+1 -1
View File
@@ -19,7 +19,7 @@ public sealed class BattlePassLevelEffect
public string RunId => _handle.RunId; public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary> /// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId; public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary> /// <summary>Node id.</summary>
public string NodeId => _handle.NodeId; public string NodeId => _handle.NodeId;
+1 -1
View File
@@ -15,7 +15,7 @@ public sealed class LeaderboardEffect
public string RunId => _handle.RunId; public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary> /// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId; public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary> /// <summary>Node id.</summary>
public string NodeId => _handle.NodeId; public string NodeId => _handle.NodeId;
+1 -1
View File
@@ -15,7 +15,7 @@ public sealed class NotificationEffect
public string RunId => _handle.RunId; public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary> /// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId; public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary> /// <summary>Node id.</summary>
public string NodeId => _handle.NodeId; public string NodeId => _handle.NodeId;
+1 -1
View File
@@ -20,7 +20,7 @@ public sealed class QuestEffect
public string RunId => _handle.RunId; public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary> /// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId; public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary> /// <summary>Node id.</summary>
public string NodeId => _handle.NodeId; public string NodeId => _handle.NodeId;
+3 -3
View File
@@ -3,15 +3,15 @@ namespace RudderSdk.Core;
/// <summary>Payload of <see cref="EffectsService.OnScenarioCompleted"/>.</summary> /// <summary>Payload of <see cref="EffectsService.OnScenarioCompleted"/>.</summary>
public sealed class ScenarioCompletedEffect public sealed class ScenarioCompletedEffect
{ {
internal ScenarioCompletedEffect(string runId, string scenarioId) internal ScenarioCompletedEffect(string runId, string scenarioSlug)
{ {
RunId = runId; RunId = runId;
ScenarioId = scenarioId; ScenarioSlug = scenarioSlug;
} }
/// <summary>Server-issued run id.</summary> /// <summary>Server-issued run id.</summary>
public string RunId { get; } public string RunId { get; }
/// <summary>Scenario id.</summary> /// <summary>Scenario id.</summary>
public string ScenarioId { get; } public string ScenarioSlug { get; }
} }
+3 -3
View File
@@ -5,10 +5,10 @@ namespace RudderSdk.Core;
/// <summary>Payload of <see cref="EffectsService.OnScenarioFailed"/>.</summary> /// <summary>Payload of <see cref="EffectsService.OnScenarioFailed"/>.</summary>
public sealed class ScenarioFailedEffect public sealed class ScenarioFailedEffect
{ {
internal ScenarioFailedEffect(string runId, string scenarioId, string nodeId, Exception exception) internal ScenarioFailedEffect(string runId, string scenarioSlug, string nodeId, Exception exception)
{ {
RunId = runId; RunId = runId;
ScenarioId = scenarioId; ScenarioSlug = scenarioSlug;
NodeId = nodeId; NodeId = nodeId;
Exception = exception; Exception = exception;
} }
@@ -17,7 +17,7 @@ public sealed class ScenarioFailedEffect
public string RunId { get; } public string RunId { get; }
/// <summary>Scenario id.</summary> /// <summary>Scenario id.</summary>
public string ScenarioId { get; } public string ScenarioSlug { get; }
/// <summary>Node the failure happened at.</summary> /// <summary>Node the failure happened at.</summary>
public string NodeId { get; } public string NodeId { get; }
+1 -1
View File
@@ -15,7 +15,7 @@ public sealed class StoreOfferEffect
public string RunId => _handle.RunId; public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary> /// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId; public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary> /// <summary>Node id.</summary>
public string NodeId => _handle.NodeId; public string NodeId => _handle.NodeId;
+1 -1
View File
@@ -18,7 +18,7 @@ public sealed class WaitEffect
public string RunId => _handle.RunId; public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary> /// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId; public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary> /// <summary>Node id.</summary>
public string NodeId => _handle.NodeId; public string NodeId => _handle.NodeId;
+11 -11
View File
@@ -101,7 +101,7 @@ public sealed class EffectsService
var key = (effect.RunId, effect.NodeId); var key = (effect.RunId, effect.NodeId);
if (_seen.ContainsKey(key)) if (_seen.ContainsKey(key))
continue; continue;
_seen[key] = effect.ScenarioId; _seen[key] = effect.ScenarioSlug;
if (string.Equals(effect.Type, EffectTypes.Wait, StringComparison.Ordinal)) if (string.Equals(effect.Type, EffectTypes.Wait, StringComparison.Ordinal))
{ {
@@ -126,7 +126,7 @@ public sealed class EffectsService
"/sdk/v1/scenarios/callback", "/sdk/v1/scenarios/callback",
new HandleScenarioCallbackRequest new HandleScenarioCallbackRequest
{ {
ScenarioId = source.ScenarioId, ScenarioSlug = source.ScenarioSlug,
NodeId = source.NodeId, NodeId = source.NodeId,
Handle = handle, Handle = handle,
RunId = source.RunId RunId = source.RunId
@@ -136,13 +136,13 @@ public sealed class EffectsService
ForgetWait(source.RunId, source.NodeId); ForgetWait(source.RunId, source.NodeId);
var next = ReadEffect(response?.Effect); var next = ReadEffect(response?.Effect);
if (next == null) if (next == null)
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioId)); Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioSlug));
else else
Ingest(new[] { next }); Ingest(new[] { next });
} }
catch (Exception ex) when (IsDefinitiveRejection(ex)) catch (Exception ex) when (IsDefinitiveRejection(ex))
{ {
DropRun(source.RunId, source.ScenarioId, source.NodeId, ex); DropRun(source.RunId, source.ScenarioSlug, source.NodeId, ex);
} }
} }
@@ -159,7 +159,7 @@ public sealed class EffectsService
"/sdk/v1/scenarios/counter", "/sdk/v1/scenarios/counter",
new UpdateScenarioCounterRequest new UpdateScenarioCounterRequest
{ {
ScenarioId = source.ScenarioId, ScenarioSlug = source.ScenarioSlug,
NodeId = source.NodeId, NodeId = source.NodeId,
CounterKey = counterKey, CounterKey = counterKey,
Amount = amount, Amount = amount,
@@ -173,13 +173,13 @@ public sealed class EffectsService
ForgetWait(source.RunId, source.NodeId); ForgetWait(source.RunId, source.NodeId);
var next = ReadEffect(response.Effect); var next = ReadEffect(response.Effect);
if (next == null) if (next == null)
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioId)); Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioSlug));
else else
Ingest(new[] { next }); Ingest(new[] { next });
} }
catch (Exception ex) when (IsDefinitiveRejection(ex)) catch (Exception ex) when (IsDefinitiveRejection(ex))
{ {
DropRun(source.RunId, source.ScenarioId, source.NodeId, ex); DropRun(source.RunId, source.ScenarioSlug, source.NodeId, ex);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -274,14 +274,14 @@ public sealed class EffectsService
OnScenarioFailed, OnScenarioFailed,
new ScenarioFailedEffect( new ScenarioFailedEffect(
effect.RunId, effect.RunId,
effect.ScenarioId, effect.ScenarioSlug,
effect.NodeId, effect.NodeId,
new Exception($"Unsupported scenario node type '{effect.Type}'"))); new Exception($"Unsupported scenario node type '{effect.Type}'")));
break; break;
} }
} }
private void DropRun(string runId, string scenarioId, string nodeId, Exception exception) private void DropRun(string runId, string scenarioSlug, string nodeId, Exception exception)
{ {
lock (_gate) lock (_gate)
{ {
@@ -299,7 +299,7 @@ public sealed class EffectsService
} }
} }
Emit(OnScenarioFailed, new ScenarioFailedEffect(runId, scenarioId, nodeId, exception)); Emit(OnScenarioFailed, new ScenarioFailedEffect(runId, scenarioSlug, nodeId, exception));
} }
private void Reconcile(IEnumerable<PendingEffect>? effects) private void Reconcile(IEnumerable<PendingEffect>? effects)
@@ -424,7 +424,7 @@ internal sealed class EffectHandle
public string RunId => _effect.RunId; public string RunId => _effect.RunId;
public string ScenarioId => _effect.ScenarioId; public string ScenarioSlug => _effect.ScenarioSlug;
public string NodeId => _effect.NodeId; public string NodeId => _effect.NodeId;
+1 -1
View File
@@ -9,7 +9,7 @@ namespace RudderSdk.Core;
public static class QuestMetrics public static class QuestMetrics
{ {
/// <summary>Metric for purchasing a store offer; auto-reported on purchase.</summary> /// <summary>Metric for purchasing a store offer; auto-reported on purchase.</summary>
public static string PurchaseOffer(string offerId) => $"purchase.offer:{offerId}"; public static string PurchaseOffer(string offerSlug) => $"purchase.offer:{offerSlug}";
/// <summary>Metric for purchasing a catalog item; auto-reported on purchase.</summary> /// <summary>Metric for purchasing a catalog item; auto-reported on purchase.</summary>
public static string PurchaseItem(string itemId) => $"purchase.item:{itemId}"; public static string PurchaseItem(string itemId) => $"purchase.item:{itemId}";
+3 -3
View File
@@ -27,11 +27,11 @@ public sealed class QuestsService
} }
/// <summary>Claims a completed quest's rewards (idempotent server-side).</summary> /// <summary>Claims a completed quest's rewards (idempotent server-side).</summary>
public Task<ClaimQuestResponse> ClaimAsync(string questId, CancellationToken cancellationToken = default) public Task<ClaimQuestResponse> ClaimAsync(string questSlug, CancellationToken cancellationToken = default)
=> _client.SendAsync<ClaimQuestRequest, ClaimQuestResponse>( => _client.SendAsync<ClaimQuestRequest, ClaimQuestResponse>(
"POST", "POST",
"/sdk/v1/quests/claim", "/sdk/v1/quests/claim",
new ClaimQuestRequest { QuestId = questId }, new ClaimQuestRequest { QuestSlug = questSlug },
cancellationToken); cancellationToken);
/// <summary>Reports progress for a metric and returns the ids of quests completed by this report.</summary> /// <summary>Reports progress for a metric and returns the ids of quests completed by this report.</summary>
@@ -43,6 +43,6 @@ public sealed class QuestsService
new ReportQuestProgressRequest { Metric = metric, Amount = amount }, new ReportQuestProgressRequest { Metric = metric, Amount = amount },
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
return response?.CompletedQuestIds ?? new List<string>(); return response?.CompletedQuestSlugs ?? new List<string>();
} }
} }
+3 -3
View File
@@ -35,17 +35,17 @@ public sealed class StoresService
/// </summary> /// </summary>
public Task<PurchaseOfferResponse> PurchaseAsync( public Task<PurchaseOfferResponse> PurchaseAsync(
string storeSlug, string storeSlug,
string offerId, string offerSlug,
string? idempotencyKey = null, string? idempotencyKey = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return _client.SendAsync<PurchaseOfferRequest, PurchaseOfferResponse>( return _client.SendAsync<PurchaseOfferRequest, PurchaseOfferResponse>(
"POST", "POST",
"/sdk/v1/stores/" + Url.Encode(storeSlug) + "/offers/" + Url.Encode(offerId) + "/purchase", "/sdk/v1/stores/" + Url.Encode(storeSlug) + "/offers/" + Url.Encode(offerSlug) + "/purchase",
new PurchaseOfferRequest new PurchaseOfferRequest
{ {
StoreSlug = storeSlug, StoreSlug = storeSlug,
OfferId = offerId, OfferSlug = offerSlug,
IdempotencyKey = idempotencyKey ?? Guid.NewGuid().ToString() IdempotencyKey = idempotencyKey ?? Guid.NewGuid().ToString()
}, },
cancellationToken); cancellationToken);
+3
View File
@@ -25,6 +25,9 @@ public class Offer
[JsonProperty("price")] [JsonProperty("price")]
public OfferPrice? Price { get; set; } public OfferPrice? Price { get; set; }
[JsonProperty("slug")]
public string? Slug { get; set; }
[JsonProperty("updatedAt")] [JsonProperty("updatedAt")]
public DateTimeOffset? UpdatedAt { get; set; } public DateTimeOffset? UpdatedAt { get; set; }
+2 -2
View File
@@ -8,8 +8,8 @@ public class PurchaseOfferRequest
[JsonProperty("idempotencyKey")] [JsonProperty("idempotencyKey")]
public string? IdempotencyKey { get; set; } public string? IdempotencyKey { get; set; }
[JsonProperty("offerId")] [JsonProperty("offerSlug")]
public string? OfferId { get; set; } public string? OfferSlug { get; set; }
[JsonProperty("storeSlug")] [JsonProperty("storeSlug")]
public string? StoreSlug { get; set; } public string? StoreSlug { get; set; }
+1 -1
View File
@@ -82,7 +82,7 @@ realtime websocket client in Rudder.Core.
Important cross-cutting facts: Important cross-cutting facts:
- Battle pass state is tied to a scenario battle-pass node: every - Battle pass state is tied to a scenario battle-pass node: every
`BattlePassService` call carries `ScenarioId`/`NodeId` (mutations also `BattlePassService` call carries `ScenarioSlug`/`NodeId` (mutations also
`RunId`). During a scenario run, `BattlePassEffect` supplies them — prefer `RunId`). During a scenario run, `BattlePassEffect` supplies them — prefer
the effect API inside a run. the effect API inside a run.
- Global quests (`client.Quests`) are distinct from scenario quest nodes - Global quests (`client.Quests`) are distinct from scenario quest nodes
@@ -24,6 +24,19 @@ public void Logout(); // drops the stored session, fires SignedOut
`RudderAuthState`: `SignedIn`, `SignedOut`. The event fires after a `RudderAuthState`: `SignedIn`, `SignedOut`. The event fires after a
successful login and after logout or a failed token refresh. successful login and after logout or a failed token refresh.
## Environments
A project has two environments, `staging` and `prod`. The SDK key configured
in `RudderClientOptions.ProjectKey` belongs to one of them, so the environment
is resolved at login and carried inside the access and refresh tokens; nothing
in the client API takes an environment argument, and a player created in one
environment is invisible in the other. Content released only to `staging` is
empty for a `prod` key and vice versa.
Tokens issued before SDK 2.0.0 carry no environment claim and are rejected with
401. The refresh pipeline then fails, clears the store and fires
`AuthStateChanged(SignedOut)` — log the player in again.
## Endpoints ## Endpoints
- `POST /sdk/v1/authorization/device` — body `{key, deviceId, region, - `POST /sdk/v1/authorization/device` — body `{key, deviceId, region,
@@ -6,7 +6,7 @@ Source: `Services/BattlePassService.cs`,
(generated DTOs). (generated DTOs).
Battle pass state is tied to a scenario battle-pass node, so every call Battle pass state is tied to a scenario battle-pass node, so every call
carries `ScenarioId`/`NodeId` (mutations also `RunId`). Inside a scenario run, carries `ScenarioSlug`/`NodeId` (mutations also `RunId`). Inside a scenario run,
`Effects.OnBattlePass` hands you a `BattlePassEffect` that supplies these `Effects.OnBattlePass` hands you a `BattlePassEffect` that supplies these
ids — prefer it over calling the service directly. ids — prefer it over calling the service directly.
@@ -18,7 +18,7 @@ public const string TrackPremium = "premium";
// POST /sdk/v1/battlepass/progress // POST /sdk/v1/battlepass/progress
public Task<GetBattlePassProgressResponse> GetProgressAsync( public Task<GetBattlePassProgressResponse> GetProgressAsync(
string scenarioId, string nodeId, CancellationToken cancellationToken = default); string scenarioSlug, string nodeId, CancellationToken cancellationToken = default);
// POST /sdk/v1/battlepass/xp // POST /sdk/v1/battlepass/xp
public Task<AddBattlePassXpResponse> AddXpAsync( public Task<AddBattlePassXpResponse> AddXpAsync(
@@ -40,7 +40,7 @@ public Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(
public class GetBattlePassProgressRequest public class GetBattlePassProgressRequest
{ {
public string? NodeId { get; set; } // "nodeId" public string? NodeId { get; set; } // "nodeId"
public string? ScenarioId { get; set; } // "scenarioId" public string? ScenarioSlug { get; set; } // "scenarioSlug"
} }
public class AddBattlePassXpRequest public class AddBattlePassXpRequest
@@ -48,7 +48,7 @@ public class AddBattlePassXpRequest
public long? Amount { get; set; } // "amount" public long? Amount { get; set; } // "amount"
public string? NodeId { get; set; } // "nodeId" public string? NodeId { get; set; } // "nodeId"
public string? RunId { get; set; } // "runId" public string? RunId { get; set; } // "runId"
public string? ScenarioId { get; set; } // "scenarioId" public string? ScenarioSlug { get; set; } // "scenarioSlug"
public string? Source { get; set; } // "source" public string? Source { get; set; } // "source"
} }
@@ -57,7 +57,7 @@ public class ClaimBattlePassRewardRequest
public int? Level { get; set; } // "level" public int? Level { get; set; } // "level"
public string? NodeId { get; set; } // "nodeId" public string? NodeId { get; set; } // "nodeId"
public string? RunId { get; set; } // "runId" public string? RunId { get; set; } // "runId"
public string? ScenarioId { get; set; } // "scenarioId" public string? ScenarioSlug { get; set; } // "scenarioSlug"
public string? Track { get; set; } // "track" — TrackFree / TrackPremium public string? Track { get; set; } // "track" — TrackFree / TrackPremium
} }
@@ -66,7 +66,7 @@ public class PurchaseBattlePassPremiumRequest
public string? IdempotencyKey { get; set; } // "idempotencyKey" public string? IdempotencyKey { get; set; } // "idempotencyKey"
public string? NodeId { get; set; } // "nodeId" public string? NodeId { get; set; } // "nodeId"
public string? RunId { get; set; } // "runId" public string? RunId { get; set; } // "runId"
public string? ScenarioId { get; set; } // "scenarioId" public string? ScenarioSlug { get; set; } // "scenarioSlug"
} }
``` ```
@@ -116,7 +116,7 @@ Raised via `Effects.OnBattlePass`. Bound to the node's scenario/node/run ids:
```csharp ```csharp
public string RunId { get; } public string RunId { get; }
public string ScenarioId { get; } public string ScenarioSlug { get; }
public string NodeId { get; } public string NodeId { get; }
public T Get<T>(string key, T defaultValue = default!); public T Get<T>(string key, T defaultValue = default!);
+4 -4
View File
@@ -14,10 +14,10 @@ scenarios.md.
public Task<IReadOnlyList<Quest>> ListAsync(CancellationToken cancellationToken = default); public Task<IReadOnlyList<Quest>> ListAsync(CancellationToken cancellationToken = default);
// POST /sdk/v1/quests/claim — claims a completed quest's rewards (idempotent server-side) // POST /sdk/v1/quests/claim — claims a completed quest's rewards (idempotent server-side)
public Task<ClaimQuestResponse> ClaimAsync(string questId, CancellationToken cancellationToken = default); public Task<ClaimQuestResponse> ClaimAsync(string questSlug, CancellationToken cancellationToken = default);
// POST /sdk/v1/quests/progress — reports progress for a metric; returns the // POST /sdk/v1/quests/progress — reports progress for a metric; returns the
// ids of quests completed by THIS report // slugs of quests completed by THIS report
public Task<IReadOnlyList<string>> ReportProgressAsync( public Task<IReadOnlyList<string>> ReportProgressAsync(
string metric, long amount, CancellationToken cancellationToken = default); string metric, long amount, CancellationToken cancellationToken = default);
``` ```
@@ -27,7 +27,7 @@ public Task<IReadOnlyList<string>> ReportProgressAsync(
```csharp ```csharp
public static class QuestMetrics public static class QuestMetrics
{ {
public static string PurchaseOffer(string offerId); // "purchase.offer:{offerId}" public static string PurchaseOffer(string offerSlug); // "purchase.offer:{offerSlug}"
public static string PurchaseItem(string itemId); // "purchase.item:{itemId}" public static string PurchaseItem(string itemId); // "purchase.item:{itemId}"
} }
``` ```
@@ -40,10 +40,10 @@ metric is a custom string passed to `ReportProgressAsync`.
```csharp ```csharp
public class Quest public class Quest
{ {
public string? Id { get; set; } // "id"
public string? Name { get; set; } // "name" public string? Name { get; set; } // "name"
public List<QuestObjectiveProgress>? Objectives { get; set; }// "objectives" public List<QuestObjectiveProgress>? Objectives { get; set; }// "objectives"
public List<Reward>? Rewards { get; set; } // "rewards" public List<Reward>? Rewards { get; set; } // "rewards"
public string? Slug { get; set; } // "slug" — stable across environments
public string? Status { get; set; } // "status" public string? Status { get; set; } // "status"
} }
@@ -78,7 +78,7 @@ client.Effects.OnNotification += n =>
await client.Scenario.TriggerAsync("level_complete"); await client.Scenario.TriggerAsync("level_complete");
``` ```
Every effect exposes `RunId`, `ScenarioId`, `NodeId`, `Data` (`JObject`), Every effect exposes `RunId`, `ScenarioSlug`, `NodeId`, `Data` (`JObject`),
and `Get<T>(key, defaultValue)` for reading node data. Completion methods and `Get<T>(key, defaultValue)` for reading node data. Completion methods
have an `Async` variant (awaitable) and a fire-and-forget variant. have an `Async` variant (awaitable) and a fire-and-forget variant.
@@ -156,7 +156,7 @@ Scenario quest node — distinct from global `client.Quests`.
public string Name { get; } // node data "name" public string Name { get; } // node data "name"
public IReadOnlyList<JObject> Objectives { get; } // node data "objectives" public IReadOnlyList<JObject> Objectives { get; } // node data "objectives"
// POST /sdk/v1/scenarios/counter {scenarioId, nodeId, runId, counterKey, amount} // POST /sdk/v1/scenarios/counter {scenarioSlug, nodeId, runId, counterKey, amount}
public Task ReportProgressAsync(string objectiveId, long amount = 1, CancellationToken ct = default); public Task ReportProgressAsync(string objectiveId, long amount = 1, CancellationToken ct = default);
public void ReportProgress(string objectiveId, long amount = 1); public void ReportProgress(string objectiveId, long amount = 1);
``` ```
@@ -166,7 +166,7 @@ completed counter response may carry the next `PendingEffect`.
### `BattlePassEffect` ### `BattlePassEffect`
Bound to this node's `ScenarioId` / `NodeId` / `RunId` — prefer these over Bound to this node's `ScenarioSlug` / `NodeId` / `RunId` — prefer these over
calling `client.BattlePass` by hand. See also reference/battlepass.md. calling `client.BattlePass` by hand. See also reference/battlepass.md.
```csharp ```csharp
@@ -199,13 +199,13 @@ configured level.
public sealed class ScenarioCompletedEffect public sealed class ScenarioCompletedEffect
{ {
public string RunId { get; } public string RunId { get; }
public string ScenarioId { get; } public string ScenarioSlug { get; }
} }
public sealed class ScenarioFailedEffect public sealed class ScenarioFailedEffect
{ {
public string RunId { get; } public string RunId { get; }
public string ScenarioId { get; } public string ScenarioSlug { get; }
public string NodeId { get; } public string NodeId { get; }
public Exception Exception { get; } public Exception Exception { get; }
} }
@@ -219,7 +219,7 @@ advanced a wait, the run expired, or another device completed it.
## Reliability ## Reliability
- Completion methods POST `/sdk/v1/scenarios/callback` with - Completion methods POST `/sdk/v1/scenarios/callback` with
`{scenarioId, runId, nodeId, handle}` using the handles `output`, `{scenarioSlug, runId, nodeId, handle}` using the handles `output`,
`onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`, `onPurchase`, `onDecline`, `onEnd`, `onClaim`, `onComplete`, `onLevelUp`,
`onPremiumPurchase`. A non-null `effect` in the response is ingested as `onPremiumPurchase`. A non-null `effect` in the response is ingested as
the next node; a null/missing effect completes the run. the next node; a null/missing effect completes the run.
@@ -243,7 +243,7 @@ public class PendingEffect
public JToken Data { get; set; } public JToken Data { get; set; }
public string NodeId { get; set; } public string NodeId { get; set; }
public string RunId { get; set; } public string RunId { get; set; }
public string ScenarioId { get; set; } public string ScenarioSlug { get; set; }
public string Type { get; set; } public string Type { get; set; }
public DateTimeOffset? WaitDeadline { get; set; } public DateTimeOffset? WaitDeadline { get; set; }
} }
@@ -257,7 +257,7 @@ public class HandleScenarioCallbackRequest
public string? Handle { get; set; } public string? Handle { get; set; }
public string? NodeId { get; set; } public string? NodeId { get; set; }
public string? RunId { get; set; } public string? RunId { get; set; }
public string? ScenarioId { get; set; } public string? ScenarioSlug { get; set; }
} }
public class HandleScenarioCallbackResponse { public JToken? Effect { get; set; } } public class HandleScenarioCallbackResponse { public JToken? Effect { get; set; } }
@@ -267,7 +267,7 @@ public class UpdateScenarioCounterRequest
public string? CounterKey { get; set; } public string? CounterKey { get; set; }
public string? NodeId { get; set; } public string? NodeId { get; set; }
public string? RunId { get; set; } public string? RunId { get; set; }
public string? ScenarioId { get; set; } public string? ScenarioSlug { get; set; }
} }
public class UpdateScenarioCounterResponse public class UpdateScenarioCounterResponse
{ {
+4 -3
View File
@@ -14,11 +14,11 @@ public Task<IReadOnlyList<Store>> ListAsync(CancellationToken cancellationToken
// GET /sdk/v1/stores/{slug} // GET /sdk/v1/stores/{slug}
public Task<Store> GetAsync(string slug, CancellationToken cancellationToken = default); public Task<Store> GetAsync(string slug, CancellationToken cancellationToken = default);
// POST /sdk/v1/stores/{storeSlug}/offers/{offerId}/purchase // POST /sdk/v1/stores/{storeSlug}/offers/{offerSlug}/purchase
// When idempotencyKey is null a random GUID is generated; pass a stable key to // When idempotencyKey is null a random GUID is generated; pass a stable key to
// make retries safe. // make retries safe.
public Task<PurchaseOfferResponse> PurchaseAsync( public Task<PurchaseOfferResponse> PurchaseAsync(
string storeSlug, string offerId, string? idempotencyKey = null, string storeSlug, string offerSlug, string? idempotencyKey = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
``` ```
@@ -44,6 +44,7 @@ public class Store
public class Offer public class Offer
{ {
public string? Id { get; set; } // "id" public string? Id { get; set; } // "id"
public string? Slug { get; set; } // "slug" — stable across environments
public string? Name { get; set; } // "name" public string? Name { get; set; } // "name"
public OfferPrice? Price { get; set; } // "price" public OfferPrice? Price { get; set; } // "price"
public List<OfferContent>? Contents { get; set; }// "contents" public List<OfferContent>? Contents { get; set; }// "contents"
@@ -65,5 +66,5 @@ public class PurchaseOfferResponse
`PurchaseOfferResponse.Error` carries a business error string when `PurchaseOfferResponse.Error` carries a business error string when
`Success` is false — check the flag instead of relying on exceptions alone. `Success` is false — check the flag instead of relying on exceptions alone.
Purchases also auto-report quest metrics (`purchase.offer:{offerId}`) — see Purchases also auto-report quest metrics (`purchase.offer:{offerSlug}`) — see
quests.md. quests.md.
@@ -103,7 +103,7 @@ public sealed class ScenarioServiceTests
Assert.NotNull(completed); Assert.NotNull(completed);
Assert.Equal("run-1", completed!.RunId); Assert.Equal("run-1", completed!.RunId);
Assert.Equal("scenario-1", completed.ScenarioId); Assert.Equal("scenario-1", completed.ScenarioSlug);
} }
[Fact] [Fact]
@@ -522,7 +522,7 @@ public sealed class ScenarioServiceTests
return new PendingEffect return new PendingEffect
{ {
RunId = runId, RunId = runId,
ScenarioId = "scenario-1", ScenarioSlug = "scenario-1",
NodeId = nodeId, NodeId = nodeId,
Type = type, Type = type,
Data = data == null ? new JObject() : JObject.FromObject(data), Data = data == null ? new JObject() : JObject.FromObject(data),