2.0.0: Rudder.Core 2.0.0 DLL, samples and skill docs on slugs and environments
CI / check (push) Successful in 2s
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:33 +03:00
parent 01e27bde2c
commit b18c59450c
11 changed files with 73 additions and 29 deletions
+3 -3
View File
@@ -113,7 +113,7 @@ save format in a wrapper type.
```csharp ```csharp
var stores = await client.Stores.ListAsync(); var stores = await client.Stores.ListAsync();
var result = await client.Stores.PurchaseAsync(storeSlug, offerId); var result = await client.Stores.PurchaseAsync(storeSlug, offerSlug);
if (result != null && result.Success != true) { /* result.Error */ } if (result != null && result.Success != true) { /* result.Error */ }
var items = await client.Inventory.GetAsync(); var items = await client.Inventory.GetAsync();
``` ```
@@ -165,7 +165,7 @@ effects through the `On*` events.
```csharp ```csharp
var quests = await client.Quests.ListAsync(); var quests = await client.Quests.ListAsync();
await client.Quests.ClaimAsync(quest.Id); await client.Quests.ClaimAsync(quest.Slug);
var completedIds = await client.Quests.ReportProgressAsync("kills", 1); var completedIds = await client.Quests.ReportProgressAsync("kills", 1);
``` ```
@@ -175,7 +175,7 @@ Store purchases already report `purchase.offer:<id>` / `purchase.item:<id>`.
Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect` Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect`
from `Effects.OnBattlePass` (it binds scenario/node/run ids). Direct from `Effects.OnBattlePass` (it binds scenario/node/run ids). Direct
`BattlePass.GetProgressAsync(scenarioId, nodeId)` needs those ids. Tracks: `BattlePass.GetProgressAsync(scenarioSlug, nodeId)` needs those ids. Tracks:
`BattlePassService.TrackFree` / `TrackPremium`. `BattlePassService.TrackFree` / `TrackPremium`.
## Errors ## Errors
+32
View File
@@ -1,5 +1,37 @@
# Changelog # Changelog
## 2.0.0
Breaking changes, following Rudder.Core 2.0.0 and the backend environments
release. The bundled `Runtime/Plugins/Rudder.Core.dll` is rebuilt from
Rudder.Core 2.0.0.
A project now has exactly two environments, `staging` and `prod`. The SDK key
set in `RudderConfiguration` belongs to one of them, so the environment is
resolved at login and carried inside the tokens; nothing in the API takes an
environment argument and players do not cross between environments. Tokens
saved to PlayerPrefs by 1.x have no environment claim and are rejected with
401: the refresh fails, the stored tokens are cleared and
`Auth.AuthStateChanged` fires `SignedOut`, so call `LoginWithDeviceAsync`
again on that event.
Quests, scenarios and offers are addressed by slug instead of id, because ids
differ between staging and prod. `Quest.Id` became `Quest.Slug`,
`Quests.ClaimAsync` takes a quest slug, `Quests.ReportProgressAsync` returns
quest slugs, `Stores.PurchaseAsync` takes an offer slug and `Offer` carries a
`Slug`, `QuestMetrics.PurchaseOffer` builds its metric from the offer slug,
and every scenario-scoped effect and request exposes `ScenarioSlug` instead of
`ScenarioId` (`PendingEffect`, `ScenarioCompletedEffect`,
`ScenarioFailedEffect`, the battle pass requests,
`BattlePass.GetProgressAsync`). `Store.ScenarioId` is unchanged — it is an
authoring reference, not a player-facing one. The Store/Inventory and
Scenarios samples were updated to `offer.Slug`.
Also inherited from Core: the effects client reconciles against every pending
poll, so a run that disappeared server-side emits `OnScenarioCompleted`
instead of lingering, and `run_not_active` is a terminal rejection that drops
the run and emits `OnScenarioFailed`.
## 1.0.0 ## 1.0.0
Breaking changes, following the Rudder.Core 1.0.0 rework: Breaking changes, following the Rudder.Core 1.0.0 rework:
Binary file not shown.
@@ -81,8 +81,8 @@ namespace RudderSdk.Unity.Samples
if (offer == null) if (offer == null)
throw new InvalidOperationException("Store " + storeSlug + " has no offers."); throw new InvalidOperationException("Store " + storeSlug + " has no offers.");
_page.Log("Stores.PurchaseAsync(" + storeSlug + ", " + offer.Id + ")"); _page.Log("Stores.PurchaseAsync(" + storeSlug + ", " + offer.Slug + ")");
var response = await client.Stores.PurchaseAsync(storeSlug, offer.Id); var response = await client.Stores.PurchaseAsync(storeSlug, offer.Slug);
if (response != null && response.Success != true) if (response != null && response.Success != true)
throw new InvalidOperationException(response.Error ?? "Purchase rejected"); throw new InvalidOperationException(response.Error ?? "Purchase rejected");
@@ -55,11 +55,11 @@ namespace RudderSdk.Unity.Samples
foreach (var offer in store.Offers) foreach (var offer in store.Offers)
{ {
var slug = store.Slug; var slug = store.Slug;
var offerId = offer.Id; var offerSlug = offer.Slug;
var name = string.IsNullOrEmpty(offer.Name) ? offer.Id : offer.Name; var name = string.IsNullOrEmpty(offer.Name) ? offer.Slug : offer.Name;
var price = offer.Price == null ? "free" : offer.Price.Amount + " " + offer.Price.Currency; var price = offer.Price == null ? "free" : offer.Price.Amount + " " + offer.Price.Currency;
if (_page.Button("Buy " + name + " — " + price)) if (_page.Button("Buy " + name + " — " + price))
_page.Run(this, () => Buy(slug, offerId, name)); _page.Run(this, () => Buy(slug, offerSlug, name));
} }
} }
}); });
@@ -74,11 +74,11 @@ namespace RudderSdk.Unity.Samples
_page.SetStatus(_stores.Count == 0 ? "No stores" : "Loaded " + _stores.Count + " store(s)"); _page.SetStatus(_stores.Count == 0 ? "No stores" : "Loaded " + _stores.Count + " store(s)");
} }
async Task Buy(string storeSlug, string offerId, string name) async Task Buy(string storeSlug, string offerSlug, string name)
{ {
var client = Rudder.Initialize(); var client = Rudder.Initialize();
_page.Log("Stores.PurchaseAsync(" + storeSlug + ", " + offerId + ")"); _page.Log("Stores.PurchaseAsync(" + storeSlug + ", " + offerSlug + ")");
var response = await client.Stores.PurchaseAsync(storeSlug, offerId); var response = await client.Stores.PurchaseAsync(storeSlug, offerSlug);
if (response != null && response.Success != true) if (response != null && response.Success != true)
throw new InvalidOperationException(response.Error ?? "Purchase rejected"); throw new InvalidOperationException(response.Error ?? "Purchase rejected");
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "rudder.sdk", "name": "rudder.sdk",
"version": "1.0.0", "version": "2.0.0",
"displayName": "Rudder SDK", "displayName": "Rudder SDK",
"description": "LiveOps SDK for Unity — player auth, remote config, stores, inventory, leaderboards, scenarios.", "description": "LiveOps SDK for Unity — player auth, remote config, stores, inventory, leaderboards, scenarios.",
"unity": "6000.0", "unity": "6000.0",
@@ -26,6 +26,18 @@ Auth service:
Login uses the SDK device id from PlayerPrefs. Do not pass Login uses the SDK device id from PlayerPrefs. Do not pass
`SystemInfo.deviceUniqueIdentifier`. `SystemInfo.deviceUniqueIdentifier`.
## Environments
A project has two environments, `staging` and `prod`. The SDK key in
`RudderConfiguration` belongs to one of them, so the environment is decided at
login and travels inside the tokens; no API takes an environment argument, and
a player created against a staging key does not exist in prod. Point a QA build
at the staging key and the live build at the prod key.
Tokens stored by SDK 1.x carry no environment claim and are rejected with 401.
The refresh then fails, PlayerPrefs tokens are cleared and `AuthStateChanged`
fires `SignedOut` — call `LoginWithDeviceAsync` again.
## Profile and wallets ## Profile and wallets
```csharp ```csharp
@@ -4,7 +4,7 @@
`Rudder.Core.dll`. There is no sample scene for this feature. `Rudder.Core.dll`. There is no sample scene for this feature.
Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect` Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect`
delivered by `Effects.OnBattlePass` — it binds `scenarioId`/`nodeId`/ delivered by `Effects.OnBattlePass` — it binds `scenarioSlug`/`nodeId`/
`runId` for you. Direct `BattlePassService` calls need those ids manually. `runId` for you. Direct `BattlePassService` calls need those ids manually.
## BattlePassEffect (preferred) ## BattlePassEffect (preferred)
@@ -24,20 +24,20 @@ client.Effects.OnBattlePass += async effect =>
- `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken = default)` — posts `onPremiumPurchase` on success - `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken = default)` — posts `onPremiumPurchase` on success
- `LevelUp()` / `LevelUpAsync()` — posts `onLevelUp` - `LevelUp()` / `LevelUpAsync()` — posts `onLevelUp`
- `End()` / `EndAsync()` — posts `onComplete` - `End()` / `EndAsync()` — posts `onComplete`
- `Get<T>(key, fallback)`, `RunId`, `ScenarioId`, `NodeId`, `Data` like other effects - `Get<T>(key, fallback)`, `RunId`, `ScenarioSlug`, `NodeId`, `Data` like other effects
`BattlePassLevelEffect` (from `OnBattlePassLevel`) has `Claim()` / `BattlePassLevelEffect` (from `OnBattlePassLevel`) has `Claim()` /
`ClaimAsync()` (posts `onComplete`) and a `Level` property. `ClaimAsync()` (posts `onComplete`) and a `Level` property.
## BattlePassService (direct) ## BattlePassService (direct)
- `Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioId, string nodeId, CancellationToken = default)` - `Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioSlug, string nodeId, CancellationToken = default)`
- `Task<AddBattlePassXpResponse> AddXpAsync(AddBattlePassXpRequest request, CancellationToken = default)` - `Task<AddBattlePassXpResponse> AddXpAsync(AddBattlePassXpRequest request, CancellationToken = default)`
- `Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(ClaimBattlePassRewardRequest request, CancellationToken = default)` - `Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(ClaimBattlePassRewardRequest request, CancellationToken = default)`
- `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(PurchaseBattlePassPremiumRequest request, CancellationToken = default)` - `Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(PurchaseBattlePassPremiumRequest request, CancellationToken = default)`
- Track constants: `BattlePassService.TrackFree`, `BattlePassService.TrackPremium` (static fields). - Track constants: `BattlePassService.TrackFree`, `BattlePassService.TrackPremium` (static fields).
Request models carry `ScenarioId`, `NodeId`, and (except progress) Request models carry `ScenarioSlug`, `NodeId`, and (except progress)
`RunId`; `PurchaseBattlePassPremiumRequest` also takes `IdempotencyKey`. `RunId`; `PurchaseBattlePassPremiumRequest` also takes `IdempotencyKey`.
## Response models ## Response models
+6 -6
View File
@@ -7,19 +7,19 @@ from scenario `OnQuest` nodes (`QuestEffect`, see
```csharp ```csharp
var quests = await client.Quests.ListAsync(); var quests = await client.Quests.ListAsync();
var claim = await client.Quests.ClaimAsync(quest.Id); var claim = await client.Quests.ClaimAsync(quest.Slug);
var completedIds = await client.Quests.ReportProgressAsync("kills", 1); var completedSlugs = await client.Quests.ReportProgressAsync("kills", 1);
``` ```
API: API:
- `Task<IReadOnlyList<Quest>> ListAsync(CancellationToken = default)` - `Task<IReadOnlyList<Quest>> ListAsync(CancellationToken = default)`
- `Task<ClaimQuestResponse> ClaimAsync(string questId, CancellationToken = default)` - `Task<ClaimQuestResponse> ClaimAsync(string questSlug, CancellationToken = default)`
- `Task<IReadOnlyList<string>> ReportProgressAsync(string metric, long amount, CancellationToken = default)` — returns the ids of quests completed by this report. - `Task<IReadOnlyList<string>> ReportProgressAsync(string metric, long amount, CancellationToken = default)` — returns the slugs of quests completed by this report.
Models: Models:
- `Quest``Id`, `Name`, `Status`, `Objectives` - `Quest``Slug`, `Name`, `Status`, `Objectives`
(`List<QuestObjectiveProgress>`), `Rewards` (`List<Reward>`). (`List<QuestObjectiveProgress>`), `Rewards` (`List<Reward>`).
- `QuestObjectiveProgress``ObjectiveId`, `Metric`, `Current` (`long?`), - `QuestObjectiveProgress``ObjectiveId`, `Metric`, `Current` (`long?`),
`Target` (`long?`), `Completed` (`bool?`). `Target` (`long?`), `Completed` (`bool?`).
@@ -28,6 +28,6 @@ Models:
`Granted` (`List<Reward>`), `Error`. `Granted` (`List<Reward>`), `Error`.
Store purchases already report metrics automatically; the metric strings are Store purchases already report metrics automatically; the metric strings are
built by `QuestMetrics.PurchaseOffer(offerId)``purchase.offer:<offerId>` built by `QuestMetrics.PurchaseOffer(offerSlug)``purchase.offer:<offerSlug>`
and `QuestMetrics.PurchaseItem(itemId)``purchase.item:<itemId>` and `QuestMetrics.PurchaseItem(itemId)``purchase.item:<itemId>`
(`RudderSdk.Core.QuestMetrics`, static). (`RudderSdk.Core.QuestMetrics`, static).
@@ -27,7 +27,7 @@ are dispatched the same way.
## Events and effects ## Events and effects
All effect types expose `RunId`, `ScenarioId`, `NodeId`, `Data` (`JObject`), All effect types expose `RunId`, `ScenarioSlug`, `NodeId`, `Data` (`JObject`),
and `T Get<T>(string key, T defaultValue = default)` for node data. and `T Get<T>(string key, T defaultValue = default)` for node data.
| Event | Effect | Game must | | Event | Effect | Game must |
@@ -39,8 +39,8 @@ and `T Get<T>(string key, T defaultValue = default)` for node data.
| `OnLeaderboard` | `LeaderboardEffect` | `End()` / `EndAsync()`, `Claim()` / `ClaimAsync()`. `IsResolved` marks resolution. | | `OnLeaderboard` | `LeaderboardEffect` | `End()` / `EndAsync()`, `Claim()` / `ClaimAsync()`. `IsResolved` marks resolution. |
| `OnBattlePass` | `BattlePassEffect` | Drive the effect — see [battlepass.md](battlepass.md). | | `OnBattlePass` | `BattlePassEffect` | Drive the effect — see [battlepass.md](battlepass.md). |
| `OnBattlePassLevel` | `BattlePassLevelEffect` | `Claim()` / `ClaimAsync()` for the level reward; `Level` prop. | | `OnBattlePassLevel` | `BattlePassLevelEffect` | `Claim()` / `ClaimAsync()` for the level reward; `Level` prop. |
| `OnScenarioCompleted` | `ScenarioCompletedEffect` | `RunId`, `ScenarioId`. Log / surface. | | `OnScenarioCompleted` | `ScenarioCompletedEffect` | `RunId`, `ScenarioSlug`. Log / surface. |
| `OnScenarioFailed` | `ScenarioFailedEffect` | `RunId`, `ScenarioId`, `NodeId`, `Exception`. Log / surface. | | `OnScenarioFailed` | `ScenarioFailedEffect` | `RunId`, `ScenarioSlug`, `NodeId`, `Exception`. Log / surface. |
Async and sync variants are equivalent; the sync variants fire-and-forget Async and sync variants are equivalent; the sync variants fire-and-forget
the same server callback. The event name must match a scenario trigger the same server callback. The event name must match a scenario trigger
@@ -7,21 +7,21 @@ Verified against the bundled `Rudder.Core.dll` and
```csharp ```csharp
var stores = await client.Stores.ListAsync(); var stores = await client.Stores.ListAsync();
var response = await client.Stores.PurchaseAsync(storeSlug, offerId); var response = await client.Stores.PurchaseAsync(storeSlug, offerSlug);
if (response != null && response.Success != true) if (response != null && response.Success != true)
throw new InvalidOperationException(response.Error ?? "Purchase rejected"); throw new InvalidOperationException(response.Error ?? "Purchase rejected");
``` ```
- `Task<IReadOnlyList<Store>> ListAsync(CancellationToken = default)` - `Task<IReadOnlyList<Store>> ListAsync(CancellationToken = default)`
- `Task<Store> GetAsync(string slug, CancellationToken = default)` - `Task<Store> GetAsync(string slug, CancellationToken = default)`
- `Task<PurchaseOfferResponse> PurchaseAsync(string storeSlug, string offerId, string idempotencyKey = null, CancellationToken = default)` — generates an idempotency key when omitted. - `Task<PurchaseOfferResponse> PurchaseAsync(string storeSlug, string offerSlug, string idempotencyKey = null, CancellationToken = default)` — generates an idempotency key when omitted.
Models: Models:
- `Store``Id`, `Slug`, `Name`, `Description`, `Status`, `Environment`, - `Store``Id`, `Slug`, `Name`, `Description`, `Status`, `Environment`,
`ProjectId`, `ScenarioId`, `Data` (`JToken`), `Offers` (`List<Offer>`), `ProjectId`, `ScenarioId`, `Data` (`JToken`), `Offers` (`List<Offer>`),
`CreatedAt`, `UpdatedAt`. `CreatedAt`, `UpdatedAt`.
- `Offer``Id`, `Name`, `Price` (`OfferPrice`), `Contents` - `Offer``Id`, `Slug`, `Name`, `Price` (`OfferPrice`), `Contents`
(`List<OfferContent>`), `MaxPurchases` (`int?`), `CreatedAt`, `UpdatedAt`. (`List<OfferContent>`), `MaxPurchases` (`int?`), `CreatedAt`, `UpdatedAt`.
- `OfferPrice``Currency`, `Amount` (`long?`). - `OfferPrice``Currency`, `Amount` (`long?`).
- `OfferContent``ItemId`, `Amount` (`long?`). - `OfferContent``ItemId`, `Amount` (`long?`).
@@ -30,7 +30,7 @@ Models:
Check `Success != true` (nullable bool), not `!Success`. After a purchase, Check `Success != true` (nullable bool), not `!Success`. After a purchase,
reload wallets (`Player.GetProfileAsync`) and inventory — there is no reload wallets (`Player.GetProfileAsync`) and inventory — there is no
`onChange` callback. Purchases auto-report quest metrics `onChange` callback. Purchases auto-report quest metrics
`purchase.offer:<offerId>` / `purchase.item:<itemId>` (see `purchase.offer:<offerSlug>` / `purchase.item:<itemId>` (see
[quests.md](quests.md)). The store slug must exist in the dashboard, else 404. [quests.md](quests.md)). The store slug must exist in the dashboard, else 404.
## Inventory — `client.Inventory` (`InventoryService`) ## Inventory — `client.Inventory` (`InventoryService`)