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")]
public string? RunId { get; set; }
[JsonProperty("scenarioId")]
public string? ScenarioId { get; set; }
[JsonProperty("scenarioSlug")]
public string? ScenarioSlug { get; set; }
[JsonProperty("source")]
public string? Source { get; set; }
+2 -2
View File
@@ -14,8 +14,8 @@ public class ClaimBattlePassRewardRequest
[JsonProperty("runId")]
public string? RunId { get; set; }
[JsonProperty("scenarioId")]
public string? ScenarioId { get; set; }
[JsonProperty("scenarioSlug")]
public string? ScenarioSlug { get; set; }
[JsonProperty("track")]
public string? Track { get; set; }
+2 -2
View File
@@ -8,7 +8,7 @@ public class GetBattlePassProgressRequest
[JsonProperty("nodeId")]
public string? NodeId { get; set; }
[JsonProperty("scenarioId")]
public string? ScenarioId { get; set; }
[JsonProperty("scenarioSlug")]
public string? ScenarioSlug { get; set; }
}
@@ -14,7 +14,7 @@ public class PurchaseBattlePassPremiumRequest
[JsonProperty("runId")]
public string? RunId { get; set; }
[JsonProperty("scenarioId")]
public string? ScenarioId { get; set; }
[JsonProperty("scenarioSlug")]
public string? ScenarioSlug { get; set; }
}
+31
View File
@@ -1,5 +1,36 @@
# 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
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
{
[JsonProperty("questId")]
public string? QuestId { get; set; }
[JsonProperty("questSlug")]
public string? QuestSlug { get; set; }
}
+3 -3
View File
@@ -7,9 +7,6 @@ namespace RudderSdk.Core.Models.Quests;
public class Quest
{
[JsonProperty("id")]
public string? Id { get; set; }
[JsonProperty("name")]
public string? Name { get; set; }
@@ -19,6 +16,9 @@ public class Quest
[JsonProperty("rewards")]
public List<Reward>? Rewards { get; set; }
[JsonProperty("slug")]
public string? Slug { get; set; }
[JsonProperty("status")]
public string? Status { get; set; }
+2 -2
View File
@@ -6,7 +6,7 @@ namespace RudderSdk.Core.Models.Quests;
public class ReportQuestProgressResponse
{
[JsonProperty("completedQuestIds")]
public List<string>? CompletedQuestIds { get; set; }
[JsonProperty("completedQuestSlugs")]
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)
{
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
@@ -82,8 +82,8 @@ 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
`QuestMetrics.PurchaseOffer(offerSlug)` / `QuestMetrics.PurchaseItem(itemId)`
name the format (`purchase.offer:<offerSlug>`, `purchase.item:<itemId>`) so
quest configs and client code agree on it.
## Sessions
+1 -1
View File
@@ -6,7 +6,7 @@
<AssemblyName>Rudder.Core</AssemblyName>
<RootNamespace>RudderSdk.Core</RootNamespace>
<PackageId>Rudder.Core</PackageId>
<Version>1.0.0</Version>
<Version>2.0.0</Version>
<Authors>Rudder</Authors>
<Description>Rudder LiveOps client SDK for .NET: auth, player, stores, battle pass, quests, leaderboards, inventory, remote config, scenarios and storage.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
+2 -2
View File
@@ -14,7 +14,7 @@ public class HandleScenarioCallbackRequest
[JsonProperty("runId")]
public string? RunId { get; set; }
[JsonProperty("scenarioId")]
public string? ScenarioId { get; set; }
[JsonProperty("scenarioSlug")]
public string? ScenarioSlug { get; set; }
}
+2 -2
View File
@@ -16,8 +16,8 @@ public class PendingEffect
[JsonProperty("runId")]
public string RunId { get; set; } = null!;
[JsonProperty("scenarioId")]
public string ScenarioId { get; set; } = null!;
[JsonProperty("scenarioSlug")]
public string ScenarioSlug { get; set; } = null!;
[JsonProperty("type")]
public string Type { get; set; } = null!;
+2 -2
View File
@@ -17,7 +17,7 @@ public class UpdateScenarioCounterRequest
[JsonProperty("runId")]
public string? RunId { get; set; }
[JsonProperty("scenarioId")]
public string? ScenarioId { get; set; }
[JsonProperty("scenarioSlug")]
public string? ScenarioSlug { get; set; }
}
+2 -2
View File
@@ -24,11 +24,11 @@ public sealed class BattlePassService
internal BattlePassService(RudderClient client) => _client = client;
/// <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>(
"POST",
"/sdk/v1/battlepass/progress",
new GetBattlePassProgressRequest { ScenarioId = scenarioId, NodeId = nodeId },
new GetBattlePassProgressRequest { ScenarioSlug = scenarioSlug, NodeId = nodeId },
cancellationToken);
/// <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;
/// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId;
public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary>
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>
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>
public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken cancellationToken = default)
=> _battlePass.AddXpAsync(new AddBattlePassXpRequest
{
ScenarioId = _handle.ScenarioId,
ScenarioSlug = _handle.ScenarioSlug,
NodeId = _handle.NodeId,
RunId = _handle.RunId,
Source = source,
@@ -59,7 +59,7 @@ public sealed class BattlePassEffect
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken cancellationToken = default)
=> _battlePass.ClaimRewardAsync(new ClaimBattlePassRewardRequest
{
ScenarioId = _handle.ScenarioId,
ScenarioSlug = _handle.ScenarioSlug,
NodeId = _handle.NodeId,
RunId = _handle.RunId,
Level = level,
@@ -71,7 +71,7 @@ public sealed class BattlePassEffect
{
var response = await _battlePass.PurchasePremiumAsync(new PurchaseBattlePassPremiumRequest
{
ScenarioId = _handle.ScenarioId,
ScenarioSlug = _handle.ScenarioSlug,
NodeId = _handle.NodeId,
RunId = _handle.RunId,
IdempotencyKey = Guid.NewGuid().ToString()
+1 -1
View File
@@ -19,7 +19,7 @@ public sealed class BattlePassLevelEffect
public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId;
public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary>
public string NodeId => _handle.NodeId;
+1 -1
View File
@@ -15,7 +15,7 @@ public sealed class LeaderboardEffect
public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId;
public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary>
public string NodeId => _handle.NodeId;
+1 -1
View File
@@ -15,7 +15,7 @@ public sealed class NotificationEffect
public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId;
public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary>
public string NodeId => _handle.NodeId;
+1 -1
View File
@@ -20,7 +20,7 @@ public sealed class QuestEffect
public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId;
public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary>
public string NodeId => _handle.NodeId;
+3 -3
View File
@@ -3,15 +3,15 @@ namespace RudderSdk.Core;
/// <summary>Payload of <see cref="EffectsService.OnScenarioCompleted"/>.</summary>
public sealed class ScenarioCompletedEffect
{
internal ScenarioCompletedEffect(string runId, string scenarioId)
internal ScenarioCompletedEffect(string runId, string scenarioSlug)
{
RunId = runId;
ScenarioId = scenarioId;
ScenarioSlug = scenarioSlug;
}
/// <summary>Server-issued run id.</summary>
public string RunId { get; }
/// <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>
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;
ScenarioId = scenarioId;
ScenarioSlug = scenarioSlug;
NodeId = nodeId;
Exception = exception;
}
@@ -17,7 +17,7 @@ public sealed class ScenarioFailedEffect
public string RunId { get; }
/// <summary>Scenario id.</summary>
public string ScenarioId { get; }
public string ScenarioSlug { get; }
/// <summary>Node the failure happened at.</summary>
public string NodeId { get; }
+1 -1
View File
@@ -15,7 +15,7 @@ public sealed class StoreOfferEffect
public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId;
public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary>
public string NodeId => _handle.NodeId;
+1 -1
View File
@@ -18,7 +18,7 @@ public sealed class WaitEffect
public string RunId => _handle.RunId;
/// <summary>Scenario id.</summary>
public string ScenarioId => _handle.ScenarioId;
public string ScenarioSlug => _handle.ScenarioSlug;
/// <summary>Node id.</summary>
public string NodeId => _handle.NodeId;
+11 -11
View File
@@ -101,7 +101,7 @@ public sealed class EffectsService
var key = (effect.RunId, effect.NodeId);
if (_seen.ContainsKey(key))
continue;
_seen[key] = effect.ScenarioId;
_seen[key] = effect.ScenarioSlug;
if (string.Equals(effect.Type, EffectTypes.Wait, StringComparison.Ordinal))
{
@@ -126,7 +126,7 @@ public sealed class EffectsService
"/sdk/v1/scenarios/callback",
new HandleScenarioCallbackRequest
{
ScenarioId = source.ScenarioId,
ScenarioSlug = source.ScenarioSlug,
NodeId = source.NodeId,
Handle = handle,
RunId = source.RunId
@@ -136,13 +136,13 @@ public sealed class EffectsService
ForgetWait(source.RunId, source.NodeId);
var next = ReadEffect(response?.Effect);
if (next == null)
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioId));
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioSlug));
else
Ingest(new[] { next });
}
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",
new UpdateScenarioCounterRequest
{
ScenarioId = source.ScenarioId,
ScenarioSlug = source.ScenarioSlug,
NodeId = source.NodeId,
CounterKey = counterKey,
Amount = amount,
@@ -173,13 +173,13 @@ public sealed class EffectsService
ForgetWait(source.RunId, source.NodeId);
var next = ReadEffect(response.Effect);
if (next == null)
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioId));
Emit(OnScenarioCompleted, new ScenarioCompletedEffect(source.RunId, source.ScenarioSlug));
else
Ingest(new[] { next });
}
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)
{
@@ -274,14 +274,14 @@ public sealed class EffectsService
OnScenarioFailed,
new ScenarioFailedEffect(
effect.RunId,
effect.ScenarioId,
effect.ScenarioSlug,
effect.NodeId,
new Exception($"Unsupported scenario node type '{effect.Type}'")));
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)
{
@@ -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)
@@ -424,7 +424,7 @@ internal sealed class EffectHandle
public string RunId => _effect.RunId;
public string ScenarioId => _effect.ScenarioId;
public string ScenarioSlug => _effect.ScenarioSlug;
public string NodeId => _effect.NodeId;
+1 -1
View File
@@ -9,7 +9,7 @@ namespace RudderSdk.Core;
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}";
public static string PurchaseOffer(string offerSlug) => $"purchase.offer:{offerSlug}";
/// <summary>Metric for purchasing a catalog item; auto-reported on purchase.</summary>
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>
public Task<ClaimQuestResponse> ClaimAsync(string questId, CancellationToken cancellationToken = default)
public Task<ClaimQuestResponse> ClaimAsync(string questSlug, CancellationToken cancellationToken = default)
=> _client.SendAsync<ClaimQuestRequest, ClaimQuestResponse>(
"POST",
"/sdk/v1/quests/claim",
new ClaimQuestRequest { QuestId = questId },
new ClaimQuestRequest { QuestSlug = questSlug },
cancellationToken);
/// <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 },
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>
public Task<PurchaseOfferResponse> PurchaseAsync(
string storeSlug,
string offerId,
string offerSlug,
string? idempotencyKey = null,
CancellationToken cancellationToken = default)
{
return _client.SendAsync<PurchaseOfferRequest, PurchaseOfferResponse>(
"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
{
StoreSlug = storeSlug,
OfferId = offerId,
OfferSlug = offerSlug,
IdempotencyKey = idempotencyKey ?? Guid.NewGuid().ToString()
},
cancellationToken);
+3
View File
@@ -25,6 +25,9 @@ public class Offer
[JsonProperty("price")]
public OfferPrice? Price { get; set; }
[JsonProperty("slug")]
public string? Slug { get; set; }
[JsonProperty("updatedAt")]
public DateTimeOffset? UpdatedAt { get; set; }
+2 -2
View File
@@ -8,8 +8,8 @@ public class PurchaseOfferRequest
[JsonProperty("idempotencyKey")]
public string? IdempotencyKey { get; set; }
[JsonProperty("offerId")]
public string? OfferId { get; set; }
[JsonProperty("offerSlug")]
public string? OfferSlug { get; set; }
[JsonProperty("storeSlug")]
public string? StoreSlug { get; set; }
+1 -1
View File
@@ -82,7 +82,7 @@ realtime websocket client in Rudder.Core.
Important cross-cutting facts:
- 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
the effect API inside a run.
- 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
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
- `POST /sdk/v1/authorization/device` — body `{key, deviceId, region,
@@ -6,7 +6,7 @@ Source: `Services/BattlePassService.cs`,
(generated DTOs).
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
ids — prefer it over calling the service directly.
@@ -18,7 +18,7 @@ public const string TrackPremium = "premium";
// POST /sdk/v1/battlepass/progress
public Task<GetBattlePassProgressResponse> GetProgressAsync(
string scenarioId, string nodeId, CancellationToken cancellationToken = default);
string scenarioSlug, string nodeId, CancellationToken cancellationToken = default);
// POST /sdk/v1/battlepass/xp
public Task<AddBattlePassXpResponse> AddXpAsync(
@@ -40,7 +40,7 @@ public Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(
public class GetBattlePassProgressRequest
{
public string? NodeId { get; set; } // "nodeId"
public string? ScenarioId { get; set; } // "scenarioId"
public string? ScenarioSlug { get; set; } // "scenarioSlug"
}
public class AddBattlePassXpRequest
@@ -48,7 +48,7 @@ public class AddBattlePassXpRequest
public long? Amount { get; set; } // "amount"
public string? NodeId { get; set; } // "nodeId"
public string? RunId { get; set; } // "runId"
public string? ScenarioId { get; set; } // "scenarioId"
public string? ScenarioSlug { get; set; } // "scenarioSlug"
public string? Source { get; set; } // "source"
}
@@ -57,7 +57,7 @@ public class ClaimBattlePassRewardRequest
public int? Level { get; set; } // "level"
public string? NodeId { get; set; } // "nodeId"
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
}
@@ -66,7 +66,7 @@ public class PurchaseBattlePassPremiumRequest
public string? IdempotencyKey { get; set; } // "idempotencyKey"
public string? NodeId { get; set; } // "nodeId"
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
public string RunId { get; }
public string ScenarioId { get; }
public string ScenarioSlug { get; }
public string NodeId { get; }
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);
// 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
// ids of quests completed by THIS report
// slugs of quests completed by THIS report
public Task<IReadOnlyList<string>> ReportProgressAsync(
string metric, long amount, CancellationToken cancellationToken = default);
```
@@ -27,7 +27,7 @@ public Task<IReadOnlyList<string>> ReportProgressAsync(
```csharp
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}"
}
```
@@ -40,10 +40,10 @@ metric is a custom string passed to `ReportProgressAsync`.
```csharp
public class Quest
{
public string? Id { get; set; } // "id"
public string? Name { get; set; } // "name"
public List<QuestObjectiveProgress>? Objectives { get; set; }// "objectives"
public List<Reward>? Rewards { get; set; } // "rewards"
public string? Slug { get; set; } // "slug" — stable across environments
public string? Status { get; set; } // "status"
}
@@ -78,7 +78,7 @@ client.Effects.OnNotification += n =>
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
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 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 void ReportProgress(string objectiveId, long amount = 1);
```
@@ -166,7 +166,7 @@ completed counter response may carry the next `PendingEffect`.
### `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.
```csharp
@@ -199,13 +199,13 @@ configured level.
public sealed class ScenarioCompletedEffect
{
public string RunId { get; }
public string ScenarioId { get; }
public string ScenarioSlug { get; }
}
public sealed class ScenarioFailedEffect
{
public string RunId { get; }
public string ScenarioId { get; }
public string ScenarioSlug { get; }
public string NodeId { get; }
public Exception Exception { get; }
}
@@ -219,7 +219,7 @@ advanced a wait, the run expired, or another device completed it.
## Reliability
- 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`,
`onPremiumPurchase`. A non-null `effect` in the response is ingested as
the next node; a null/missing effect completes the run.
@@ -243,7 +243,7 @@ public class PendingEffect
public JToken Data { get; set; }
public string NodeId { get; set; }
public string RunId { get; set; }
public string ScenarioId { get; set; }
public string ScenarioSlug { get; set; }
public string Type { get; set; }
public DateTimeOffset? WaitDeadline { get; set; }
}
@@ -257,7 +257,7 @@ public class HandleScenarioCallbackRequest
public string? Handle { get; set; }
public string? NodeId { 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; } }
@@ -267,7 +267,7 @@ public class UpdateScenarioCounterRequest
public string? CounterKey { get; set; }
public string? NodeId { get; set; }
public string? RunId { get; set; }
public string? ScenarioId { get; set; }
public string? ScenarioSlug { get; set; }
}
public class UpdateScenarioCounterResponse
{
+4 -3
View File
@@ -14,11 +14,11 @@ public Task<IReadOnlyList<Store>> ListAsync(CancellationToken cancellationToken
// GET /sdk/v1/stores/{slug}
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
// make retries safe.
public Task<PurchaseOfferResponse> PurchaseAsync(
string storeSlug, string offerId, string? idempotencyKey = null,
string storeSlug, string offerSlug, string? idempotencyKey = null,
CancellationToken cancellationToken = default);
```
@@ -44,6 +44,7 @@ public class Store
public class Offer
{
public string? Id { get; set; } // "id"
public string? Slug { get; set; } // "slug" — stable across environments
public string? Name { get; set; } // "name"
public OfferPrice? Price { get; set; } // "price"
public List<OfferContent>? Contents { get; set; }// "contents"
@@ -65,5 +66,5 @@ public class PurchaseOfferResponse
`PurchaseOfferResponse.Error` carries a business error string when
`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.
@@ -103,7 +103,7 @@ public sealed class ScenarioServiceTests
Assert.NotNull(completed);
Assert.Equal("run-1", completed!.RunId);
Assert.Equal("scenario-1", completed.ScenarioId);
Assert.Equal("scenario-1", completed.ScenarioSlug);
}
[Fact]
@@ -522,7 +522,7 @@ public sealed class ScenarioServiceTests
return new PendingEffect
{
RunId = runId,
ScenarioId = "scenario-1",
ScenarioSlug = "scenario-1",
NodeId = nodeId,
Type = type,
Data = data == null ? new JObject() : JObject.FromObject(data),