From 6d115f158eb3454a688bc07373ac803f000df898 Mon Sep 17 00:00:00 2001 From: edmand46 Date: Fri, 4 Sep 2026 14:23:38 +0300 Subject: [PATCH] Server-side scenario execution: new Rudder.Core.dll, realtime/planstore glue removed - Rudder.Core.dll rebuilt from csharp-sdk effects rewrite (96,768 bytes) - Realtime/ adapters, UnityRealtimeTransportFactory, UnityPlanStateStore, UnityPlanScheduler, WebGL jslib and RealtimeUrl config deleted (with .meta) - Scenarios sample rewired to client.Effects; samples login path updated - AGENTS.md/README/CHANGELOG/package docs + agent skill updated; realtime.md skill doc removed --- Assets/LiveOpsLocal.asset.example | 1 - Packages/rudder.sdk/AGENTS.md | 243 ++++++++++++++ Packages/rudder.sdk/AGENTS.md.meta | 7 + Packages/rudder.sdk/CHANGELOG.md | 36 ++ Packages/rudder.sdk/README.md | 51 ++- .../Runtime/Plugins/Rudder.Core.dll | Bin 115712 -> 96768 bytes .../Plugins/WebGL/RudderWebSocket.jslib | 66 ---- .../Plugins/WebGL/RudderWebSocket.jslib.meta | 2 - .../Platform/RudderUnityClientFactory.cs | 6 +- .../Platform/RudderUnityClientOptions.cs | 1 - .../Sources/Platform/UnityPlanScheduler.cs | 15 - .../Platform/UnityPlanScheduler.cs.meta | 2 - .../Sources/Platform/UnityPlanStateStore.cs | 30 -- .../Platform/UnityPlanStateStore.cs.meta | 2 - .../Platform/UnityRealtimeTransportFactory.cs | 51 --- .../UnityRealtimeTransportFactory.cs.meta | 2 - .../Sources/Realtime/IWebSocketAdapter.cs | 22 -- .../Realtime/IWebSocketAdapter.cs.meta | 2 - .../Sources/Realtime/JsWebSocketAdapter.cs | 312 ------------------ .../Realtime/JsWebSocketAdapter.cs.meta | 2 - .../Realtime/NativeWebSocketAdapter.cs | 233 ------------- .../Realtime/NativeWebSocketAdapter.cs.meta | 2 - Packages/rudder.sdk/Runtime/Sources/Rudder.cs | 150 ++++----- .../Runtime/Sources/RudderConfiguration.cs | 3 - .../Authentication.meta} | 2 +- .../Authentication/Authentication.unity | 303 +++++++++++++++++ .../Authentication/Authentication.unity.meta | 7 + .../Authentication/AuthenticationSample.cs | 87 +++++ .../AuthenticationSample.cs.meta | 11 + .../Leaderboards.meta} | 2 +- .../Samples~/Leaderboards/Leaderboards.unity | 304 +++++++++++++++++ .../Leaderboards/Leaderboards.unity.meta | 7 + .../Leaderboards/LeaderboardsSample.cs | 93 ++++++ .../Leaderboards/LeaderboardsSample.cs.meta | 11 + Packages/rudder.sdk/Samples~/README.md | 20 ++ Packages/rudder.sdk/Samples~/README.md.meta | 7 + .../rudder.sdk/Samples~/RemoteConfig.meta | 8 + .../Samples~/RemoteConfig/RemoteConfig.unity | 305 +++++++++++++++++ .../RemoteConfig/RemoteConfig.unity.meta | 7 + .../RemoteConfig/RemoteConfigSample.cs | 60 ++++ .../RemoteConfig/RemoteConfigSample.cs.meta | 11 + .../Samples~/Rudder.Unity.Samples.asmdef | 16 + .../Samples~/Rudder.Unity.Samples.asmdef.meta | 7 + Packages/rudder.sdk/Samples~/Scenarios.meta | 8 + .../Samples~/Scenarios/Scenarios.unity | 305 +++++++++++++++++ .../Samples~/Scenarios/Scenarios.unity.meta | 7 + .../Samples~/Scenarios/ScenariosSample.cs | 201 +++++++++++ .../Scenarios/ScenariosSample.cs.meta | 11 + Packages/rudder.sdk/Samples~/Shared.meta | 8 + .../rudder.sdk/Samples~/Shared/SamplePage.cs | 143 ++++++++ .../Samples~/Shared/SamplePage.cs.meta | 11 + Packages/rudder.sdk/Samples~/Storage.meta | 8 + .../rudder.sdk/Samples~/Storage/Storage.unity | 305 +++++++++++++++++ .../Samples~/Storage/Storage.unity.meta | 7 + .../Samples~/Storage/StorageSample.cs | 105 ++++++ .../Samples~/Storage/StorageSample.cs.meta | 11 + .../rudder.sdk/Samples~/StoreInventory.meta | 8 + .../StoreInventory/StoreInventory.unity | 303 +++++++++++++++++ .../StoreInventory/StoreInventory.unity.meta | 7 + .../StoreInventory/StoreInventorySample.cs | 125 +++++++ .../StoreInventorySample.cs.meta | 11 + Packages/rudder.sdk/package.json | 13 +- README.md | 84 ++--- skills/rudder-unity-sdk/SKILL.md | 17 +- .../reference/authentication.md | 12 +- .../rudder-unity-sdk/reference/battlepass.md | 31 +- .../rudder-unity-sdk/reference/bootstrap.md | 24 +- skills/rudder-unity-sdk/reference/quests.md | 2 +- skills/rudder-unity-sdk/reference/realtime.md | 36 -- .../reference/remote-config.md | 5 +- .../rudder-unity-sdk/reference/scenarios.md | 55 ++- 71 files changed, 3338 insertions(+), 1034 deletions(-) create mode 100644 Packages/rudder.sdk/AGENTS.md create mode 100644 Packages/rudder.sdk/AGENTS.md.meta delete mode 100644 Packages/rudder.sdk/Runtime/Plugins/WebGL/RudderWebSocket.jslib delete mode 100644 Packages/rudder.sdk/Runtime/Plugins/WebGL/RudderWebSocket.jslib.meta delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanScheduler.cs delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanScheduler.cs.meta delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanStateStore.cs delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanStateStore.cs.meta delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Platform/UnityRealtimeTransportFactory.cs delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Platform/UnityRealtimeTransportFactory.cs.meta delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Realtime/IWebSocketAdapter.cs delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Realtime/IWebSocketAdapter.cs.meta delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Realtime/JsWebSocketAdapter.cs delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Realtime/JsWebSocketAdapter.cs.meta delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Realtime/NativeWebSocketAdapter.cs delete mode 100644 Packages/rudder.sdk/Runtime/Sources/Realtime/NativeWebSocketAdapter.cs.meta rename Packages/rudder.sdk/{Runtime/Plugins/WebGL.meta => Samples~/Authentication.meta} (77%) create mode 100644 Packages/rudder.sdk/Samples~/Authentication/Authentication.unity create mode 100644 Packages/rudder.sdk/Samples~/Authentication/Authentication.unity.meta create mode 100644 Packages/rudder.sdk/Samples~/Authentication/AuthenticationSample.cs create mode 100644 Packages/rudder.sdk/Samples~/Authentication/AuthenticationSample.cs.meta rename Packages/rudder.sdk/{Runtime/Sources/Realtime.meta => Samples~/Leaderboards.meta} (77%) create mode 100644 Packages/rudder.sdk/Samples~/Leaderboards/Leaderboards.unity create mode 100644 Packages/rudder.sdk/Samples~/Leaderboards/Leaderboards.unity.meta create mode 100644 Packages/rudder.sdk/Samples~/Leaderboards/LeaderboardsSample.cs create mode 100644 Packages/rudder.sdk/Samples~/Leaderboards/LeaderboardsSample.cs.meta create mode 100644 Packages/rudder.sdk/Samples~/README.md create mode 100644 Packages/rudder.sdk/Samples~/README.md.meta create mode 100644 Packages/rudder.sdk/Samples~/RemoteConfig.meta create mode 100644 Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfig.unity create mode 100644 Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfig.unity.meta create mode 100644 Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfigSample.cs create mode 100644 Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfigSample.cs.meta create mode 100644 Packages/rudder.sdk/Samples~/Rudder.Unity.Samples.asmdef create mode 100644 Packages/rudder.sdk/Samples~/Rudder.Unity.Samples.asmdef.meta create mode 100644 Packages/rudder.sdk/Samples~/Scenarios.meta create mode 100644 Packages/rudder.sdk/Samples~/Scenarios/Scenarios.unity create mode 100644 Packages/rudder.sdk/Samples~/Scenarios/Scenarios.unity.meta create mode 100644 Packages/rudder.sdk/Samples~/Scenarios/ScenariosSample.cs create mode 100644 Packages/rudder.sdk/Samples~/Scenarios/ScenariosSample.cs.meta create mode 100644 Packages/rudder.sdk/Samples~/Shared.meta create mode 100644 Packages/rudder.sdk/Samples~/Shared/SamplePage.cs create mode 100644 Packages/rudder.sdk/Samples~/Shared/SamplePage.cs.meta create mode 100644 Packages/rudder.sdk/Samples~/Storage.meta create mode 100644 Packages/rudder.sdk/Samples~/Storage/Storage.unity create mode 100644 Packages/rudder.sdk/Samples~/Storage/Storage.unity.meta create mode 100644 Packages/rudder.sdk/Samples~/Storage/StorageSample.cs create mode 100644 Packages/rudder.sdk/Samples~/Storage/StorageSample.cs.meta create mode 100644 Packages/rudder.sdk/Samples~/StoreInventory.meta create mode 100644 Packages/rudder.sdk/Samples~/StoreInventory/StoreInventory.unity create mode 100644 Packages/rudder.sdk/Samples~/StoreInventory/StoreInventory.unity.meta create mode 100644 Packages/rudder.sdk/Samples~/StoreInventory/StoreInventorySample.cs create mode 100644 Packages/rudder.sdk/Samples~/StoreInventory/StoreInventorySample.cs.meta delete mode 100644 skills/rudder-unity-sdk/reference/realtime.md diff --git a/Assets/LiveOpsLocal.asset.example b/Assets/LiveOpsLocal.asset.example index f1f1388..a5f4422 100644 --- a/Assets/LiveOpsLocal.asset.example +++ b/Assets/LiveOpsLocal.asset.example @@ -14,5 +14,4 @@ MonoBehaviour: m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.RudderConfiguration ProjectKey: YOUR_PROJECT_KEY BaseUrl: https://api.rudder.build - RealtimeUrl: wss://realtime.rudder.build/api/realtime/ws TimeoutSeconds: 10 diff --git a/Packages/rudder.sdk/AGENTS.md b/Packages/rudder.sdk/AGENTS.md new file mode 100644 index 0000000..310cdf0 --- /dev/null +++ b/Packages/rudder.sdk/AGENTS.md @@ -0,0 +1,243 @@ +# Rudder Unity SDK — agent integration guide + +This file is for coding agents adding `rudder.sdk` to a Unity game. Read it +before writing integration code. The public API is `Rudder` + `RudderClient`; +verify signatures in this package and in `Rudder.Core.dll` (`RudderSdk.Core`), +not from memory. + +Human-facing install notes live in `README.md`. Copy-paste scenes live in +`Samples~/` (Package Manager → Rudder SDK → Feature Samples). + +## Goal + +Wire LiveOps into the existing game: initialize once, log in with the device, +then call the feature the user asked for. Do not build a second game, a +debug dashboard, or a wrapper layer around the SDK. + +## Install + +Add the scoped registry and both packages to `Packages/manifest.json`: + +```json +{ + "scopedRegistries": [ + { + "name": "Rudder", + "url": "https://hub.rudder.build/api/packages/rudder/npm/", + "scopes": ["rudder"] + } + ], + "dependencies": { + "rudder.sdk": "0.4.0", + "com.unity.nuget.newtonsoft-json": "3.2.2" + } +} +``` + +`com.unity.nuget.newtonsoft-json` is required. The package does not bundle +Newtonsoft. Unity `6000.0` or newer. + +Namespaces: + +- `RudderSdk.Unity` — `Rudder`, `RudderConfiguration`, `RudderState` +- `RudderSdk.Core` — `RudderClient` and services +- `RudderSdk.Core.Models.*` — `PlayerProfile`, `Offer`, `StorageItem`, `RankEntry`, … + +## Bootstrap (do this once) + +1. `Assets > Create > Rudder > Configuration` +2. Set `ProjectKey` (required). Defaults: `BaseUrl` `https://api.rudder.build`, + timeout 10s. +3. Add the `Rudder` component to a startup scene and assign the asset. +4. Get the Core client, then authorize: + +```csharp +using RudderSdk.Unity; + +var client = Rudder.Initialize(); +await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player"); +``` + +`Initialize()` is synchronous: it reads the serialized configuration and +creates `RudderClient`. It does not create a GameObject — the component must +already be in the scene. Calling it again returns the same client. + +`Auth.LoginWithDeviceAsync` logs in with the device id. After that, all +features hang off `client` / `Rudder.Client`. Pending scenario effects are +fetched automatically (heartbeat after sign-in, and immediately from +`TriggerAsync`). + +Login uses the SDK device id (PlayerPrefs). Do not pass +`SystemInfo.deviceUniqueIdentifier`. Tokens persist in PlayerPrefs; a second +launch can still call `LoginWithDeviceAsync` (it refreshes the session). + +Do not call `client.Update`. The `Rudder` component pumps it every frame +(effects heartbeat and wait-deadline checks need that). + +Subscribe to `client.Effects.On*` before `Scenario.TriggerAsync`. + +## Feature map + +Call sites hang off `Rudder.Client`. Match the user's request to one row. +Copy the matching scene under `Samples~/` if the shape is unclear. + +| Need | API | Sample | +| --- | --- | --- | +| Sign in / profile / wallets | `Auth.LoginWithDeviceAsync`, `Auth.Logout`, `Player.GetProfileAsync` | `Samples~/Authentication` | +| Tunables | `RemoteConfig.LoadAsync`, `Get(key, fallback)` | `Samples~/RemoteConfig` | +| Cloud save | `Storage.GetAsync` / `SaveAsync` / `DeleteAsync` | `Samples~/Storage` | +| Shop + inventory | `Stores.ListAsync`, `PurchaseAsync`, `Inventory.GetAsync` | `Samples~/StoreInventory` | +| High scores | `Leaderboards.FindBySlug(slug).SubmitAsync` / `ListAsync` | `Samples~/Leaderboards` | +| LiveOps plans | `Scenario.TriggerAsync`, `Effects.OnNotification`, `Effects.OnStoreOffer`, … | `Samples~/Scenarios` | +| Global quests | `Quests.ListAsync`, `ClaimAsync`, `ReportProgressAsync` | (no sample; see below) | +| Battle pass | `BattlePass.*` with scenario/node ids, or `BattlePassEffect` | (no sample; see below) | + +### Remote config + +```csharp +await client.RemoteConfig.LoadAsync(); +var speed = client.RemoteConfig.Get("player_speed", 5f); +``` + +`Get` reads the cache. Call `LoadAsync` first (or `GetAsync`, which loads on +first use). + +### Storage + +`StorageItem` is `{ Type, Id, Data }` where `Data` is an opaque JSON string. +`GetAsync(type)` pages; `SaveAsync` upserts; `DeleteAsync(type)` deletes that +type. Prefer `JsonUtility` or Newtonsoft on `Data` — do not invent a second +save format in a wrapper type. + +### Stores and inventory + +```csharp +var stores = await client.Stores.ListAsync(); +var result = await client.Stores.PurchaseAsync(storeSlug, offerId); +if (result != null && result.Success != true) { /* result.Error */ } +var items = await client.Inventory.GetAsync(); +``` + +`PurchaseAsync` generates an idempotency key when omitted. After a purchase, +reload wallets (`Player.GetProfileAsync`) and inventory — there is no +`onChange` callback. Use `Offer`, `Store`, `PlayerInventoryItem` as-is. + +### Leaderboards + +```csharp +var board = client.Leaderboards.FindBySlug("my-board"); +await board.SubmitAsync(score); +var top = await board.ListAsync(10); +``` + +### Scenarios + +Scenario graphs run on the server. The client triggers an event and +surfaces pending effects. Subscribe first, then trigger an event name the +project configured (examples: `player_login`, `demo_round_finished`). + +```csharp +client.Effects.OnNotification += effect => { /* show UI, then effect.Done() */ }; +await client.Scenario.TriggerAsync("player_login"); +``` + +`Scenario.TriggerAsync(string eventName)` posts the event and ingests any +effects the server returns. The `Rudder` component's per-frame +`client.Update` also heartbeats `GET /sdk/v1/scenarios/pending` (every 30s +while signed in) so effects that land later still arrive. + +| Event | Game must | +| --- | --- | +| `OnNotification` | Show UI, then `effect.Done()` / `DoneAsync()` | +| `OnStoreOffer` | Show offer; `Stores.PurchaseAsync` then `effect.Purchase()`, or `effect.Decline()` | +| `OnWait` | Deadline only (`DeadlineUtc`); the server advances the run | +| `OnQuest` | Scenario quest node (`QuestEffect.ReportProgress`), not `client.Quests` | +| `OnLeaderboard` | `effect.End()` / `effect.Claim()` | +| `OnBattlePass` / `OnBattlePassLevel` | Drive the effect (`Claim` on a level effect) | +| `OnScenarioCompleted` / `OnScenarioFailed` | Log / surface | + +Unsubscribe on destroy. `TriggerAsync` does not return runs — follow +effects through the `On*` events. + +### Quests (global) + +`client.Quests` is the player's quest list, distinct from `OnQuest` nodes. + +```csharp +var quests = await client.Quests.ListAsync(); +await client.Quests.ClaimAsync(quest.Id); +var completedIds = await client.Quests.ReportProgressAsync("kills", 1); +``` + +Store purchases already report `purchase.offer:` / `purchase.item:`. + +### Battle pass + +Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect` +from `Effects.OnBattlePass` (it binds scenario/node/run ids). Direct +`BattlePass.GetProgressAsync(scenarioId, nodeId)` needs those ids. Tracks: +`BattlePassService.TrackFree` / `TrackPremium`. + +## Errors + +Catch `RudderApiException` (and subclasses) from `RudderSdk.Core`: + +- `RudderAuthException` — 401; refresh failed → `AuthStateChanged(SignedOut)` +- `RudderNotFoundException` — 404 +- `RudderRateLimitException` — 429 +- `RudderNetworkException` — no response / timeout + +Fields: `StatusCode`, `Code`, `RequestId`. Surface `exception.Message` in UI. +Do not swallow errors. + +A 401 on an API call is retried once after refresh. Game code does not +implement that retry. + +## Unity rules for this SDK + +- Use the game's existing UI. Samples use IMGUI (`OnGUI`) only as a demo + shell. Do not add UI Toolkit / `UIDocument` / `PanelRenderer` just to call + Rudder. +- Call the SDK from a `MonoBehaviour` (or the game's existing service object). + Do not add a second singleton next to `Rudder`. +- Use SDK model types in game code. Do not wrap `Offer` / `PlayerProfile` / + `RankEntry` in project DTOs unless the user asks. +- Do not declare consumer-local interfaces (`IRudderStore`, …) over + `RudderClient`. +- Do not edit `Runtime/Plugins/Rudder.Core.dll` or generated models. Change + the C# SDK repo and copy the DLL if the API is wrong. +- Do not add `replace` / local project references to `Rudder.Core`. +- Game `await` must not use `ConfigureAwait(false)` so continuations return + to the Unity sync context. +- `Rudder.Initialize()` is idempotent: if `State` is `Ready`, it returns + `Rudder.Client`. +- Keep the project key out of git. Use a gitignored configuration asset + (this repo uses `Assets/LiveOpsLocal.asset`). + +## Do not + +- Invent APIs (`Rudder.Auth`, `client.Scenarios`, `BuyAsync`, + `LoginViaDeviceAsync`, `AuthorizeWithDeviceAsync` are gone). Current names: + `Rudder.Initialize()`, `client.Auth.LoginWithDeviceAsync`, + `client.Scenario.TriggerAsync`, `client.Effects`, `PurchaseAsync`. +- Port a Phaser/React store (event bus, phase machine, `Cozy*` types). +- Generate scenes from editor bake scripts. +- Call `Rudder.Initialize(configuration)` — that overload is gone. Put + `Rudder` in a scene. +- Block the main thread on HTTP. Use `async`/`await` for `LoginWithDeviceAsync` + and feature calls. +- Claim a feature works without a matching Rudder project entity (store + slug, leaderboard slug, remote-config key, scenario event). Missing + backend data is a 404, not an SDK bug. + +## Checklist after integration + +1. Configuration asset exists with a real `ProjectKey`. +2. `Rudder` is on a startup scene with that asset assigned. +3. `var client = Rudder.Initialize();` then + `await client.Auth.LoginWithDeviceAsync(...)` before other calls. +4. The requested feature uses `client.*` / `Rudder.Client.*` with SDK types. +5. Failures hit `RudderApiException` and show in the game UI. +6. Play Mode: login succeeds and the feature call returns or shows a real + API error (`LastError` / exception message), not a missing-component + throw. diff --git a/Packages/rudder.sdk/AGENTS.md.meta b/Packages/rudder.sdk/AGENTS.md.meta new file mode 100644 index 0000000..d16505b --- /dev/null +++ b/Packages/rudder.sdk/AGENTS.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7c4e9a12b8d04f6a9e3c1f5d0a2b8c47 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/CHANGELOG.md b/Packages/rudder.sdk/CHANGELOG.md index 7492f38..3c8c515 100644 --- a/Packages/rudder.sdk/CHANGELOG.md +++ b/Packages/rudder.sdk/CHANGELOG.md @@ -1,5 +1,41 @@ # Changelog +## 0.4.0 + +Breaking changes, following the Rudder.Core 0.4.0 rework: + +- Bundled `Rudder.Core.dll` updated to 0.4.0. The local scenario graph engine + is gone. `client.Scenario` is only `TriggerAsync(eventName)`. Pending nodes + surface on `client.Effects` (`OnNotification`, `OnStoreOffer`, + `OnLeaderboard`, `OnWait`, `OnQuest`, `OnBattlePass`, + `OnBattlePassLevel`, `OnScenarioCompleted`, `OnScenarioFailed`). +- Session types (`NotificationSession`, `StoreOfferSession`, …) are now + `*Effect` types. Completion methods: `Done`/`DoneAsync`, + `Purchase`/`Decline`, `End`/`Claim`, `ReportProgress`, `LevelUp`. +- `UnityPlanScheduler` removed together with the core `IPlanScheduler` + abstraction and `RudderClientOptions.Scheduler`; the factory no longer + wires a scheduler. +- `UnityPlanStateStore`, `UnityRealtimeTransportFactory`, + `Runtime/Sources/Realtime/` websocket adapters, and + `Runtime/Plugins/WebGL/RudderWebSocket.jslib` removed together with Core + `IPlanStateStore` / `IRealtimeTransport` / `RealtimeService`. +- `RudderConfiguration.RealtimeUrl` and `RudderUnityClientOptions.RealtimeUrl` + removed. `Rudder` no longer disconnects a realtime socket on quit. +- `Rudder.Update` still pumps `client.Update(Time.deltaTime)` (effects + heartbeat and wait-deadline checks). +- Samples and docs use `Auth.LoginWithDeviceAsync`. Pending effects are + fetched after sign-in via the heartbeat; `Scenario.RestoreAsync` / + `AuthorizeWithDeviceAsync` are gone. +- Core model fields are nullable now; samples check + `PurchaseResponse.Success != true` instead of `!Success`. +- Feature sample scenes for authentication, remote config, storage, stores + and inventory, leaderboards, and scenarios (`Samples~`). +- Removed the Cozy Collector example project from the Unity SDK repo. +- Added `AGENTS.md` with Unity integration rules for coding agents. +- `Rudder.Initialize()` is now synchronous, takes no arguments, and returns + `RudderClient`. Put `Rudder` in a scene with a `RudderConfiguration`. + The old `Initialize(RudderConfiguration)` (hidden GameObject) is gone. + ## 0.3.0 Breaking changes, following the Rudder.Core 0.3.0 rework: diff --git a/Packages/rudder.sdk/README.md b/Packages/rudder.sdk/README.md index bb1139f..ae6d8b9 100644 --- a/Packages/rudder.sdk/README.md +++ b/Packages/rudder.sdk/README.md @@ -1,10 +1,12 @@ # Rudder SDK for Unity LiveOps SDK for Unity: device auth, player profile, remote config, stores, -inventory, leaderboards, battle pass, quests, storage, scenarios and realtime. +inventory, leaderboards, battle pass, quests, storage and scenarios. The package wraps `Rudder.Core.dll` (the .NET SDK) with Unity adapters: -`UnityWebRequest` transport, `PlayerPrefs` token storage, websocket realtime. +`UnityWebRequest` transport and `PlayerPrefs` token storage. + +Coding agents: read `AGENTS.md` before integrating this package into a game. ## Installation @@ -21,7 +23,7 @@ your project's `Packages/manifest.json`: } ], "dependencies": { - "rudder.sdk": "0.3.0", + "rudder.sdk": "0.4.0", "com.unity.nuget.newtonsoft-json": "3.2.2" } } @@ -33,26 +35,25 @@ Newtonsoft.Json and the package does not bundle the DLL. ## Quickstart Create a configuration asset via `Assets > Create > Rudder > Configuration`, -set `ProjectKey`, `BaseUrl` and `RealtimeUrl`, then initialize once at -startup: +set `ProjectKey`, add the `Rudder` component to a startup scene, assign the +asset, then: ```csharp using RudderSdk.Unity; -Rudder.Initialize(configuration); // or add the Rudder component to a scene -var client = await Rudder.Ready; // faults if initialization failed +var client = Rudder.Initialize(); +await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player"); -var session = await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player"); var configs = await client.RemoteConfig.LoadAsync(); var stores = await client.Stores.ListAsync(); ``` All features hang off the client: `Auth`, `Player`, `BattlePass`, `Quests`, -`Stores`, `Inventory`, `Leaderboards`, `RemoteConfig`, `Scenario`, `Storage`, -`Realtime`. +`Stores`, `Inventory`, `Leaderboards`, `RemoteConfig`, `Scenario`, `Effects`, +`Storage`. ```csharp -client.Scenario.OnStoreOffer += session => { /* show offer UI */ }; +client.Effects.OnStoreOffer += effect => { /* show offer UI */ }; await client.Scenario.TriggerAsync("player_login"); var purchase = await client.Stores.PurchaseAsync("cozy-camp-shop", "moonberry-boost"); @@ -64,14 +65,36 @@ var top = await leaderboard.ListAsync(10); ## Lifecycle +- `Rudder.Initialize()` — synchronous. Reads the configuration on the scene + component and returns `RudderClient`. Idempotent. Throws if the component + or configuration is missing. +- `client.Auth.LoginWithDeviceAsync` — device login. Use this as the first + awaited call. Pending scenario effects are fetched after sign-in via the + per-frame `Update` heartbeat. - `Rudder.State` — `NotInitialized` / `Initializing` / `Ready` / `Failed`. - `Rudder.LastError` — the initialization error when `State` is `Failed`. -- `Rudder.Client` — the initialized `RudderClient`; throws until `Ready`. -- `Rudder.Ready` — task that completes with the client or the initialization - error; `Rudder.Initialized` is the event-flavored sugar over it. +- `Rudder.Client` — the initialized `RudderClient`; throws until `Initialize`. - HTTP timeout defaults to 10 seconds, configurable per `RudderConfiguration.TimeoutSeconds`. Errors surface as `RudderApiException` subclasses from `RudderSdk.Core`: `RudderAuthException` (401, session over), `RudderNotFoundException` (404), `RudderRateLimitException` (429) and `RudderNetworkException` (no response). + +## Samples + +Import **Feature Samples** from the Package Manager (select the Rudder SDK +package, then Samples). Each scene is one API: + +| Scene | SDK calls | +| --- | --- | +| Authentication | `Initialize`, `Auth.LoginWithDeviceAsync`, `Player.GetProfileAsync`, `Auth.Logout` | +| Remote Config | `RemoteConfig.LoadAsync`, `Get(key, fallback)` | +| Storage | `Storage.GetAsync`, `SaveAsync`, `DeleteAsync` | +| Store & Inventory | `Stores.ListAsync`, `PurchaseAsync`, `Inventory.GetAsync` | +| Leaderboards | `Leaderboards.FindBySlug`, `SubmitAsync`, `ListAsync` | +| Scenarios | `Scenario.TriggerAsync`, `Effects.OnNotification`, `Effects.OnStoreOffer`, … | + +Assign a `RudderConfiguration` on the Sample object if the field is empty, +then press Play. In this repo the scenes are already visible at +`Assets/Samples/Rudder SDK/`. diff --git a/Packages/rudder.sdk/Runtime/Plugins/Rudder.Core.dll b/Packages/rudder.sdk/Runtime/Plugins/Rudder.Core.dll index 7998b7f52b92cfac01dc22f640fa7fc5cf78c7e2..25ee8e06fc16c002a53a5f9047d58b4fe5b872a6 100644 GIT binary patch literal 96768 zcmce92Vhmj*6z$cH75kpdk6s%4rvg2f+E;KQ4m2;KoA8%;9vrxBnLsHiUq|kR_tOy zEGYI0VnMN8CH8jJt6qD#@qOQ#**QC)_xj#{{}(+o>sxEhnl)?c-e>Q_@T1RDKBbf& zpWlC1>VD+>8!YL+PZmMkzW9OmYE#nlCHLz?pD#J`#OV#4=G4cg)lZ()Y0Bi;vtx5R z9d|;f`gyZEO`qLq|HDUhniV_#gzA)(NLOom!~sejs(tF6&7(ecYkOC9?v$*1Dm6zd z6*k;{=YZ}6`fB7#r3&pBzsaEg^7k&};PcO?CM~5Z|4-x=l7+ty&(`X260mz`s}VBn z-=kW!1Np^jt&Vhgebhu$fe-eStDbwpsdIt%nhk#;leSvFT{W;LRo6GvPXR-G zgO4f*P>KNg3l6zQ}@u@*HcW$T*eiD^vzKL_2C)tJEB$FlA`jL zl8#OkT^lK%XmTg#>=8=o(rn)qahUX%*IB-AcZjxor5Ka`@{Ud;xeNqDLyDr%x3lLTF@~6OE$ENV@dXoM?CYMJ*><4wAb5*0ErCzGwv`m8tb` ze@=Z3l7>n!7C64*9Su^RyuT1x@d1)I83R$YsUlqoiq=7bR;U8B;V$Ip zr?kvSW^!gbzqFyMk3Wj8tSBw5@|R^+1foH$f{|7f21<5C?E%^~`^uVC$h5!6#G%UP zt4OOKf+%!Q(Y;V%bZ;cdPNy~Sq&nT{w3d0c(`f_q{EDtlFcfY`Mo*Dme*L84o5Gq^5@N)dYs^pkg{_;T>aAY|CxW$9$;k%`e63%Wkh8xW^p zX}cH(MncBMQU=F^r71CZ8g!h71CYhp9iTrT%`O4`6=`-0=wC^*Ye0WQn%x5$(>WMt z7lD40e0CG)*GaRhKp#e$-39t{((E$O50Peff&Q7abYVY-D_77Ldl+f!!h%^;QC^KT zC^Ha64^*@YupYDuu%uPUJRgg33PI~UAB-;HJRc&tFOIGS_Q&^=JP_Ysa?JAsgvg2? zD0!197sb-V=}ly&cR|m9T7o)IVOql@4(4yri4VjuaT+-$lC#@|(w*o*(03ZqNB+tr zlczKvfzH6{5{&Of?O-FbI8_W8YzaOJa}C&RBGI8pI&qVX4g(cC1WB1uOKFaTD}$lv zp#0Q}!#$X@Gv*LS8-elNO%w{%`D2m-a zf|=bqQ?Y9M%R6Ic3xTN%rwl^~FiKp85MXq68A5>3#bpQqMpu_11Q_UaV^j#YdhCXr zYN#CZ7_*RV^C@f?JocnHjorYF1d~I)=m@CI#r!O2=QNQU4soxlhy+8u-}!gtj`)w| z9L4bjV~ph}sK-a!SdNk07e7jJe|)Uuf%rJdF`kYVA}fBR~lHW8+xP%gQmVMDHNN{_D?ac~vp_=lJ}wlsyEcS_IgjIIjZ=iNhSqWm1Ek*2I6mbD~H`eK+?K~&7M4aXDzOo4_&7#>ft*ufqK3&-~N zFz8|IKr(!>Va)su$Dlp2eL)9eBUsBH)N!oG0$q!Z_b{Z6u^xuhaWomehVe8$hO|F6 z5m_KM($wK7Y#=;ObIeYFR(yhtJSRR;a$o#d$+P0vH?j6K8F3WL9Y-Wl^jH+*$YSj4 zaiuZG7kh=)5A?8NJIQgCuGv$GETfbdkX5!*nYVVT0Jl>Gu;nyOP=6onWXG@*a(fY0 z%YPR>k0E#PXxH02oO(DtpKSOi((I!<nwo;DK9TO%RBjf8-g*n*>-MuPg!;kaCD#BdU+ zXl4J{mW{YcQMbDY+%TcL#8Sxdj;>bL?oQTNCz-c8DZq77z)RLig8D3&_Be^NzoSk- zjN2o|PNV4WfPQ^3^mq*#nTP6iBv=)U;!IUh9*jXd(l8ZCaxgXxS-XntaO^~CV#&}c zNtK~cQfxYKI5q=m)%$V+2**wWcsm3gdj{yQI*&7F0EDEqVy;vKZb$~XFc5o{?MR4U zBTTH2y-)}%B(@4UYZLO!FZle{brc60YV~Cn;I)!mVB!EG%9OGY#mp&ZF7m{L9Y8jm zX5Jci0dCv{xN#?_@4H*vc{b>XSaguMiwXxyri`74Ibv5i!Sc-3ir6i5rk1sHq6vX` zu&^60%%-6nJke{ZjfqASy^a7PtgIdpkrBZUcdn&+=GqLIOHTYG$$jyelKbPcB+rUZ zm%PcSMYGLZBWj&%m!o1c*RB9%T{Xiv(Q~-sVV?|SyAV~v)~E__qbfk6iZHZMm)IZ_ z)Nn(QIDe%{jB#@Ip1&M({vsFWuN*kP$N6jba&rFq3+1pZV=f)gwmzS!k6hH3`$zTd zUQX)!3+1rNp}qs#*5^0%k&F8B{;0m)%SnBIp&XVs)Q6SX8_$5Lk6hH3|3~%hUQX)! z3+1rzpuU6K))zGOk&F5Y{;0m)%SnBIp&Ujs>Kod&zL2SpT-1l_zdgsx?&YMuzfcZ? z5A_XeTVL4JM=t8awdbDo?Osmm`wQhT_)y;=ZR^8D1FuKOMSWPB_N;IBa#G)4D2Ks^ z`VMVdU&Pc$F6zVe+Mf08UQX)!59L;3E#sfP?=cAGoram%dg3?%f}BNiFiH1ljgvYq z>l>NHaJCD^Hsa*Ca4*28+Tf9>UfY5v$^++vZ?)U7g%(_KbYQ+mKEAQSdn6m}ZfD`|n)Jb=ov5G_TC z`UdPw`8+E|D)$0!Id~bN^bk*lk*f6FLsdc1sSsr#AcMtTS}=AR7(oFEnMswwBy$)^+?WW)8d*MEk(M5gHjzwn(vqT2B359;!KnJUxqW)LCA%v^WJ)83 z=tW0$R$H1)3>}rBUj5Xie^DKCHI7wgZ#^7277bp1OoF*Mz7COl94AZei{onmV1K+p z@<1H-2Y|EUG0B^ZdbAU!3C^=bW{;J9dn{}hd2BaI87JN(xi21<+#g>kc_5DP!#1wR z7YUISUm$stDHkmELQEt;mgajXLO5L%a8riaqg|FU@n}_zFd0 zdWvbTVMjZg<`T8WG^`GAltQ%A9ACqBjDm>aJr! z9Iv3ZtCe{hnGYbrs<=OTI@o~86^wR4oXWxC2`1REmMpYKSUh9|8)lGIYFP)7BC_#?}8%9xz2`JKGQeZM!-GyaVQ`if} z1|uo0@|9H#%q=*3B(6S6v+I9HW3MgEF`asCX>R=}9~pT*S(>gb&96U(j3`EZB(@0) zKH7aK>hQ@D=Zh`Diez-_CqvQLslN_n^h^|KKolYk=hB^Wx`S92^b9I2&#HuDYWKO( zuoU|3%wiZ@20EppQ@Ss@92A$=#uXrZ(ubAdFxIq{z>u#(R`oj8C0|Q6nrhdjaO^CW z3sjV*2coMB`%$$*4<#{B-f7`$hm&((RSmF&)>lPz_`?Qw!7DhNvf#KwdW{| zLae;BHyHgAJCB9eAc<6DVTnB-RI)D=y#RToKc`)D0hk!Fyc%tvu3EA&N^CQ;%fsv+ zb6+}5b?$Ba=XYYqfR9lx`=Kv(A+2X8R~!%wUDhr<7ljJ^v5UZL?vY{#X4~AmhT`!IG0)xJdaNP`(8`uJcqy3gso>QJ(?tV2k z$bL1(V~*z{`ba=rij5{0*I1I{l8m{Gd|sak-~?y|+|J8qSP3?ppxg=ITG?w z$EinQR^a>89q`kau3|VdL@HB)mAFCbHWzozgRx4qH#xK#%`T|&`73lNxj9hgPe$0> ze&WiU-A@J55w9bhMsZr)!lmD%uueT*Uqi0YC0N7JR|L0$ji{;Hl+fw$5@Q_ zA~RacV5@Koy()zgHvq;kK4xFX#8Q|u+}E!_gW?Db3OeyKCHKW~o{>H049S~}I9i1^ zVjm}pUda$mbsDZ>act#@lj%{W8fgpaP+09S-l@X)T@C%JET7Y`3*KQ*6@D;w4cLf4 zM$cq55@q=3G}2Oi9!5fa2@+q_k4~x|4l?I#Orip;h{po>vwQk7D?%L{`Jz@P`;?G7Q(-BfU*&1f{b`)$*2<~|`NHT&UunqDFCcH%1~_r+I9 z?vI}(c~%@}O7u&UQR7$a><^;ojVR7X8_>bL^FE&SPjniYn-DH%2xB#DNJ0(UyZz25 zsAeJN9pFoz8{CIjA`#EhoPQf`Y)4yBAdn~^9e^%oCB}!>p%9ccrM*Aai>{ns+|uwS z!|lY^q8X*W_(jb8ahy~9rGfZm$Yjih6lO5K4CPo7gP{O=kp>%r(p{xhnPu)%51#KT zVm?ZJRTv^cU$i&slo@_A!W0Wg-}a#&z3Mx_o+G{i^hkh2uqK*daZTg=fm`giTi9<` z!A;Jet0nivahqD^&y|ul8F92PYC?YzMR8n-@CywZHQ(n9N1Y81V9a6!Zbs&h^`mF9 zh8~Xku(Ytg4Xkgy*mdI9OYV!`Ah|z&qvV13O_FEDuamsVs7L#o`iY`VKfiRA^A7u#vF>5~x}Sb+M#8>*KytKEa({e_ z7+&b29pXvA8nfYS~L9jFsL(7Au zISr4YcnqBuj6DttSMSm@IGg#@<60@K0%;DLq+a6g!%OAJOISp;P`nz9RJL=jr!2DT z0u;cb6!uxA*i#VVqTm>C-HU>}aR&t8;aYsq208r^m*6v?H}zT%|HoG!lTz1J4r4jDa}rr~&U%SU(?QUk(6uIQppp&k1v> z%f!_Lq6)POTuvQMPHF&FV=%9AnN_0(&}69o=2p&-0gHsQxs`KJ#mS-it*x+A#c`qf zS6gBCicz8ZcUoap#r{qrsL`LWY@0YXALc}#M(kD}hd5i-K-qvQc=T;O1~_T{NONbe zjb~6Q`YaM#3WtP9vuyrD3gIgLa5hpHl?ro-&sgw0pgAtrQHVd^OH0(6bt@gB ztlam~v_DqaMit7Sa^fGWAoh{fltE?sAFFJq3T5TC=I6=4fku0_7DUE86Lt`M#0#UGX2AAd~ptoTEcHyJf9$Xqjt%yn*JJ)U9~rVL!8{IBg@ti$YG zpJ7zA?p=epUU2XF-t1l5;2!p_?ach~7nlX&FCi;$b{fToY*)0=fbSs^8xj^&vmw2J z(8Tan1(yB6H2$$ImlVV{$8xP_o*ZyI+p;<4AWRWIcXxi*9!HevJ zSK%7hwAUo}#b1{^E51|mCL@jxF|jA&`8QI&%4wucdnThFawd~q(;wTHesiA)sngKs z7`|ACMV}|G)~nbOl;c&b6y;Uy5Mku`EhE2v6d7_A+knS+xE~H==8x4f3;fwm-yc$( zEB2!s`#PIv)- zehVf97?`exAp{thoQ5F;7?_HNAp{thc!nW_`y3AhGhBfXV4UPKgaBiv%Mij-^b}Vh z1T^Qn3?V$4r@8_mpoy+F?g#zDN2r!ylh7e%HU4{@~EOZ${c&b0$6$k;% zMJ_`KkLF@mAOtkea2Y~)G?%ynA)tAt%Mb#Lr7lAVPtj$rKnQ3qcNs!}vBG5t;VHV( z6$k;%RW3saFwSxrLV$t2(S%wEFwS-vLV$6O%Mb#Lb6tiIU|{1kHiQ6Ujmr=M4D6vs zLkKV~a2Y~?fwO_p5W>@@3tfQ_(7ecH2;tGZ*cAu?%}ZQ{5MW&DGKBCHz04H|0nN)@ zh7cajD_nsP&|K#-gaG48mmvfgSGf!!JhrZO1wwcP*SG>9Jc4Uofe^5Loy!mcjP))< z2r#a98A5<@gUb*Cj2m5s5T4p@as@(o1UI_^Av}UxT!9dSh7e$Ea2Y~? zal6Y9!ejgnS0Ds5?{pbLcr@>F1wwcP8(o1AFmbob5CV)%E<*?~?r|AHfN`(O5CV+* zT!s)}-0w1k0AsVu5CV(`T!s)}Jm@ln@OZq%6$s%GJmd<5fQg4)h7e#p;xdE)<58C( zgvZunu0RNn;Bi+V1WY{PGK2u*NtYo67*DwjA;5UrWeDLh{){US0-Ddd3?abS>N11? zW1Gtm0*vQeh7ca(+g*VW(0txy2;tFu!4(MM5xnRMgn)^cT!s)H%^j{l2xz|SGK2u* z6_+6d7&~2t5MaFOGK2u*HJ2d-7_YkwA;5UUWe5Srn=V5LkAr`61wugcEtesLNAqo0 zAOtjbxeOsZn$5022xz|JGK2u*U6&yQ81K0ZA;5UwWe5Sr2QEVhFg|n{LU^kG$Q1|y z&5vD%5FX7>T!9eK{M2O#;nDoe6$k;%zqZtIH6=WBfN)AOtl3iw zE<*?~9G4*k7(SOF1Q2gz#vlxB?-diT99mMyksY!ecAV6$k;%beACn7#S`@ z2rx2Th7cZGS*}0`XlA<%Av~J6+hZIS0-CulLkKYPT!s)}v{waEuIw28HmeZe0PF%-CoOfavCX(VPTKdH{iC>HKW{bCeuz=|D-VRdG8qeG1m@y3+F5*>+QIgVt?J|h?uuzLEvd#B#LDi5AviX zWrS_LScW527&~+`hNTz1BP15xNLrXriw74VuK=u!;6;tB17WdwGFTug+BF{)%SDQ+>nlqn<2$&_H5zDjEsZGkFoYyKOha1;xEq1Wp4kfjXvvY68YlN*w`^i=2Pt*WrU(p=ySQPZ@2_ z1@8=ex`Bo*-Osq|i(-d~l;=mWuO!~Os&04)ckUL|aXPlnLWB**_eJ@RG7oXr$uIAl zd7m0;6fP>jB-{%Mz3~JDS();d*=`R3nD6J?tM2{XX8?j~2-?=j*%&`GAn|oyyKzM$~q9W4@t6@d4`yt2k3fBl4pr>oNU?E8 zYzOjDOWDCAF@zAEf-T$h1-59j7-DO7JB1Z;F@%d)60jr7VmOSQat>;g?OPT@Dzs!F zRA#kcIV^_QsUz)nab98ZOT%lUz&R<+Dk*0$HeP(I)0?r4VVlN-f_N$r+jB&oN`%y1 zu#5|#I5uY2g^&|Hgf1WsaDf8!O%wB$ti#*5fTx&gsaOsRk$H@8X;sLe1yStakysIu z|5D5bIk{M2|2$^R6aG@buy|!ks4^GenMsT}u_)2U1C+A7$(T#9?Sh7J;EuU2pfAN| z3qD+bLT0E4{Sm_UEk2$x$DXxokr{JGLf&7-+#wi_7(&C5B?oJ-qAhu8(9HLg5a#<@ zjHyz?jbY5vU1>B*Tw`3jRjtG{+pu|ZEPX|`!C&eyAwPu+} z!4pL;s7$Uuh^@lGDzd<~ife+=H;S@m$Q!u9BrXj>c9Zxe{-ohKK9n{~LxLTS;s}O) zrx0G^kNtuXuWRQ$tU5x>MA^hzaa zF};#thoe>CV%&QB1%pKOY}+rR;ixZKZFG4iegg7Z>6h^kpT54!+3#=#bn4MBRr{kpBce(tmu|wQCAE@8_Oro zD`s1T!=_tmK-g}fYwVWE%*{KP=#~-e7HpZAEEKR?n73q&b-RV-&@H`TiO1PVEZ)ep z_AE5dkg?@(Mb_&+P^+A0T3G)&#-Vla6>+dH{YpwtiuNPHg&vmyk*Yy*AT?M~WH0y-pCo(gyUOG+` z{aw*K5?{N4ix*3nd4j?sFv6f3?oB&7#e- znVDDM(B@IL&E%rZ%sg#ok=@!XR-|~FHYeEsrp+^8&eP_gx)DT6Ez@QbC9O{9R8v}#V#Mbb{Z*2ahjO5WF6y9Y?gwZ z{a^L|V{@dT}zXNRl9hm4pT0;L}xk3LS&eDHafGc)kTSos;fc|6Fk~Puo zKbAuO9Rxe=LScDyFdSXLUSgm_@*I3dAUh+N7QmGjF1#Y>HMsugZ(=p4;#xa7Em#>s zoN@k&yxlxM!!1+>qp`tAVWds^(sBrRoo@Wyf*t>)+GY zw=pKdT+}3-5=Y!zNQ{XDyLC)pY36m)t@vO|mTP%g{y>x88PO*nM_%*OXrTFrby2;J z&!_mzH(2fmV6ez@(Pwz{X@~kFz{Ad}dVzNTc<$r=av?sRF~$y!yv%Q5{H&qdoJ}bWbHwX)L2N33k(gu{RN3N9&K5SE(Qb7 zFi?4%4CaG5n0eo9C2bVLRp8&Yo_$^?@Xwjl>QcP(KYAZN3<@PM!w0 zuQOmw%axx5Rf35tzr7I;vwtVptz#PV7EzWl$2D#lK3wOp{mLK5BKaHf83LMnJOvz! zpMd%vAJ151_j$&mU*Sy3=ZbOO!6@fgJQErA{v$(;Sn=&cSF@yXOCgD2yTR^-OBpjx zFqLHYI*eUv4mh-f)enwhOrURM^&5|3Job@`rH@%l*75F8VG%5Sm{SpVd+N<>(|A z2_~<^0$d8jwiv~VhRaHO=1;K0(NW-f&OPT}-Foh!FTNe z7Hs?2kUTgak_YGUhzz)RE(U4rEv%Q>vC%Lg(`5_^4sxvPJofT7Gfi>2tY&VWna6aQ z!1kYlhiSm&Tw&glb)p*p7J`{^Fq0BWlC?i6+Q|*UdJZBuiyK$(AZ@)am8zr)Y?eo( zthoY8u)|RdjYzqB23)_fH5MySp)ZR08!2~x@F6+@D1HtEcv9?aW>H-CMB-Ra@RToq z8|xUXTNq;)D-|>8)Ga}tM1s+o45I1)39`6{AYZ}4y#G$O)BV^Nv7J`zx`&lD zQpi{{Z^=5zjWx@{+7wvh1<>Vyc$x>PJM=!n{H?u!&KrJ=j2KSMk@z~ODKxmctfIfu zL=-)N?uDGRkTl3C{taV*RqX2OZLy7m(_CtcO=V-YG-n61dZ!syOZF_dXzqo#hV3l6 zAr-Y`QQX$kPv96sN$<_A_}c4Q@ZRoNvvEq4jI^zd^Pyjvj)$(0$ELv(*dT3t#e_48 zs}S6nI~RQHyEtV>nkR7%{@1K#M{z2Tl$$DY43PU=++%;oXE;7sY48hZi9fDu7ZU zZ!)aq#SqXJK^*581$_x=j#Q`VGNkF+yz>-_;(94knH2QJIesgS=%AfODhR(Sy@TFO zvpsB4Zq&PAm=XD+Ghs&h0dnb@S!B>A-?wtq6tr(YZ~NqXml%dc*hVpmtEh;BUX%Ht z_&ZD$CcaS|X7E#z(w(8`$pAUM(_L5(*wBEa+zBTDA>8HMlQ?3$9*E66J?`~avdG3dewV~4}84&hgbOlL{0f7nt} zy)?C9J{>}6a6gB89KP>D9ao}FShuc5X7)HN7xqP%HL}ND14(QWqLv-Qg@9RiP6NR! zv<1lR9!EP^J1___7Oq0dE40a6|I0O2-^G#Su+?;<4u$uC(*%y%|z!65R49XS2sGW3DLQknRyEc(ZS%A z=#Y!(Fl)&=#f=V&z{pYc_>_lx$7F#K3T7=?*e%UKVG)D^3qYg{ z6=4B0J8dL-0?M@x6R|8N5eO_$HV_GRIEuB4!=&{d(8`NjV?kGZQLJU|9KoFc{x&b# zyb*yxjhm36&Buotv52^DBgh=V?rNTzm&-&v?g$*4Dx(4`qzMHUEeQn{Ha8Tsi%{Ih ztm$r~48=5tVicZ`1V$*BwPY=HL%|{l1vc=+01z`$9__{sY}=h+hohJ(ZQ6bI<*n^z zy}l@BiQ8@r9EHnzwEKRD(C!D2q21%W?aqtvaQ`4A{P8ZV4Ol?T;RNf2X)tEHd%;9Q zJOOTDZr(=2S!y~8?izsFoJI7{WDFA(cZUb800-wDOjYv@8|J7?q3nwO^#{RkPCPJD%F>ftvT9LRS~7l}cm}-5 zNXo`T1WD08D3KOv(I7DwBF9z@z;A@OX82RaI675igww*6#l|gM*KCl<87_n7tt=jv zx5VJbZ9sK1`Qst!xZ+Pq54L14L8azc6Ebg+87myg@|C~DOX7qt`C-`hXo4M%-VQGA z<>HJRw{t!gy^GA@O z&rb+7VsW$k!Dz&;0aKf6;-iqqFypZVK1On;41zmhQ4Xeef#4E|G3X9<8pdFMoSAv4 z2!kC5xQgc4P9wP(?95uSmb=~0A~1pxiu*s}NURhmVfRiD7MEb`5is0qA*n)aVHPl; zbiC((7M1h=(;%?EJ%-Xg{M|3zfc-{ky{^~X#xEH@_1SZCbxzqp4f_A(3e{7<*# z{68Bpw&#BeIn9{2WUX@J%(9qfSZd__zXQ-|Wa@4_6A)?l{7-(2j2MPvB>oCu;`~n( z#ptrTXR|5-)i{4dTnN*v_;FU7FTc+UT1wPfS4-+KPHVKe7{5ob}{*3)C*7|;Lo z@(<4cQn8UbO}p7XyrZ?1G@q-Vz&XnX$WYjr&Tb6CfjfZk4;V-)lYq&ZTZrk9ZJcB<$3pX1k@ z|Ecir&i|Afb)Nr;?D?PlfV!k>W^v5XX5##hYobK^+MNFx#nw}?%m>AuKdiM)p7X!$!nWss z=`7Qp*>HhZI^LFgn!5Y>|8S{eC)$E_>osIp?{SX{M=^W0St)z`>yX6e$nuWu-mE*= zre%4@%IYrfw1egS4QA%OMJ(?pb9u-4pIj{O%v!R}ahDwy!Sar+ruBrIi|H%pf9x-b z1CH1hqgdCm-Pr9g!45~UbMXA)-q-Qm<9Zdf_@am%<@=5rwy*myTC~HScS@h?p4Gj3 z_O8J(fN%L?8&ivLhq4O}TwfqRavYGZ^%__<)qXpZ78g@IkQgcV1WWv0td`I!&v>o%Ir?rNwtt zc2M_Kk$-jZlFmVuRB~9ipz70!=_pBe6qEjkq)!Ov5TXCskroau*{d|Dde<;bDQ0?C zl<70QnPw(4U0BMrwD02XK@~3`eOxiiey2XK4ysQ}r+1F1kqeUCm0s>`~u>E&_s=KYtZsoLdu-@s5sQv1wGZJ)A6-l~6Vl_x;$6?uS4y6~UoxW$(8R*ql zBz;Rw>V{qrI;gJfNX@vk@fv6%4q~T5?0hAyo~M?h-bZSfF05v{E{ExcJeGY4@4RYH-O%(YI_}9mJpSJF(Ol>5Kb2lU|Enh^SloGpf@yIVI9BmsQfv z+vSYXl|7jDO(JLiI_m$uoDo=4ae9xSx<)+tP3+7sApfGgHAn;bv^GPc-Kl#T#>Vaa z$;l~aI!L6iN!eu*vA=a8|0GHG%4Xkn>iHY&FDqfXpmY1WpxU=T`6DFVT2A`-+?RU? zRb}@B5tIJn`3Ze#=N^oKh#K2tU3ZS=Wtbxz@fh2f;_r6cSzX(TY7u`T&b3taHhQ2I zv|rF1K|aiEjqkU9?6$h#03KJAhUN!Axp-PESx_WuP&+}-Sy_spMk{MC$SGf4FcjrF z2>PHuQ5bjKX!G*|qGXi?q|}WaR~KA}pLEL?*F4NR2?Shq~1goYLuWFK~-w9pfo|%YKEYL1oc#>3R*6xMx7z(=U%kZ zORX04p`bokRaoEAqB#J+JVDeXs7|dH)I)sP3*RR$g6zvGS{bgkON+Ya6CDY}a)*lM z7<`LJ^sBV!DD|eGZ}X@*PVEx(rO3vs4+PyPXoC7kEk?O(E2%kAeI~L&Vsj$qA8kG) zn#ZcI1nm-ZocdYN?SiIYWuRsUDTj?s)9W|8(&h6cn^Ln`nkg)2#i5 zDxz6=t#NpjjfjLtQTDj-tMuhvHxPxmwWp z&O{sWBrbiqy7rWsp?Lc8enGVzi5|kQW>8ip=rQ%Apo{^OJ+7V+^kq+?XVlArmWq|_ zYL}o-iYR+Qy)S5P;rnR&i|QjmUv(wgq5dwYLn+bU)V~DH7(nzkzNcXA2UiqU3|08) zFF^<76Mdwz1^v>S=o4H&Q1hC?Un+-UlywqRHIV3YRVrxN0HUu{wV*>g{$4j!eWQ8{ zik1@nQ}q*6TsybtQ1zo4B50o?qTke^f;tZ%QhKzJ6%qyXBtZ|A6D8^Cf=(1G$$GY+ ztzsobHwb!Otfc8CL4Ox3S$e6U`vx4;WvI&0s|3ALNK~jV6ts`XI_k>=O%PcZeZ8Q6 zh^$oKBB)&v%a!R3&NYb3$}UrS;FrVnMnO+^H|TB$!B)MxPU&$WWSc~GM>%Cx7QNix zpnF92Sb;(JIzG%Q9NRFbs`LYb)_39EraHYv5bs?KRRi@Sf<|N2pM{z7n4piP+z|bQ zApGDYWQXXd1U)Y3Nd1hU8w8ElTLqPgl_~l;K?jIzx_(~JPl96lML|PQ`z&>e-XW+? z$}P~Z2*Ujq$j;EO3IYIHu3r~)BCHI>EP7KA4#kjNsXrF9T57mXe=6vbrx(sq`wtp>U%dm8iSgFE7Et; zRc47gH$8w5+|SgQMML4O;g_g>`3FH~Yfsi?Xr@{XN;#wr zr$psQ9I7St(zhcrC2DHgVOGCw*(u3KWY(y5T^ToeQlc(FOiI+6(xaf?Ty+BKI;P|h zq}hF#Zt8Ue((lVoLAtQ~bfgX0E0Ff;vj%C}fXk3>>3JQ}iCu3)dU@m?q!W@JM(ch= zUyNDwQvuU?*-ZE8!!%<6(}#O9og{rXA%~H?p!YLKjaMdJk+&V32|4VAjU5=wrgW#ZchY}@oxM|OKPNMorBzdAD$+%;snw6^meCJ7BP2d~V&7Q!T)efOh|YB3aHOG)UYnWp;iHVfNR2;x>KJR! zw`WXprFBN%MBpaj>{%+3!j^6n`!yNt<*6MKZQrxi7l<9xUemh6C3;%YOT_2x?P$To zvovKGXcNzx%p*a2qnDL25u6R_)G-k={`{WFF>{E-;eAQB3CFa>@Mor~|yej^{o5 zah2#FDYN7LG{*m=RHm;;OAUX|Qa?+t{aJsRQbxy=H6#4|j5}c2NWYN2Hno_Myk{+D zB$zSKLuQj1Mc#OtNWL!d+$%NhGYGKa9u(_@J4d}m$9WY`43Cm}x=XR^t=$qP2z1|upYVN1w)z7H$dEGfVVRNV0 z{1UZn^>qV1TfB|s(dWDJnHrzRt7cD)pR|_xbNz2T`oD<2KijpoK!>yGPYTlKbvF2J z3=2SStL}uff1BF()am*cbc!;AsyXKwq;0K5_o%T5b85V*`2)Ud;o>?*V_p9`=(sW2 z+oOLV=xx;z)a6P?d!!RZIt%H9J{&EsPQ6E`5pLV#kv`rk z4fywL?~AZjlktW}|Bx`P9f_1J3aalVN}EfD!=Lf$x(C8m=oav*`hD-Txw}6nwXQSdU;T$+DFjGN&ALY>0EVit1O83 zvFEm;IXSuNS)-|o{quA3)qu{dVU3znu_C9F3U?8ddqL%SIbBteppSJ;*(%*tm0Gl| zl&H$0;XOh?dt0;-ck0U2a6u-XWoooR;p4ij%_&n8EUK-z6zBwt#&*66XqH8<$va9a`T53^I)qOx`TU0#oA)pH_y0GUHKn+;1`wKuDEXt_a z33QL3CE@dXyb1J>Mfdh^2HIxP^93IQy>8JP-Tw~sp`c~qYwK3&GWCr`=XLoSvM_Jf z%gFixXb!%W22!g!{+81XcfpC4sA9j)jjFR+QCe;#?oqq4;@lo;eJiTW?XB(ZiwzJzbJYV1&=R0&xkJ=>R`ccX ztlWLovle{hR#-G6@Ko+- zwL=iwurv24^@)*%-^DB*r@pi37eN}|d9cUE3uezL>Z>sviBDQF%^5MG_ z_840l`wsouR2&nU*@Yq zizdLA`Kpgald1>i%~$tZL|^8srwkHb=Bu3+Z3`TjcbfXpB7e<@ye9RnAo>#03sn;C z9C8M5b}Uk97SWeQDo+qK$K)+igFKpNsslZmXR5<2qSt4tF&3@vc5L36YN8<1_A}Ki zD`S02)dGuH-%_>QB3fCh)>`ydXQJCIVm~ZZuUW*lFI68{#BsJ%eJ;q1v!&`gBa^r+ zQ@)R8Sbi_W&I&Iu^2(s?wt0@`GxX+DnibXRFk?7IB=dRvQGdeaGjW zt)4LDB%bH0mn>rY&Q-fCV*Acjf4At<;HSO8)YAW|yh~NMx2Zic;HtdK)L@I&_q-+Va&@Xj_W)g??zCt~*L(8TsZT9B3g}AJ z2~W#$oHa!r&AUpCwP-ES)#^Nph9o_gca3`1qA@_%%Fo1LFGh5yPNIh{n-DtyzA8^{SA8B|9;*L>N|_J1^$tDvl=^qvc>B6@UM9r@N&69zu@<(Z&wFf zl-5P(->$B}QwP+{?LH>&4t2?12G#Zn>ATgZ7IjVv=ij3a-P_1U_34m*uX<9@8uezc zqWt^S+ZGMzR+hh6bs0p>HF5%cKn)PY6GUbH1L`n?!pn2|=5JABEaL3gqNZ5H(X&O( zv52E*i;7#s@v=o-VvvlNE$Taqp2n*9uxhstYhR`w@0h9{Rpl0OWq3>twumdklj>MO zCe}}@Iabyj7?}Tzde9?#R=r_my33*YTUEQktYL|29~hOtO-;Ax6#wM>=he*yp|vOF zzofPaS{B}lIsLMF*`lYb8uDLOe-pGg{BqSopbv~peG*ue|BCv_qHhBi-$TdsCfhWzB)R^6@>DMSBMx&3{LA-IrFDsh$28^WRrX1zn`h zDsP|kkviWZr?h?2$LcCUroKJ9~8she!McK!mO?=1SCG1H9hg%P*3A~}^q*dkth0x*+%ok5##xFUA;?(ipeI?`w}EpD zQuVDCH3zOPNYgJ_wA{a;AVZHHMk^Po8*|$y<>(VF`nhgPL9YHx(6TVE`115O79Etu_1)ID2JpffD0t=j=qXwiPSZY@ltsgE@-NoMT67dn{>A!4 zi=MzN?xg2f^g<|Ab<(F9BsG-i#a5PG{f~kYz0#tR>Yocb>+>yozss`)UG!xZebHqH z&~*lhm9F|$D;r-EDD0~5wrF}yYGJ9~V$r-_g@tAMNsE^B>IU?@MSl3%O}}bUGW_hO zn=PuWezqX0KelM^>K#B|S;P}dcm1P9Q}a_*cdZUJeY7IGcVW2>TXcSQEl`?8unaDhSv7cFI%(%t?i@Vvgp;GgA4oW z4=j4G=TM+843Zl9>F=zJqoAMu%_5G1emXGR^w_1oQHA|=ibdD@#sOtp^i*_}9-uo~ z^lCI!4ba^z8rgT1uGKv(n%p;4)#?Ejab(r$!4`33)#;%Yar6w-BQ4_S8K{r8sCUmv zg?s7aEE?4Fq{6-RNp3mBdXR3g=p@8?kZ!VwyXZdpOpB5r+ee>m5%;6P`a+9dMn4SJ zS6Xy)%_==a-(b%RIvi#S^M)sGq^(cMo!Yh}ef<`?d#U$Ursk24DQ z*Kb-h6McJte$S#t^z8xq?*@sL1NA?x>{8zOk!~%=8iCd5})Fi19o~ zXIj*!V3j^t7h1G;0iMs$r3Oh2Lv@vvaU>7beJ$ch9;ydf#5N4m2U^579HI|5NUR*H zkFv5|IHez|kG1I2JUo-6Pqc{Vm*IMjMbqHtaDAFZJog-?7hCjq$PUx1EMos1uFtoK z{dc&&+#soaguc$op6E5IaD=|qq8ECN1KQ-#9I3ZhSw_t{g(LNo78TcA3iP~3^9cQ_ zm2n0fp?6ut8E}OD*dn%Ql>UcBY|$wFqeX1dk@|Ow*rFqK_;Ax>Y|&_)Y7tvBTIX5B z9vh=eEMkw1(d8ET3knO5(!DIoD(D6@&>)G#SiPT>P4B--kJX1*)X+awjnzkbG{@=j zR>nw-)5lxHNQ~37Jeo)Ac~-_1^JpEnh%4sNdYMJ1Ve}lM&#`C)M$a+&Vv881@%k!@ z7^U(0CW{!Q3HnZp7^Mk%vqg;3ME#gWjM7BC%^+#*vHE2zdn{>i;j#K}7QL7>6zBts zI3_0P&n@DZn54fmNUTiOzggMF>gx+9YySw-M-NxuRd}3EF-SD0=qxMSUNX3FiY~I~ z^^&1LWfpNYJziH^#MSh8-QOavm?!9cEaHlJfj~;tDoHueFFP*bKeSMJ22BN&0$= zPA*ARC+XWQ8j0~aQ{QXRu^68-^&u)UL{uk3fTg3e@rk#l@bouVgN#GUID zJZ-ODF3>A2;ttfP*I2|Is8L^LkhGym zUu$KY0ZsZAi#P+C^xYN(FmL1fL5tEbZ{zw&i@w6XvQTfg=ojoO3-zlOU5wVAu6J2< zBU*d9{@9{jz27QWr2k>jr@cP}`q3cqY_a~`%D8GQ)}bRzk8#z&Q=S&F_A_*@MXdb{ zU1E?}S)#jJ+2fVIq9wYS;QUTY`xVY?g(e=mo4HfK1ct} zBF^G-^amErD|@TpT>ZI4i_1O)`p%-e;mdjYH;bNuFX!pNDCr}K#2TGqP?(Weqq8hx zB-ZGT7SXfwb(uxx_hx+U=i2di}ezV*ejRlvn*n-T%y-nG#V$C zOZ62N9fuRkrTTh{F2EuTC{(~FraT-R5P;ZO8v7%BWlJMU8S8P6Mb~Gju;f4n)6n{ z)jHjx`kW7e3M}Ha%r&};MZA`|MpqgnzFe#OSlQE6Cl+0+_p<2as)nNL^Z^#V-{q}> z^?JBPUv&8pXpBK(<$68Q%6N5sy`E|judc7x{INOQF9^pF&l~i7ixwiDH|Rwc-H94* z)GI97j2dp#Yb?64;Pj%K^raTvRj>l+T7#taoAoUo&71W`kLJz#L5q0SzePV`5zqR! z=Y=bL4RpwT-`S4?=9l$wn4X8^ij?#eY+0g z7xUQmAMh>n?Ye_Sq1;i0cjz38I_8c8>SPi9yi@a+_E?U7-l=OW;(WhL*ILB+ewW_Y zAZf!!JZ~k=>)0 zTEr9CJ^CDrmZP=z>WeHoAFaJtUuBSZcAvh{$~ty^w%|T}hegq@JAm%D=`^_!%6JNTRFAcYr=UmmWQ%wTdQ4BZh^L^(biGA91wF18Sj1D%6MBh7 zJOw?e&$5W8peOZOgQWJS^c7adxICrTTg13Lr8ii_HaxBGv50MWT0d-%Sb0W2ZDow$ zGx`OK7{O=s>lQIe&+2z9Vw9fMpBf}qw(75~Y-rD$i?-^YEIPX9rlM_HA7%Q8em2!zD3s~g4=Z$i#S4_*A*6VggmeN7$mj7p!c#e?o}`7{Vn2N^@1L5 z5!?5o9&HiZ_oAL?5l6^Nda6YnAunnEOrtwy%m-PxOV^6=V&$Ea< z{j$E)BKGDh`WlPao3H3w3=%6l^+qeZ6<4i0^#c|?fUDM>`U#78#qg?r&LUnhysCFv z#NF^U{kBEi4PVnAS;XD&b^WD9+znsXKUl;a?+x8z5qG>dbZD%!T|9eJcQ7cN2F*8h zjzuNVd{cL_=q5Z3^EVx}Xme?*`kSsXNUXf2Ypsk==DekcSadjI{gxhP5!?5+KEfil z?`?gIMQq!yBdx>A^vEvPTC%*(}s-TaP=*!o7mLT)I&DVOKAoESy z*Lraq*=j*c@V4HwMPKVL1eqGX(Q_syYWPMkvWT_+Q-3%qA^WHP&LYab)pL$Z$iCH! zETZf?9X>uG`%dRtMA`S+KQ$ryUT0cF*$?_BkL(8>o@O*D`%!;k5hL-V{>>x%Nq;vz zVe=>LpOGl{vp#WVLiV#h)gs#bMSnLtA^S!9V~KLV>aQ(geZS(tyoBsG{hLLU{iY)) zQ^tPKc;Bn1AQRmdy}iMdqh^bK$)Zr@j-uc7opX)sxZvgZQSIV+f|9Q8*IeW{-3-#p z`g~aAbNX8JYR$cPk9|Ljb|pQe0?ufQUh1=5B{|1iRNU)cymeo1QTND0D%m;Hq6_^y zR0ro%ix>&K#d}J^&koLq7EzY!d_6xQOLbZVEemg~{;Vj?37$&Xvhc&z-xsAj9SoA0 zli}nFGLgt|ezS;?$Z*=7W^A&COy@g`D9d#G3lg#{=hH@mD9dtwvWVrfo!^=gvTP?3 zCo=tz&O{W#!_!VN2_f;m_Crtw5>JzOgjmFYN~HH zj;Z&rYtlnw^XU>={wwJ)w70G1Z1F#aNc!;Pd!qKOKDLWepQ6++QWFilFCunw(RYrj z4<*{Nv6fLaUi}%}AMtV2wx0Cl`#S1xE=f)0bKK-K=TN$?f5PUfuH-D~VmL7fS zrLcf^aWG~aTWUB)Cf=tM{zMrw#+tWx=!rWTO`PdT+x~Jnv^K17VpN#cwZm$l6;lWA z9IzE;oEXl71-o`zdF|g3PShLl@7{K=riq)e?~S2LC+hlMV)!)XtWV8C1WbD!p?#Rk3C_3D zzb2Ard-{T$|JPO<4^6BL?QKOntP*aI|Ja%B9f%0{Bv$U&Gvj|;wd<$bU;lYt7_Yth zdvd(e|Nrz~_ve2@^MAX&f8FQ*TTk}1yeG%l^zyy*|D?wMiJgC6@BhT|pOyVH{{NmQ z|K9Tdt@N+^|JODDZ+-Z0>zgyu@I2!&SGKn2&$g%8wx`yw@~M9@&U;!J`hT9@C;L0? z@Xy^<4Kk@#W(PN^zZ=(To(gbXfSuj1Zj<%-ujnMabuF8fJi%|ZHzumY% zWg$OVHR98R&q91o$7c~fi}5)FZzY|9f9vB6=$`@oGoXJ4_)G9fQoOQHLmI@dm7ZN0 zQQeW^Sv5&(B^@Fuer*H%k&=#+bh4z=kp@xE*_BypGtvU}p5pc1dx}@JC7RMpk$( za znr~>Hsn+)&i}dj>Gxep;-M!=b8s}boqqEZ4(CZw1jrvFBT79$lzg8@-h2=N&TCu!V zEUy*IYsKbTv3Zx1UGcrX0=|Wut*W9R#o6qf*eBgtuj;yVbnbGd^y%;1Cf?pAwcjSS zzo(zfpXO|JPR(m@-qSgqr#YKMXOrk`5}hr=*&>`R!fB_@&D-RxL6lbMCsE@jXS<|3 zMRTWU?i9_F^_q@(zLPcEd$NAKOQ~<9-qrPB-wwq5aNnCwy#ILL+bD})dC;dZ?oQzCt~>&wAMTxa+Zj-=vn`)ajL{2f9irq^R)=#ru%o zNxIVM(Pg}ErSs1o&%sXrfiEH5(CcSOe-Bj3DE$H!4iEOR^^V2!Yn7V*g9B@o@apuy zULyTMA;N+0Bn1c3@6=({D}(z(=lbBG(0MFatAFV7tsgX`@976>UkM(m8SRydZ?_E9 zeG1+W4%JWf{xmpJY9FaLl<3g$h;yYrUgCT_;ye@2-!N7)A>Au9Q?tGF{D9D0@HaV4 zB0W<|ohhZxlv3Kaz4qMDYLTuL=~|Jl73meixk5Ns2xq-$-X{FpgnygxHwkBxa5h;@ zda_0MPwH{yhlHQhd>i^n&3Bxi)O-*6NzFO(q~;viB4xKo+3n)XKC}C=REKI{+{>D z`yO{A@Tl7leAL}3CzIxd2?s@q!_>^sw1F zV@bg!#_bYf?l&$O($a5sNgr~_kdp)^!R=nyky8sU8Pe(}8RO?3EQrERz7cfIs6Gr` z`h<_0M;1I-aGR^d^|69pn0~5YlCgNnXJINiaM*QC&KC>*U^Fj0RghyA)txOUHot`Z zOP35e1K|>KufZ5^Mhf46t`;3d=yF2B8TBLDkrNUws+mhB6sBBLbFVI(Y>Gcs^k0bFMRH-d3WJ>vvAP~bG#}2^D^VAYlDT$=}$BLSxbK& zRT77_^yg6}t{zq5>QVOUZKjlCn<;%)n<;tKW=bx#nUVu-rlfQm{S4F34*J<<%GnET zrlhfhKDU{2Cc{xBsYuY~9hOZwpWzPL`M7J`SuYKWNc3|wkH|ehw17lT|G)yC+O-?x;jDoCusi!?Vq6ileB-D_RrG(Y1)5|{meX+ z;Pm34QB*gpIBG}G-km#Du%{SE4Wpq}GmopDKs#V$$pc$ehfWa?Ga!wj*Crn8VI*mKIz!wCI^8D{7U z>1sJ$HPh80x@xAYL$rU0_7Bm1qxr$9uaz8kdGdc#vX*Y!=yn_3HnPTUqua1cdc_VG z$8?wUb(`4FH=5rJFp@{TuLu&}BaGI8(O_4BnXRID#tR7;D9%8H>rcKPX5c)$* z*F#L#QM!7Ru1?U^!*q2*yQ2MvY5!r`Kj~r*=92#Hq>FtTeRz&OoTU#ZU2^8bNtg75 z=Uh_yXX)EXm-L4xUD6+(V?LZ?KAdw&eYik>F43Pi=+6np%sSl6ResPY}ilc^%bmMdGn)TuG$vL@8K3`syv(q=2_mpeEH(350N6f06=(R@` zOi@OU(*H;4|D*JOR*uxGh4go2jp*-bTHR`dN;K+Yr`K1%#hr+U%gs1Z|#U z3=_0}M~>9Ya}4(!!#&4vJ(!KYS>eGcoo`mS)l%Ye^+zbzQ(s5Cg%}~;2`p88%Jh_~ zqpHZ`QD30`6!8r4hr~&S_&kL;n>Y_xihUDT!R=CG0dTpo0@z?|fj^BjZ8mO&vc`x) z@l#oAycc{EeYlwzCGH{i67MD+Cf-l{An}vLe&XZ6QvAZ^)9~#Ha{T%Q{3-I2#t#tg zfbk!|)5e5xrTE1}1MoTHCg2N3Vw?xRBM=|X8lM3FA-D%`SKq8C#9aF%l-J0AOFqRE zQy+0Uv60wMe4O}O;vb16F6I?+6VR=;xSoNZH&Y1`_fWr$dJmOe;+@n})bFNpKkeH#$Qkh3wMrr|7sUM)}?fY+&Qz$W!;;5*fyfUP)dq+Y!n*n}@B{js76 zXHEaHqJ{qZfla;}a9ZuI86ld6Xc|(x)nhZlG>y>aduS&P?)Rl=^Pq*|+d(Tuxk z^IqC~kT&~)O}>8GJYu2PKLY!In(;Z>{0eOj&~%V^N=-v3r|8==#P87N4`_1;rcJ&f z+8iccfZpVLg*Jaqo4=t=Wk`B+4avzuL((`7-0z!CWhRvcR9dLqK&6#Rz>qkEfK5R0 zAwo=2IcTBihoCq4?xtxUT^+GdOpj3?pz;j)5My%}TAZ>~Tyif>eZ)%IsiZ!g*g(CJ*hJGN>Mg_o^&l}s(-8FtF-bi|JV?`n z)bA$tQST=nq3IFo15{2?IYng{==Tj%KTrKUbyqG^lq)H!B(EfIB5xvZ2BHm82~jx+ z?)M!eKSF+l{1ok+A|K9W{mB!Xt~~lrUP)dB^!pm};_9%kF^{c=rcE>rQVCHBQAxt) zKlxJB4^lr!y`RbvDo3acP#GkiqJE0{5S3x#dFtn>t9-^YpYhCR$pZa8FLfWWlBSh3 zZJ^RbrHM*_N{|?W-sB5WPf|HZNksdvvf zPy6R-s@$xNZZS37V(M};=5C3(m!=KGChASp{XoAjKs`h~L_Gx`cFjmpKS=!`^?vI8 z)CZ^!kPlKHBtJ#{6z!j;|3g%UXgW-r!{q0wpQlY#5Ld2gMuCKD6foxt82?j!kw**PQtPV^1GiQmp?-w=aVVkcQ&dh-IRmA$`Xca-YF80`E)t&$ir!N9 zRaa7}q*4pzqt%T?Yzx$zsQaNGs}52RQ4dj%7D*mPVfuJ=vM8>4ryT}=y7~z19HE`# zv~wK#v(=}lpQ3&S%8S+Kshp>B5y~sou42Zpm@zDVOTAWINu`oXEtEf3H&JP#;)haD z6QUBL5`{9p<{*`WR1QPAue!fj(sdmCSoI+JDcEW9ouTP4m5bmXtu{(nD@w$)pyVx8 zS>vNp3B~WLg|eikkxCO4Ka}R0Ae9g?3T10`iu@q#th@3sl-8OfRE~p(YEF@#0e`W2 zi2OY9B9!hLRmwOJ3rgQox7SpX*Mi?y(?sqEf2BG^9tHn%^+EE(rBW`3p*&o3gvxR7 zM`{L2U%Z28D5iM3G1*EEth z5&d-KhyHR+kV=Rch4N}miu@q)FqH0^e)1z_l1s;-yiz?#LNRL_$(x9ND5bSQ@(?i!<*M2g`C;%UYx>ELgHNv=BtHW_ zuXdRHB6xR=Q6ZrhRETdr@>=k#Y8%P@;7`^B$)n(J)}+V}gRiLVBkw03hqAGDfP9d6 z1`5KekQ5D5xd`RHYUPm-4Pt@kE%okNFS(Cc7JVJN$6`^o#MA0J_>42qZ?7Gueu4T$DDSH^#z|~*#<5(+ zNe%aoleXidUPZkY`t3E1RGO*yq1;y!pc14KBoC8EVfspSl1hqri27mZ57zcmIZEX? zlt*d?shp;A2Fm!FVJa7>T!iv)tudbQ9M5=;e@lI~)<>m^N-dPH);5fft2d@KQg5d2 zhkmLyNF@r6mQH?{riY>YsJ5TVQ7XrwglYzJJ;IWs^WBu1gUS(7AB5f4KdU(-*19Q=JV2g%QXKQwdrN=eZ$m5Wf&SL0l^*_Fmb ziAljk@y!b*Qt6v0Hht7>EXOPMn@-xu$>W0aOsb7ThbhU96L%&Mq8$R+{@NspGgnqKJr@d8|oU# zo3EB9BR`aH*956V!N=F6$Pa^KK5(`8-%sT@l+xM(@W6;3E&zSb^dR*pl+L;o`C;&H)b+u%V|qW8<4`_QH%NX4 zys~DP{33X1t#OTno^y@lWWhC(4?Zfj;0XPixcbcWMk;<`e4W>HSoWLwT@vko*id+86mnaI`NkQ|*=371LT9mGf@6%<`DU?S8{cj{37(TGnG$5H;6etrrjsu zda3w`wNQRp*FfG#Z1x?2D?jxBl^`)nw@LC8@i6W5k@x!~^nUUI;vjK|I7~FANqoFS zAF+0tj1INaWLD5XrI8pQ28l^xir7c&Cmx+9DH5>v!JVn1v!OH0>krCmy9TKt4zuA`TPPbmk|qftV!r5eJBa)8)x%h{SPc#|(c9;BWk9;%j{>7#y>e1LqArbFbz zfP9dAh}=6{d}|;Eh{4(7bCNtoK0rQ1RP|y%r(SZ{OYW-|`&CpLs5DXukO#@b zG)+=DMBYc<4}RB-A@U2<)f}d7j`*1~M?&!dkw)?cnl_UM$Wyf0N8V39Kt4!5M6TwF zpI)MGuK3nK9v}vx+&d!-L~W#!nk%6nqS8m@DER>SAoyJ~PE#48qUJHq^TZ$DJaOAV zB|uC;N12ij5Qk|;&8MsRV&6;NKy0KEAWsrgR1VFT81_-=r*f3a0F^;1L*!}!(@P8x zg9{{-BzbB9<3rw0K0tmN93v6%-WeCj)k4N_A!A71xKP|S1JS;ygvpcSDe!w|^pW?| z)d2Y*`4G8U#JCZCi^R7E@<#CZj7IVRF$@JILY`VAaX3V!k4it40rDZ@Fcr0!abC%^^> zynz@X_7Mk&LqxS)>^Bet#6IExafovz3?hx1C;PANf)80rJ!2L*y68)jJrgcd#tTtH>M3o5=&@Ve&)7 zJ}Lvm(^Q7YFOaJS@w2Ld@g#3150EE`eZ-^GPZNizs79ug*gy;r!;RuwlKc>PANc@r zhe8 zfH*`{t7(VWKnxI*#6IExafs+$Bla7J0b-KaM;ssy5tW}l6B~$0VjppUI7C!8&?d2g z7$Ei$2Z%#N^-kI(HV^~EB(aY;KvZjKlh{BE5R=3{;s9}o=v^oF8;Aj7k~lydBC7TD zlNcZ-iG9QY;t)}7piN=}F-hzr4iJZkY9nnD8;Aj7A8~*Q?H+B(aY;L{#ncpV&YQ5R=3{;s9}or~KkSE})9D!xcP4IiJc#@#G@IcPri zS1iTvVXnomg;uD^_|o(>>IT@@02`ZNqgBmTTh$!=HfXN81^X|;aIpiwBif}F;`cd= z@I?&S>9GVmJeFd2$1?1=xK`bbUl{$ZTCU!Q-^Se!Uq7JUfiFro;5*WdI9YoozGU2_ z`c<>~2ZZ(+wMuGjtFeX|>YmDjBTesk_x;6KdU1YA{nlj!qq z0e)-FcHnDEb^?EM^&Vi}%sB9gYbBJ=RrUfOs}io2Y@1)5d6#4JmyBBps52 zChuohugrbR@&9|SoNJW&agD??vPe*esKZ)0w-BaX^hehOo!-s!#iowK>^T!)=Psu7 zd#D8$l@JU!9rsXj6;qC(QLFec93YWl7ouB_OZzXtU z?#1iZL0K_d?C2cR@tMHV{}c1-^DL1|3#2~iv}pa@tgUc0drlCTnYTwWYQ@nR61Tc( z;#TL6j`Q6!MENdFb%~5Diw^kt1NH-%b!-RQ;3F&}odY}R!)wv#fO}x+Z6bqHBV#1K(t8 z(zPfvKXpznWPUzBUDB?5{1*5=0Zn{i zeKpX;i0x8t^?8ihI2TBL5oqErAAeuMA5LKeni$^;aD5!V%Pm)5 z2AUYri=cc(6+<}zG%>!HKzRaR8N?}wKog^V8I&h+U#`A}KNItf3Mfx05AMDWG%;To z2jv@>IpEwupo#gx1fUo52UGbldoVFuxEff=*-916O*I`RCcYC`39QDQiF2^3fwlM# zr`O=u$R_?~0~cd%fs;{yCT0-xz?T6{%pw+mUk5ZXlUM|PJ6!1uvdQ{4mqOw5XI1AYMeX-)M2cGH^J`-yLFsShCpQ#}a3P4!{;YGS^0 z7x1H)SDET#@Y}>*(Yt|v5C2T{33V88dl+b{W9mNOXW+Mq8P(qbAAzqp#|ZwJ>UsEQ zV)pb=;P>E%iCNSqfZtc21pY*Q8u(Mp$_zCGG%}i!5e@k_G$bJ_)4IunvIviTYx6MlJHaT)j(6NF^0kYKvUgd zoCkj=(8L!CUIAYRH1XAepMh@xnrfqQ5quNSRGW=ofVTos=Z)9Ew*XP+jbDM^1T=BN z{;$Dr2Ab+!#&5vi4a7My#&5xI1>$Tf<9FZzAliuWd+-p@#QvRGkc6m@DBsg?u_Z+9|fY_;hQrCc8gbo ze*)i;L5l<8gdU>~{8KIlBmVqmY+9PnetJn+u|(JJxH6azc#7l0o( z7J+{bXsXW}OTZrmn(7P2GVm_~P4y*wb;Q6qyVrp~Zd?!kWuS?3ao+)c0%&4?P9ykN zfu{PJ(FFb!&{W^RH#`iS6x#y+G`_@P;Pltk;Qxpny#{vg_`wH_cY=QlXksUid=>Z^ zpsBu%FIyPc)w2Qoj3Hme{tnPo-^F(&@O^h6`V)LB!oV4%Tfo0(+ywpt(8Rf*H-rBG zXk!1*yTN}5G;w0*E#N-_nmC2C9sI{Y6K81#!T$wl;?&C!_)mc7i||bX^hH2Z{VTpR zVBiGFo#3zF+X069H=wD0hVKCw>Q$hrevVW9@!fZziQTDP;I9Eq^-Jsm$H@vn^l3&M z{MSGeCoT4X{|0E{^u&GOzXh5&5wREicR-w6g>&u=oDA?c;Qwjd3H}z)#EFA1RoDHaT4E$!LJ0O|1>`eJ_(3X-uyWDWFY!a^Aq4xff%pN zPlEe^7_ZGwgI5C4XPW)sGl1wb%_HD7K=hgBG4Po{jMV05!Dj){XPU>s=KxJL*Ze&A zJRtf^^9$e$f#@^MFM%%sqR%uR2VVw6pJ{#t{5l}Ubn^-D>wy^4%_qU%0Ysl^J_WuK zXsRaj8{o}AQ?-~+gRcUbYPI=~;A?=U@|%O;HvrLRny0|m0nulg&wy_LqR%wH4ZaD8 zKGQq{-U`GF!Tc`xjX=y0%;&+k0!@7V{d?f=0-EaG=J&z30nulgKLEcKXsUquL+~IF zeWv*%@DLDvruk#=2oQay`7hu*f#@^Mm%-lyM4xH?6ubk7KGPfq?*gLFG|z+I1~gUN zd<8rKG*yrJGw{7Y^qJ;G@clr{M$BJ;-wwn)#C#3>P9WwX=C8o-0-7pm{u(?5G}Qs~ zH{b_>=rhgVg8wZLeWv+4@OyyhGtJ+F-v>0+`^~q&?+0S8V*U~Q?||q(%|C;G5QzTM zG)(lLK+IT77x+hjn6a3-;2#5Gu43kce*$Q#hs^@;PXbN#DYFRt(?Il}W(jyd&{Y3m zmVqAuqW?52z&`^-|7nf`e*|c%&zTdzKMzEoX)>%*My0C=Z51m30o7Q9Q{1KzFf1@Bhx z2fs~y0Q@%f0C-G&2t1}90*|YYfXCIxz!U24!4v9X@E-Lk@E+9%zE}MN_+EU!*@qe5 zXTbNVN5J>1&w=k(kAnBAFM{`~$G~q_C%|u4UnT!qZXNj7b7uoj=FS0rGj|?vAa?=q zTe*vXr*oG8|0#DF@Y&q!fZxfz9y7}4;LqQv{|Elup7y9St-=fji19L(q6fqD4`%(B}tbMC_2_d}Su z{s^-9snZGfWE8t4FPUXCiGe7sb-1WIPT6kaKgN2_c{6XP`!e12rd*KyDlZ$p1brtO^dc5eVqQN3n>@F@Z zzQ6dx#h)zx=i+}Y{&{hI$>Nd~CHqT~CHItkvgFy47fN0#nON#Attq{}bXDnw(q!om zO8=$weCeB|)n#+amXzIA)?4x ztngQCs@PHSwTgj?XDeQ=$Vp-`7f*uef1kj4b|z+E_P>9tST75~c)m;j`&Gp&HFCEb z&wg0l#Ir{kYIPZMwhXn{gWBsstsRfrdIf6f)u^3QP%EdPHcmk;^rH4nL(Qo|jhTU( zg0Hiql-`TKyYQF9UkXoj2koMzk(YpS^ zy8g<#?#|We*qyrwztRjC`!L50W6l=FY%Gk~Rv2@tIQEBs5+lzM^sdL%$IUOH@7bol zj=xj*dk%j;F^?KQGoMxkuA@eo>y)|%*E-i3btCwFt|RI={=SR9*YH=Cb41OZH{mi`1n2~?0 zT9+SC@5*0i+zEaVe~0ro8U4^6%fHF^PX0FaLjEnrFY+TsxqG)U-Th0ryUV!Qy-)qj zJf%*%Z&lxMZ!%tR2h>m9{l@Qc|7Z6$m0R#rb zWlLP8<+rLp`D)jD%UfOFD_>x|UWV2F@?LXX#WUv36+bl(Rotq+TydxCg$k)tYWdn| zJkc2M?F_95x3|xmtCp__wYN7%-lmRVJicgYJJijQ9kED!S6kQaNM~Hz-`<{K zurbmZj&|;Jd|EgUKJ~yywS09XBY=5j zboT6t#OyozI6`Xb>f90C*%OO|#f!#$!Dzy=ye`<;6YSU=iANHnf@tcBiQkS2-WWcy*dk;Ja9@XDTOM>rD8h)JT0cbPHK(wbm=S8F68RyRk2;SHS~y^@`BpNQ;{ z99YuczAqN+hR~TWtgByAp}+&5_%BBJqS}p)D5Oxib=T zvJ`GIvl6!>$`1FE;kdM-EF4!01D0}AG_h-QBoXU%j210{na+})Zsh%Z_0C8y?>7dc zG2A+-YK}6qg0Wsvkv0j;N*gG0ysKkx#PN9kf|1F?Eu-4e;aGuWd$2$QJ0pqqEp4loil4!B4&r`v0_QEeY-|_6sJUy5=7N+gf*FcX zS70X*5Z85udpaU3uspX{nnpVYdc>tUm2UOn+YnPolh)oiN~pf6tD_^r zVvE0)AS4bR+Z%4F4 z=c{UtggT;~_Dz?~T6?zdi6&Y@Xq3zaWtXqMvl8IvopeQAacSW7n%cfaoZ?d>3RATP zt*WRpi%sH|k?}BNhnCrIiIvU4&fRuD20Ky>T9IE?&t|*Q@yx8#I)g0Tu8|ql&Pifs z6;^8_dm|lNy0x>$aCoalGn>8|t2S9Voo*B&)qSWiWx4K<) zcFQ;SgwZLshIgZ0#Uk~nu#t{4AZ+1mJa13`^?4=S=9 z*2vp2gGB!~vbWRC38g8uaZ4hk!Wn!gT7o~!27!y*HTA?|k$i*{MYnYU>Z#%0HjNX)Xe!Qx%kRm?YHvfx;7 zZfK$l&vI*(9qzp>)OW_V$&*kPHZ`qLB`$=@B>EOpm77dULw3$u{(dvo*U1$u`k_ zNVcBtI?yKkyPl5~YpY+#c4YSlW0=^TL$;CCA7mRi-9fg|$aa)% zp4o=7O`TemZDdueY=d+?$~N1GSym)lUK#9=YME`In`O4XKDZ~cVQ(ZBi-yN=g&ws# z5=%rQ@ypw3!jO~guXK<-XiH<8aa_-KG;;9GHeZYWwg-b|w#k}cXBbo5tQIB7%T{%r z8QW}qG_<>OOjpuN+pR6z&$iy~F+$SMKVuqkf;xsj@(e7qk<0m(?L?k6_l{w`HG2CP z>Sl~q7`ynGo9$O?N6*eNREf{nMl!@@J7|lcW6PGCWL(RZWm=rAwsj?f9oaI6&}?~K zaQ|gYR^riaOKc3E8uy|Xj*V%Q`Gl1nR%0lHM@X(#WE*ad?8JzWt=|}l#m6*|=YWlo z*q&&7Z1bC9m~Qoo%#r49y-=(0=!# zABknV!t)#!8?trX60`M|SgdOd>oW1j^f^A3nbX5soqIgn*R{y$NNjr-9y7-<)z6<} z7_@Za87(q~0h{R1H{GfefW%$QxL+V;RW&c?{i}8MZ~oG+h}? zBswA+u`ZXLd96LH_1Ok32r85q!^KTOtPzZ5fK(>%1T=;dmfaY-9!|2u-qIb$L_FM> z$Tn)iO!zWJdU_il%au+{wr_307^Z#MdQ0ctXsoMq50aB@Xg%p=8?@{l(|+TguAZ^{ zL2ufLb&A*+LHIkvk^R{L=#WFb*(O+o%NCm?+(>LpGiy*D)65@^?CHj6(i!R<+q_c} z+7-(V5^E59un>f&-o5SZ-LWq8evu73I9zUsiNPAISYpnLB~yhJB)N2=E+sI|qxC91 zyU%FP_Cwf)&P-7^X+Fkq!o&w6%{ZadbmX+7H4>ARJ}h~p$4Oh`^PFXF_nx+y?#k@D zj9A2NO?MaeUAnE=iq(+pTiq7yJHMTkw$L88A|j6?*1V7Dv$?-hvI{8@3wr)XbBm6C zKvTNGtc`Z!DU@TIWw9yaK_^p>^GT1sXU-xS#t1>eM;?!wNm^x*B{z0=cl6rRP3s9X z9HDKQPU_S+vrE>K^kkE!{VLU}a%1CYMOA{RTw&)9asV^nxgS8S_mDRt$TvUdmMIX*XkG(TsX; zDR|gRU@=1G$Ck1>l90PqX!Vw&Yrb{UzKfH2)rtpWgrnOP@=q3i8ERV;gA|!mGlovW zdd9UPM^Df66&6D~6OKK5ny+13bAHun6RbZbf^CSlsi?&H7c?WxhWD}&WyO1 zI_4N!i_?r|=92>h93eDg&|k)SGbD*DsvMR8v4qmp z(~-bSHPsaC=GC4b(Ob-b<6WH_cf~MMS8aA%(2L&bZc=yVjwxRe+!zh&$)_Wua*b?j znI_gMZ>D4|$vcv+gm&lNgelB+Jjyu6cBp!(#*yvhq+@F>2RJsYCyIP0LKK`K&=FhqbSHY9JL?UO z+8B(X9<|5xJDAmIluBogHN>g*ouW149S@0a8qQiR28em>?QG1>yC0RFwEHny=(l^S zWzY5qUR+@Mi3f!3J@8W=LeNdc_(}|cWEeS?wn(q<+#wpj^BvoYp46_2p%>lP72BP5tK$@jt%)LeooU^U#VVxH zdY;WT^mp#)itXXLXs{#8R{E_^=DZ@?7kl)fKiS3^54LGTw6Rt!1EWgdeJ7^(cB!<< z3+R@Ph|JPu1jsPb1!gVcb_Dm6WtiJ3M#T>Igc4~#?IPl{3|>b$fuZ&V_cX?0!QQlH z`y!zQJ2MQd!I63*x;@$vO=OtfAibiVIAy3Sdr{Hs+Ksmd>_RyXq(=nZ2DB&@8wrnw zn6@>12G%ChL*osEzG;i~qK#=Dj_e4c!`3%@Wnf_-dZCG-SX7pb@jAo0Lu%4Dc5_cF zXsGfcpEGM*By?G#(xZ-$RUN^dqXsJbU0`OvWQ`f=>3P<>2tB34{J}P{^10rsj$(3C zFxDy8mUt-G9gzij{VG6X4s#qD*Q5C|-+pA()PTx6fRkSm_vR4+)R9rxM zs9++rOSO07Eh0jD&n_wJO?)H$(X4!DVtpij_`Cw#1^m;Ut#v(JUbg zamvqyH(Jn4MsLmZYnimUjE8hjBFU8ZI_clGsD#{Nb`#wb?9f5VI-L%ZtgE|yHJ`m> zZJ5d7Y3?@sY(}Bp#S+``NY3|Ls8e*yg(kYyOn1e|czvrM^U_0)R%7~o+lJ231`=C) z9gvXzQ6hxmGyu42^zp z)cKiSJk>dxagSg!CP=D1m|>QXg>iL5SJaNLEVEiMC5>_6m0?)ToTfIH@*26+YB|na zeWmMB3$C;_TGKU?rC@ce=@p0(YDl+a_Pus*w7R2fd$1#VyB7J97LBq9NUHS$73E<4@6W-_N_hLvf7lX>V^rkRah#@u$`VerG2op?&xS~eIz0CjI`Vs+lgmU49fU{ zURsrhhK^|1iDTMC2E_DTv=hHOO5fsfEPbQnuo+XZwNd;UC~aT|nO0<_$$IaX79;kW zIx{X~w!zwvd5?Fx3H;KoYe%A9qK9fA4>L-SAspCaQ4zLu?(Xc`*NKrxHDgSdtMyPM z6DH*ZZI2ODrPSx@< z%jmNgul?5dV166JA_0H;<`1KPF;B)UKFb3bcMbi5YsLy?wj>DW$a*$j2Ez5v3bqhGsvYX&aJD7-aBDLT96)8XL_Fh-w@Ksk+pv0tqhkd@FT>ma3N*HqCO<4t`G6Dt_wf3!Y!XM^-n9}c08M- z!JVC5aZH%wEC{PZjf830AC5C~vsGEoYK@@3hYekmM*pBKV{uG-oh4ZBpcsP9?S5ac zqt!bdg|!gu#M_?ZO3dULbhPCOb_P3oa}=;fFGW6a)qqkk5?}8vN+4<#_e&L@ym$s z#3`eNGf3;5A$u_Cw(y+6serBPy4%zd#lUQ9UHU!aW$Lv(LRsx)7uH|w46%ovdi~s~ zZP+|x=&cD`s#O~jE42DL3bri^+OlMMY2)9oTY1P0gSF~9?>sRm9u7`*JgBnSuaxQz~)v> z<$F|}9Vkka*yVCSudp$Ei%R3mP-TgzN9VdlM>QZz9lNH#{onnq)~n_S>aX-kx_B!N zl6dFUFH@hsW5)3L6N_&;XPi4Rb6)<^EPaQjfE5ze-ihBBI1j5C<)y8&m{6#}ur4`e z*M0rt%u%80HM9L+3asn1_Cr?Y;?V5k58!26Liw=pMj|9yc;Bf(Pp z$o&ISSN{8gcpK@-3{bjUJMiTz=3|W4djVLmG8Ur)XQA5Meu~s4GG5vy92xB!34^~W z(KSNebXs0w(nzL%T0&!s2Ki$XJf~t^PRWxiJd>wBTF2s{{>2Ghr0=Xpq_n^&v2vz0 zKYkS-w_7(mpLFxqw{6z*_T^sj-6k)-^(LTT3!__S(2L8kj?=M|r}U@eekEMVEeWxe3j4zMQT^t&J1T+db_9 z{Y=^;d*)%g9P{m*c9;2@O)D`g4)9SPboDC(c|nI&ab7qUS?i7)dUR!#zP$_F>TvB_ zJV%RaZ`iSWk2Z|2tYg?Iv%YJe!lH=6v>7P1n7?OYrPJ&NzDc2xZaAS{nmRYzyTZLR z#t)EaB3JP#{hKE3!1;-j3Z(i5=Vg+1zV zrD|<|wcWqq>zs}7txLTJp@iVWJ8|v(OR*@}h%*LS5w2P?2O)*AKYkAOz0U(m{1W)D z1AcTfuJ|GinE1bw>A*;no?qetovE72dFiS&RI&mtJlF|91%J#s% zHW)i*av-|2^^XwNu%U<7%${LC%^X=AEl4txzV z94TwmiXtg>soyBOtEEl!pw#sJZ04x0MMhS8k-8p4u1ad=zq=Yp&!^M*mRKiE#EHFW72J!U#ojM)FqLV!F+#d`UZUuS{R@o?E*?Kiq}?bZMX* zi*|rZdw|^n+e{U_hdBcCYevUyWDcm3W|a3@mU#<&uygkE?xaQPGEe6am2uMBj?6{n zx?U;QJMf1StxHxS1=5O`c3%*-rL;%(0CUjmNLeE0v)fR+rES@xk?sqm)k)pbtyA}3 z^-5hgI-NnpB>^9#_C#?OO1l0?ZH=IG1+nC#)UvPt*m0CmG{$~XTA}!y zU|vYiC%uTY4jBhKQ5#0~>C(eUJmN5wbR#W)ReYoe+Y7hSOQ}lnLB`bd=#tepXSKP} zJ)EwyL6l7gYGe>T?^bHT=sX(BmGp(u8%a9R3+&GBy;i}$DE!sAB5hIUR=PC2s1=#M zjMW26>Dhf?T=#QtD}Ivq()&rv#i$v~j$bSDF|)TH>BbB9-AJL-CaIg5{jHR;^vUV* zLFHbHdsR6)?<9X@B$Aj&{lt{4dbFL9eTcLZj)Yk~+(>&_^PMdFx^Bq$8^H;#k~-;Q z@v{&3kF#87kB-M*vTu|~?U42;sd;;K1dB8b&Faslj1JH4pQKIT?BRzGFKgPgr}0zI z-+OPg`{oXn<24Mo%Zp(aq@qG@OG&R6^xRCmEq{V1*>6-V)mP6p>)K&mw_4Y3>l(1G z8?9@bbq!nBUE=GMQa614z2QmCD)Jf=JSoE%rz#>4!63T5YC=US*D!OuICM7;a)DvO zA$XqGs7RIMm3njF4iTF?Ij^da2v@}N-Z=9L@9R8^W=Xn2y3p$tlM zl?j7fg$R_CInePlMce8gxug(pci98KSdXhpq36 z;alp&WK$ww;?QJca=BT^yrUxNpFB}AC6bFH{8P8&Ql99|@gzU)Sd-$ZNCjbxOfx3u zA|q2@(-~kQ5xBWB-$mOMOGjRcl<7(STK`{MZn&l@Pc-Q33yUrcfQM$N|aWtqmuBX_U9BLqa~(aFbeg> zO|PG|z1~l+pLG=)+Tb~4Lq+l_s?VWLAsb*;IuY5J97K$tL6IHD^$f1ZKv5$>N>QQ; zA4GsNXed#)R7Bb>l)~56MWZ`dFkSg*Z<3KOFsv8s9Q161t$xF~*cs*7=Gf@aHtdUI zqrRx{2>}o8E$@TS~nHRPvY!vp!~lnKi`GxWU~MVlWJ{Y zzBB5S6MiO9g zxk&aqPIP)+L3)ZaUCYg|;}!adisWkL3bV1DAeG@j(k=(MQMwY{0k+lBP((g@Ec!lWW~i9KGiCsj@> zuNS40A+f7Sy>2HW#eVL9k|LFare-5$5Mk+nqT@f9PnvN$qL~F&F*_C7iJ5WsXvyy4a z=RC<5FszkIAI~Bvor0{98ZP-DS4=_RYbi!~1KkQ5gOpHdZGMT{Gk2Wf>6uV*C$Sgk zNv1r>yQRV0X#jd<;-O~^Qi-Ko@T9&_Dmit(j4r9iF@QjN0u@RkF`0Stl-uiq!_-rl zx`^puiI)Socz=II1n~(Xr~*u$5WqN`BPv%<4<}A{LcqPmDBr%as1fb<~RdA4EsF8)53A| zlUL=t-Ja!A!yaIGazRv64|rNwW8V*o28-m#RU6@on57;lw&Iw2027R9`GxRkoPjby z3U$H_&t$nnN_q`bXY)y>1@k0H67xweFy}>F>P#7D=YV*HsgqbY^Clyi@QR5wbz)5` zu?WPnCbc;Ayr<>r{Cu}(jVG1F|2oFFNZOKC+U@|g(uS)xveK4x(w3}94cI>2gXxd< z2^aC{9?Pdgpq5X#Y9q_1dmNwcsYso!%`b9$He%j`|Fuv1E7oZTxW~|g0q}{6GbH9rpnQ4+nw)9|QGd%ID)*r;Qdk_RKH?6}e=xk~689iKK(d?9Cv*n*$#lL39zvhb61!;>ZAI_C^dnNyp51>8PXp6XrPc@#@ zt9bNs{HwA2ldJew%DF(1w=Lf6PgJ#=4g?M-Tm?k^|g>8$;jl@AkjoW|>@kGHaR zyp^@MGD&iksE>D|K3>L2_<@OEYT{&;miCgpwI@}j9adSb&I`rz6Ic2vSMjsT@v{m; zYZbkEcpU1^o#_!(TsXt4WUP#e<5Y=M07u8zlwk{Z86IHV$-IRJdV zs!2K~7_N0Zagow>TUq3`l7p+(t>n0!seDO$ zgo}9elIH+2=bYV8k}p}l$Q3ms`I6&{*tFV5@|-k`18&_04#}aFZ6kW+5 z(Um-TR{M8S2Yb@$-A-DO$Cc5Ms|0(}3HD^gfnuwYC(pW{`n*m16+hdZ{p?N=Lp$n|QgGN`>har7OmMiGkCTWmPhv3e$^e|%r^)2+q!lHk-ge{#n`WXCfl;k#CGIIdR=eE=%wDVV{ygUZ<=yUDLFYD z+%3f?|8l|rx4qEs%A1mF=H+4`Y7-`NTX0>9IShIm6Sr$*%K3hQqXG{IJR)#NAPF05 zg?vn)4|fUtHRC2F)>C3LC1$BUfrn6o_2La|-C^cQ04+Irc$_N{1LvW@=9zK@!BvXDuhD+#GXk4LIza!(MmT37e%RIt-%>B7}zrN15fYxssJjo!bV7t+7Hc zag;WPA;lG`hsEV%v~}1pkr~nlq#l4J86q(4w56mY-D6AlIMRNU+D7qdziqZ(%J2z+ zr(m`fe^Nf|U|zM!B{Q$kwy$J}%6MG1^^GI-HIj=?1W>Cm8Ij1#j6uSmB~U9+WBX9U zSaNDH(yxiOjq$}9VVBx!l~e4=<4!`|wyC?!c4IyD+6He}B>AR9^_;*mCY$C#3vj|FNs76uBuxxC8agseetDW8j{3pB~8c;3WQaYZCw zxjYkL746!LH08 znl-SvY_5sNT{|Wfk&?LvmT+;euu>H%o@>YoutXB`IIoKwvDJ@;rhY6vsxOb|iw>es z-&hwN;POHQh-M%!60meHq2PfA*@LN}m)Ri?O;|-0rNqlWKhL)&cG|aNGNop+>r`&hA4m*xPW7HDJTx0lM9}0Y1)ydThp{V zO#^AVF-_ajG@PcpkOax-ml3zuix@W$M7fyj`NA1hyy#fUa!S6Ai!6$w^kw>uB9g^< zHy&|i&GQ%*3-Kt#bjTwdtJ&-2^3q1;u=+U%>k*P!_mny%*IbI3j+Hefx>zueI^^VA zYK;|7Qcd(rBnhpYiG_lsr{=|BcFT~{4MiG9o-Ea%rSs*6w3j?(PA>J_Y{xhG z(#ZIt7eRc>SebNuljlZ7H+eQ4-Q-y&R;NAr0-H#YEa9T#2NHMr!HAqi^-CPF3 zSXSsrg*xjwm(@^NdQG{DwXSc{_D^KkKjFx!iD@}CF)dHdkSFuC9LHum$kMb; zc{M3j9PvoaO3QOI#eH|tOrEW$Ob3Eh^p4d<}saet+A!f7CZJBtkw(~G~JR=Wzqa8^7W7(Z^)hIXq zN^@#tAOc_@5`36=l=uMg5#k|YQmn5f{TQ)NgoHq|++!dTq!SE8!XF~`3F38GjWP>s zB$n(`#a#_=x|DLt&oAmxz+Uh`> zo=el$8K_kY)A9+2qJl}ZSVoh@b58iu+N?C4D?y^JIQARTbd4RalC;*Arn~5LRys1t z6NJJCH`S#2^(}4qsX0&Yn`Ldu zzkcuh`g!o7bi8u}jck6#VF+?aONBV`+HSnCT4bm^y)8oFv!^P5GY;?x;-nvye9%zI z`?ZmGEzZK-&>i>cEfe0>=68B=9*tLgyVe`TKKof-2_h1k<&EQKxRLlQ?{@C)@^%N~ zxZw|>afRQ`VWG3UQTtcBv%GTrL?Yr1aho7Kv3`>s_ab%lQuZ9^7|yL-8VR-T4Wpl&x7cPs0LW_6oSh6S*UbJIrWU*3s!&{`& zq>t%RnDt@a2Sm0m!#Dr5p|i4;)3tiss*Eh9=&lZ+aW%0gU{Zo`J)`_nA&Ga){Xgz8GH~|E&VUt zjqxQ9$tZkS=Iy!8`mTw%#`l<5#Ycth{M6kk;?)toOIsYxS8B1L=8bZbk+pi42I>X& zA2=n7k4TMG3rBAKaKfY05g!J^2ZzPh>2}!7zi~5#3{P2_EjG9oSoq>2t{0UW-qa zD-)l$z{kf`@t%xBuyuqxF{*37y5;KyI$k~Y3C`GifbZ~*+DyZ7z6)Dc<@+!UjZxx> zTB;1+^X;||Sc0+43F6kyZ01lHhkEI86z3S&=b&pLlCf)&$z~w<;iBYKxBcY^{E8c4 z*dx3|K{B34dC^f^@0pfLm#cT~(}8Ru#s(!EQ?4EG08Tp4+thVNTi@!T;~2l1Lo3z> zU8Y8J#7Z)BU&e6@R=IPdAA`);)1(VUp8|v$N{{qp1s<{N`Mm(vCYS71Vhr~3lsCF1 zqgf){Pgd?8i zvtgX`IIbj2F|_s+t1ApufkP4aWwkK-Y&ndBQI7C~H8{6V#}YL%<4Aq0)Z~OYMgzMX zq_B)5fGIG|bQ{=kE=|J}B!_V~%PJ7}>%nTm8 zzj&60uD|K?YFW3CmFRV(KE-(gD@$G)j&mZix)PtIMhzW(oHAozoyklOI(X(8vxk(Y z1~CO_n%rNm*kJc#(~<<3;8Af5FqC=L0+nvedhu;M{e>W1<9g5(_sF?Y|fa z=7C`hH*6gw#4Zf*tG%}$RmQrF(ly?RzlQq_)zF2W*!m}I9R8~D_cY9&&M*@z=~yHFf*oRJ)@6`#|A zCx)(W=Q=wS#|akQv*Y32b5IxOSdSTVI@9Nm<6&fu9`86cukQ|TSFLLr=Pz2Utnjb2 z!u|)7+QXU=;R$x) z;Lp?H$LOrc@acc(|37m8U!q6EC*tvTWc>LQsOFBLi@i1Y+uDbx>YTKzwj9hv)s3nZ z?j-kz9e(c9wk9$9G6}bF+I_Fn^l4+6c)V^XL5dX=&wAIXi&A7q0-0|Kq27mMp z5kGe(bkSNBeQSfM?4*ff2T6KAkM`%WoD=dg0&&ti3}gpNwoebx&RnEh|F;l3T#yH9 zq45E@ZNhGdJ=|L)J2NmSjMydU#Xc$hc69^xfW*;U^zKHzkHd?Qo3Vpc_Jqiu4lnkl ztmnQe*#lzlMw$hlxyMcJ)9*srs-S@s?i@jG)jhr~a< z`$NZM(xTUYA*jIymHu=TBP^;$gZA_ zj2Mm`PCdewolgpTIpr(eYZ1;)x|G_{jWp{0E4$#ULjFiwk#IIxy5zfrW7j>o(>d~A zg}0RXBfGa`e`ycjz+WyuGxOzu?Azh4ue7_-xg&X^^QKNMhR=)CGUSu&yPShCVLuXC->#! eA?5Tx|NgJ!KoZk@DU%idSA72;Pyauj1OFR7EKVZ; literal 115712 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 diff --git a/Packages/rudder.sdk/Runtime/Plugins/WebGL/RudderWebSocket.jslib b/Packages/rudder.sdk/Runtime/Plugins/WebGL/RudderWebSocket.jslib deleted file mode 100644 index 51530cd..0000000 --- a/Packages/rudder.sdk/Runtime/Plugins/WebGL/RudderWebSocket.jslib +++ /dev/null @@ -1,66 +0,0 @@ -// WebSocket bridge for the Rudder Unity SDK (JsWebSocketAdapter). -// Implements the __Internal__ externals declared in Realtime/JsWebSocketAdapter.cs: -// RudderWebSocketCreate(gameObjectName, url) -// RudderWebSocketSend(gameObjectName, data, length) -// RudderWebSocketClose(gameObjectName) -// Events are routed back to the bridge GameObject via SendMessage: -// OnOpen(string), OnMessage(string base64), OnClose(string), OnError(string) - -var RudderWebSockets = {}; - -function RudderWebSocketBase64(bytes) { - var chunks = []; - var chunkSize = 0x8000; - for (var i = 0; i < bytes.length; i += chunkSize) { - chunks.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize))); - } - return btoa(chunks.join('')); -} - -mergeInto(LibraryManager.library, { - RudderWebSocketCreate: function (gameObjectName, url) { - var name = UTF8ToString(gameObjectName); - var socket = new WebSocket(UTF8ToString(url)); - socket.binaryType = 'arraybuffer'; - RudderWebSockets[name] = socket; - - socket.onopen = function () { - SendMessage(name, 'OnOpen', 'opened'); - }; - - socket.onmessage = function (event) { - var bytes; - if (typeof event.data === 'string') { - // Text frame: UTF-8 encode, then base64 (the C# side decodes base64 to bytes). - bytes = new TextEncoder().encode(event.data); - } else { - bytes = new Uint8Array(event.data); - } - SendMessage(name, 'OnMessage', RudderWebSocketBase64(bytes)); - }; - - socket.onerror = function () { - SendMessage(name, 'OnError', 'WebSocket error'); - }; - - socket.onclose = function () { - delete RudderWebSockets[name]; - SendMessage(name, 'OnClose', 'closed'); - }; - }, - - RudderWebSocketSend: function (gameObjectName, data, length) { - var socket = RudderWebSockets[UTF8ToString(gameObjectName)]; - if (!socket || socket.readyState !== WebSocket.OPEN) return; - socket.send(HEAPU8.subarray(data, data + length)); - }, - - RudderWebSocketClose: function (gameObjectName) { - var name = UTF8ToString(gameObjectName); - var socket = RudderWebSockets[name]; - if (socket) { - delete RudderWebSockets[name]; - socket.close(); - } - } -}); diff --git a/Packages/rudder.sdk/Runtime/Plugins/WebGL/RudderWebSocket.jslib.meta b/Packages/rudder.sdk/Runtime/Plugins/WebGL/RudderWebSocket.jslib.meta deleted file mode 100644 index ffd649e..0000000 --- a/Packages/rudder.sdk/Runtime/Plugins/WebGL/RudderWebSocket.jslib.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3f90f5c02b3cd42e8a599649ae0a142d \ No newline at end of file diff --git a/Packages/rudder.sdk/Runtime/Sources/Platform/RudderUnityClientFactory.cs b/Packages/rudder.sdk/Runtime/Sources/Platform/RudderUnityClientFactory.cs index 8532978..c86a16e 100644 --- a/Packages/rudder.sdk/Runtime/Sources/Platform/RudderUnityClientFactory.cs +++ b/Packages/rudder.sdk/Runtime/Sources/Platform/RudderUnityClientFactory.cs @@ -11,16 +11,12 @@ namespace RudderSdk.Unity var clientOptions = new RudderClientOptions { BaseUrl = options.BaseUrl, - RealtimeUrl = options.RealtimeUrl, ProjectKey = options.ProjectKey, Logger = new UnityLoggerAdapter(options.LoggerSink ?? new UnityDebugLoggerSink()), Transport = new UnityTransportAdapter(options.BaseUrl, executor), TokenStore = new UnityTokenStoreAdapter(options.KeyValueStore), DeviceIdProvider = new UnityDeviceIdProvider(), - Clock = new UnityClock(), - PlanStateStore = new UnityPlanStateStore(options.KeyValueStore), - Scheduler = new UnityPlanScheduler(), - RealtimeTransportFactory = new UnityRealtimeTransportFactory() + Clock = new UnityClock() }; return new RudderClient(clientOptions); diff --git a/Packages/rudder.sdk/Runtime/Sources/Platform/RudderUnityClientOptions.cs b/Packages/rudder.sdk/Runtime/Sources/Platform/RudderUnityClientOptions.cs index c80f3e5..c7316db 100644 --- a/Packages/rudder.sdk/Runtime/Sources/Platform/RudderUnityClientOptions.cs +++ b/Packages/rudder.sdk/Runtime/Sources/Platform/RudderUnityClientOptions.cs @@ -3,7 +3,6 @@ namespace RudderSdk.Unity public class RudderUnityClientOptions { public string BaseUrl { get; set; } - public string RealtimeUrl { get; set; } public string ProjectKey { get; set; } public int TimeoutSeconds { get; set; } = 10; public IUnityKeyValueStore KeyValueStore { get; set; } diff --git a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanScheduler.cs b/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanScheduler.cs deleted file mode 100644 index fa028a8..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanScheduler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using RudderSdk.Core.Abstractions; - -namespace RudderSdk.Unity -{ - internal class UnityPlanScheduler : IPlanScheduler - { - public Task ScheduleAsync(TimeSpan delay, CancellationToken cancellationToken = default) - { - return Task.Delay(delay, cancellationToken); - } - } -} diff --git a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanScheduler.cs.meta b/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanScheduler.cs.meta deleted file mode 100644 index 9ee2b78..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanScheduler.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f4f8e4e8237be445c96672d96294c6c7 \ No newline at end of file diff --git a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanStateStore.cs b/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanStateStore.cs deleted file mode 100644 index c615e55..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanStateStore.cs +++ /dev/null @@ -1,30 +0,0 @@ -using RudderSdk.Core.Abstractions; - -namespace RudderSdk.Unity -{ - internal class UnityPlanStateStore : IPlanStateStore - { - private const string KEY = "liveops_plan_runner_state"; - - private readonly IUnityKeyValueStore _store; - - public UnityPlanStateStore(IUnityKeyValueStore store) - { - _store = store; - } - - public string State - { - get => _store.GetString(KEY); - set - { - if (string.IsNullOrEmpty(value)) - _store.DeleteKey(KEY); - else - _store.SetString(KEY, value); - - _store.Save(); - } - } - } -} diff --git a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanStateStore.cs.meta b/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanStateStore.cs.meta deleted file mode 100644 index 51e75e2..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityPlanStateStore.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 118ea3286be8b4daab53eaf7885507a0 \ No newline at end of file diff --git a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityRealtimeTransportFactory.cs b/Packages/rudder.sdk/Runtime/Sources/Platform/UnityRealtimeTransportFactory.cs deleted file mode 100644 index d4beaab..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityRealtimeTransportFactory.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using RudderSdk.Core.Abstractions; - -namespace RudderSdk.Unity -{ - internal sealed class UnityRealtimeTransportFactory : IRealtimeTransportFactory - { - public IRealtimeTransport Create() - { -#if UNITY_WEBGL && !UNITY_EDITOR - return new UnityRealtimeTransport(new JsWebSocketAdapter()); -#else - return new UnityRealtimeTransport(new NativeWebSocketAdapter()); -#endif - } - } - - internal sealed class UnityRealtimeTransport : IRealtimeTransport - { - private readonly IWebSocketAdapter _adapter; - - public UnityRealtimeTransport(IWebSocketAdapter adapter) - { - _adapter = adapter; - _adapter.Closed += () => Closed?.Invoke(); - _adapter.Received += data => Received?.Invoke(data); - _adapter.ReceivedError += ex => Error?.Invoke(ex); - } - - public event Action Closed; - public event Action> Received; - public event Action Error; - - public bool IsConnected => _adapter.IsConnected; - - public Task ConnectAsync(Uri uri, TimeSpan timeout, CancellationToken cancellationToken = default) - => _adapter.ConnectAsync(uri, (int)Math.Ceiling(timeout.TotalSeconds), cancellationToken); - - public Task SendAsync(ArraySegment data, CancellationToken cancellationToken = default) - => _adapter.SendAsync(data, cancellationToken); - - public Task CloseAsync(CancellationToken cancellationToken = default) - => _adapter.CloseAsync(); - - public void Update(float deltaTime) - { - } - } -} diff --git a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityRealtimeTransportFactory.cs.meta b/Packages/rudder.sdk/Runtime/Sources/Platform/UnityRealtimeTransportFactory.cs.meta deleted file mode 100644 index fcebb54..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Platform/UnityRealtimeTransportFactory.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: be75d4878806d40648e49338f0ecd508 \ No newline at end of file diff --git a/Packages/rudder.sdk/Runtime/Sources/Realtime/IWebSocketAdapter.cs b/Packages/rudder.sdk/Runtime/Sources/Realtime/IWebSocketAdapter.cs deleted file mode 100644 index c4e3721..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Realtime/IWebSocketAdapter.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace RudderSdk.Unity -{ - /// - /// Interface for WebSocket transport adapters used by the realtime transport bridge. - /// Provides connect, send, close, and main-thread event dispatch for WebSocket connections. - /// - public interface IWebSocketAdapter - { - event Action Closed; - event Action> Received; - event Action ReceivedError; - bool IsConnected { get; } - bool IsConnecting { get; } - Task ConnectAsync(Uri uri, int timeoutSeconds, CancellationToken cancellationToken = default); - Task CloseAsync(); - Task SendAsync(ArraySegment data, CancellationToken cancellationToken = default); - } -} diff --git a/Packages/rudder.sdk/Runtime/Sources/Realtime/IWebSocketAdapter.cs.meta b/Packages/rudder.sdk/Runtime/Sources/Realtime/IWebSocketAdapter.cs.meta deleted file mode 100644 index 96fc80a..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Realtime/IWebSocketAdapter.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a7216258d1bc14b1699fce5966a73073 \ No newline at end of file diff --git a/Packages/rudder.sdk/Runtime/Sources/Realtime/JsWebSocketAdapter.cs b/Packages/rudder.sdk/Runtime/Sources/Realtime/JsWebSocketAdapter.cs deleted file mode 100644 index 60bf6d2..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Realtime/JsWebSocketAdapter.cs +++ /dev/null @@ -1,312 +0,0 @@ -#if UNITY_WEBGL && !UNITY_EDITOR -using System.Runtime.InteropServices; -#endif -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using UnityEngine; - -namespace RudderSdk.Unity -{ - /// - /// WebGL WebSocket adapter using JavaScript interop via DllImport("__Internal__"). - /// Uses SendMessage callbacks from JS to route WebSocket events to Unity's main thread. - /// Only functional in WebGL builds; throws PlatformNotSupportedException elsewhere. - /// - public sealed class JsWebSocketAdapter : IWebSocketAdapter, IDisposable - { -#if UNITY_WEBGL && !UNITY_EDITOR - [DllImport("__Internal__")] - private static extern void RudderWebSocketCreate(string gameObjectName, string url); - - [DllImport("__Internal__")] - private static extern void RudderWebSocketSend(string gameObjectName, byte[] data, int length); - - [DllImport("__Internal__")] - private static extern void RudderWebSocketClose(string gameObjectName); -#endif - - private CancellationTokenSource _cts; - private readonly ConcurrentQueue _eventQueue = new(); - private JsWebSocketBridge _bridge; - private TaskCompletionSource _connectionTcs; - private volatile bool _isConnected; - private volatile bool _isConnecting; - private bool _disposed; - - /// - /// Fired when the WebSocket connection is closed. - /// - public event Action Closed; - - /// - /// Fired when binary data is received from the server. - /// - public event Action> Received; - - /// - /// Fired when an error occurs on the WebSocket. - /// - public event Action ReceivedError; - - /// - /// True when the WebSocket connection is open. - /// - public bool IsConnected => _isConnected; - - /// - /// True while the WebSocket connection is being established. - /// - public bool IsConnecting => _isConnecting; - - public JsWebSocketAdapter() - { - var go = new GameObject("JsWebSocketBridge"); - UnityEngine.Object.DontDestroyOnLoad(go); - _bridge = go.AddComponent(); - _bridge.Init(this); - } - - /// - /// Connects to the specified WebSocket URI via the JavaScript WebSocket API. - /// - /// The WebSocket server URI (ws:// or wss://). - /// Connection timeout in seconds. - /// Optional cancellation token. - /// Thrown when uri is null. - /// Thrown when timeoutSeconds is not positive. - /// Thrown when the connection attempt times out. - /// Thrown when not running in WebGL. - public Task ConnectAsync(Uri uri, int timeoutSeconds, CancellationToken cancellationToken = default) - { - if (uri == null) - throw new ArgumentNullException(nameof(uri)); - if (timeoutSeconds <= 0) - throw new ArgumentOutOfRangeException(nameof(timeoutSeconds), "Timeout must be greater than zero."); - -#if UNITY_WEBGL && !UNITY_EDITOR - _isConnecting = true; - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _connectionTcs = new TaskCompletionSource(); - - // Register cancellation (including timeout) - _cts.Token.Register(() => - { - if (_connectionTcs?.TrySetException( - new TimeoutException($"WebSocket connection timed out after {timeoutSeconds} seconds.")) == true) - { - _isConnecting = false; - } - }); - _cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); - - RudderWebSocketCreate(_bridge.gameObject.name, uri.ToString()); - - return _connectionTcs.Task; -#else - // Not available outside WebGL builds; editor test uses NativeWebSocketAdapter. - return Task.FromException(new PlatformNotSupportedException( - "JsWebSocketAdapter is only supported on WebGL. Use NativeWebSocketAdapter for other platforms.")); -#endif - } - - /// - /// Gracefully closes the WebSocket connection via the JavaScript WebSocket API. - /// - public Task CloseAsync() - { -#if UNITY_WEBGL && !UNITY_EDITOR - RudderWebSocketClose(_bridge.gameObject.name); -#endif - _cts?.Cancel(); - _isConnected = false; - _isConnecting = false; - EnqueueEvent(() => Closed?.Invoke()); - return Task.CompletedTask; - } - - /// - /// Sends binary data over the WebSocket via the JavaScript WebSocket API. - /// - public Task SendAsync(ArraySegment data, CancellationToken cancellationToken = default) - { - if (!_isConnected) return Task.CompletedTask; - -#if UNITY_WEBGL && !UNITY_EDITOR - byte[] arr; - int offset; - int count; - if (data.Offset == 0 && data.Count == data.Array.Length) - { - arr = data.Array; - offset = 0; - count = data.Count; - } - else - { - // Copy to a contiguous buffer since DllImport needs a pinned array from offset 0 - arr = new byte[data.Count]; - System.Buffer.BlockCopy(data.Array, data.Offset, arr, 0, data.Count); - offset = 0; - count = data.Count; - } - - RudderWebSocketSend(_bridge.gameObject.name, arr, count); -#endif - return Task.CompletedTask; - } - - /// - /// Disposes the adapter, cleaning up the bridge GameObject. - /// - public void Dispose() - { - if (_disposed) return; - _disposed = true; - -#if UNITY_WEBGL && !UNITY_EDITOR - RudderWebSocketClose(_bridge.gameObject.name); -#endif - _cts?.Cancel(); - _cts?.Dispose(); - - if (_bridge != null && _bridge.gameObject != null) - { - UnityEngine.Object.Destroy(_bridge.gameObject); - } - } - - /// - /// Called by JsWebSocketBridge when the JS WebSocket onopen fires. - /// - internal void HandleOpen() - { - _isConnected = true; - _isConnecting = false; - _connectionTcs?.TrySetResult(true); - } - - /// - /// Called by JsWebSocketBridge when the JS WebSocket onclose fires. - /// - internal void HandleClose() - { - _isConnected = false; - _isConnecting = false; - _connectionTcs?.TrySetCanceled(); - Closed?.Invoke(); - } - - /// Called by JsWebSocketBridge when the JS WebSocket onerror fires. - /// - internal void HandleError(string errorMessage) - { - _isConnecting = false; - _connectionTcs?.TrySetException(new Exception(errorMessage)); - ReceivedError?.Invoke(new Exception(errorMessage)); - } - - /// Called by JsWebSocketBridge when binary message data is received. - /// - internal void HandleMessage(ArraySegment data) - { - Received?.Invoke(data); - } - - internal void HandleMessageError(Exception ex) - { - ReceivedError?.Invoke(ex); - } - - internal void EnqueueEvent(Action action) - { - _eventQueue.Enqueue(action); - } - - internal void ProcessEvents() - { - while (_eventQueue.TryDequeue(out var action)) - { - try - { - action?.Invoke(); - } - catch (Exception ex) - { - Debug.LogException(ex); - } - } - } - } - - /// - /// MonoBehaviour that receives SendMessage callbacks from the JavaScript WebSocket bridge - /// and dispatches events on the Unity main thread. - /// - internal class JsWebSocketBridge : MonoBehaviour - { - private JsWebSocketAdapter _adapter; - - public void Init(JsWebSocketAdapter adapter) - { - _adapter = adapter; - } - - /// - /// Called from JavaScript via SendMessage when the WebSocket connection opens. - /// - public void OnOpen(string _) - { - _adapter?.EnqueueEvent(() => _adapter?.HandleOpen()); - } - - /// - /// Called from JavaScript via SendMessage when binary data is received. - /// Data is base64-encoded by the JS bridge and decoded here. - /// - public void OnMessage(string base64Data) - { - if (string.IsNullOrEmpty(base64Data) || _adapter == null) return; - - try - { - var data = Convert.FromBase64String(base64Data); - var segment = new ArraySegment(data); - _adapter.EnqueueEvent(() => _adapter.HandleMessage(segment)); - } - catch (FormatException ex) - { - Debug.LogError($"JsWebSocketBridge: Failed to decode base64 message: {ex.Message}"); - _adapter.EnqueueEvent(() => _adapter.HandleMessageError(ex)); - } - } - - /// - /// Called from JavaScript via SendMessage when the WebSocket connection closes. - /// - public void OnClose(string _) - { - _adapter?.EnqueueEvent(() => _adapter?.HandleClose()); - } - - /// - /// Called from JavaScript via SendMessage when a WebSocket error occurs. - /// - public void OnError(string error) - { - _adapter?.EnqueueEvent(() => _adapter?.HandleError(error ?? "Unknown WebSocket error")); - } - - private void Update() - { - _adapter?.ProcessEvents(); - } - - private void OnDestroy() - { - _adapter?.Dispose(); - _adapter = null; - } - } -} diff --git a/Packages/rudder.sdk/Runtime/Sources/Realtime/JsWebSocketAdapter.cs.meta b/Packages/rudder.sdk/Runtime/Sources/Realtime/JsWebSocketAdapter.cs.meta deleted file mode 100644 index fc40c61..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Realtime/JsWebSocketAdapter.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e53d8ae2e7487470280f0da178c56cf8 \ No newline at end of file diff --git a/Packages/rudder.sdk/Runtime/Sources/Realtime/NativeWebSocketAdapter.cs b/Packages/rudder.sdk/Runtime/Sources/Realtime/NativeWebSocketAdapter.cs deleted file mode 100644 index d6eec29..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Realtime/NativeWebSocketAdapter.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; -using UnityEngine; - -namespace RudderSdk.Unity -{ - /// - /// Native WebSocket adapter using System.Net.WebSockets.ClientWebSocket. - /// Dispatches received events to the Unity main thread via a MonoBehaviour Update loop. - /// - public sealed class NativeWebSocketAdapter : IWebSocketAdapter, IDisposable - { - private ClientWebSocket _ws; - private CancellationTokenSource _cts; - private readonly WebSocketDispatcher _dispatcher; - private readonly ConcurrentQueue _eventQueue = new(); - private bool _disposed; - - /// - /// Fired when the WebSocket connection is closed. - /// - public event Action Closed; - - /// - /// Fired when binary data is received from the server. - /// - public event Action> Received; - - /// - /// Fired when an error occurs on the receive loop. - /// - public event Action ReceivedError; - - /// - /// True when the WebSocket is in the Open state. - /// - public bool IsConnected => _ws?.State == WebSocketState.Open; - - /// - /// True when the WebSocket is in the Connecting state. - /// - public bool IsConnecting => _ws?.State == WebSocketState.Connecting; - - public NativeWebSocketAdapter() - { - var go = new GameObject("WebSocketDispatcher"); - UnityEngine.Object.DontDestroyOnLoad(go); - _dispatcher = go.AddComponent(); - _dispatcher.Init(this); - } - - /// - /// Connects to the specified WebSocket URI. - /// - /// The WebSocket server URI (ws:// or wss://). - /// Connection timeout in seconds. - /// Optional cancellation token. - /// Thrown when uri is null. - /// Thrown when timeoutSeconds is not positive. - /// Thrown when the connection attempt times out. - public async Task ConnectAsync(Uri uri, int timeoutSeconds, CancellationToken cancellationToken = default) - { - if (uri == null) - throw new ArgumentNullException(nameof(uri)); - if (timeoutSeconds <= 0) - throw new ArgumentOutOfRangeException(nameof(timeoutSeconds), "Timeout must be greater than zero."); - - _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - _ws = new ClientWebSocket(); - _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(30); - - using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)); - using var linked = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, timeoutCts.Token); - - try - { - await _ws.ConnectAsync(uri, linked.Token); - _ = ReceiveLoop(); - } - catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) - { - throw new TimeoutException($"WebSocket connection timed out after {timeoutSeconds} seconds."); - } - } - - /// - /// Gracefully closes the WebSocket connection. - /// - public async Task CloseAsync() - { - if (_ws?.State == WebSocketState.Open) - { - try - { - await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client closing", CancellationToken.None); - } - catch - { - // Best-effort close - } - } - - _cts?.Cancel(); - EnqueueEvent(() => Closed?.Invoke()); - } - - /// - /// Sends binary data over the WebSocket. WebSocket transport is always reliable, - /// so no reliability parameter is needed. - /// - public async Task SendAsync(ArraySegment data, CancellationToken cancellationToken = default) - { - if (_ws?.State != WebSocketState.Open) - return; - - await _ws.SendAsync(data, WebSocketMessageType.Binary, true, cancellationToken); - } - - /// - /// Disposes the adapter, cancelling any in-flight operations and cleaning up the dispatcher GameObject. - /// - public void Dispose() - { - if (_disposed) return; - _disposed = true; - - _cts?.Cancel(); - _cts?.Dispose(); - _ws?.Dispose(); - - if (_dispatcher != null && _dispatcher.gameObject != null) - { - UnityEngine.Object.Destroy(_dispatcher.gameObject); - } - } - - private async Task ReceiveLoop() - { - var buffer = new byte[65536]; - try - { - while (!_cts.IsCancellationRequested && _ws.State == WebSocketState.Open) - { - using (var ms = new MemoryStream()) - { - WebSocketReceiveResult result; - do - { - result = await _ws.ReceiveAsync(new ArraySegment(buffer), _cts.Token); - ms.Write(buffer, 0, result.Count); - } while (!result.EndOfMessage); - - if (result.MessageType == WebSocketMessageType.Close) - { - try - { - await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - } - catch - { - // Best-effort close - } - - EnqueueEvent(() => Closed?.Invoke()); - return; - } - - var data = ms.ToArray(); - EnqueueEvent(() => Received?.Invoke(new ArraySegment(data))); - } - } - } - catch (OperationCanceledException) - { - // Normal cancellation, no action needed - } - catch (Exception ex) when (!_disposed) - { - EnqueueEvent(() => ReceivedError?.Invoke(ex)); - EnqueueEvent(() => Closed?.Invoke()); - } - } - - internal void EnqueueEvent(Action action) - { - _eventQueue.Enqueue(action); - } - - internal void ProcessEvents() - { - while (_eventQueue.TryDequeue(out var action)) - { - try - { - action?.Invoke(); - } - catch (Exception ex) - { - Debug.LogException(ex); - } - } - } - } - - /// - /// MonoBehaviour used to dispatch WebSocket events on the Unity main thread via Update. - /// Created and managed by NativeWebSocketAdapter. - /// - internal class WebSocketDispatcher : MonoBehaviour - { - private NativeWebSocketAdapter _adapter; - - public void Init(NativeWebSocketAdapter adapter) - { - _adapter = adapter; - } - - private void Update() - { - _adapter?.ProcessEvents(); - } - - private void OnDestroy() - { - _adapter?.Dispose(); - _adapter = null; - } - } -} diff --git a/Packages/rudder.sdk/Runtime/Sources/Realtime/NativeWebSocketAdapter.cs.meta b/Packages/rudder.sdk/Runtime/Sources/Realtime/NativeWebSocketAdapter.cs.meta deleted file mode 100644 index 7c7779b..0000000 --- a/Packages/rudder.sdk/Runtime/Sources/Realtime/NativeWebSocketAdapter.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 508609c897805437dab5527c21c9f914 \ No newline at end of file diff --git a/Packages/rudder.sdk/Runtime/Sources/Rudder.cs b/Packages/rudder.sdk/Runtime/Sources/Rudder.cs index aa4f153..068dc57 100644 --- a/Packages/rudder.sdk/Runtime/Sources/Rudder.cs +++ b/Packages/rudder.sdk/Runtime/Sources/Rudder.cs @@ -15,8 +15,8 @@ namespace RudderSdk.Unity /// /// Scene bootstrap MonoBehaviour and public entrypoint of the Rudder Unity SDK. - /// Add the component to a scene with a RudderConfiguration assigned, or call - /// Rudder.Initialize(configuration) and the hidden GameObject is created for you. + /// Put this on a startup scene object, assign a RudderConfiguration, then call + /// . Feature calls live on the returned Core client. /// [DefaultExecutionOrder(-1500), DisallowMultipleComponent] public class Rudder : MonoBehaviour @@ -43,33 +43,77 @@ namespace RudderSdk.Unity if (State != RudderState.Ready || _client == null) { throw new InvalidOperationException( - "Rudder is not initialized. Add the Rudder component to the scene with a " + - "RudderConfiguration assigned, or call Rudder.Initialize(configuration)."); + "Rudder is not initialized. Add the Rudder component to a scene with a " + + "RudderConfiguration assigned, then call Rudder.Initialize()."); } return _client; } } - /// Completes with the initialized client, or faults with the initialization error. + /// Completes with the client after , or faults on + /// the initialization error. Prefer in game code. public static Task Ready => _readyCompletion.Task; - public static void Initialize(RudderConfiguration configuration) + public static CoreClient Initialize() { - if (configuration == null) - throw new ArgumentNullException(nameof(configuration)); + if (State == RudderState.Ready && _client != null) + return _client; - if (State == RudderState.Initializing || State == RudderState.Ready) - throw new InvalidOperationException("Rudder is already initialized."); + if (State == RudderState.Failed) + { + _readyCompletion = NewCompletion(); + LastError = null; + State = RudderState.NotInitialized; + } if (_instance == null) { - var host = new GameObject(nameof(Rudder)); - _instance = host.AddComponent(); + throw new InvalidOperationException( + "Rudder is not in the scene. Add the Rudder component to a startup scene " + + "and assign a RudderConfiguration."); } - _instance._configuration = configuration; - _ = _instance.InitializeAsync(); + if (_instance._configuration == null) + { + throw new InvalidOperationException( + "Rudder has no configuration. Assign a RudderConfiguration on the Rudder " + + "component (Assets > Create > Rudder > Configuration)."); + } + + try + { + State = RudderState.Initializing; + LastError = null; + + var options = new RudderUnityClientOptions + { + BaseUrl = _instance._configuration.BaseUrl, + ProjectKey = _instance._configuration.ProjectKey, + TimeoutSeconds = _instance._configuration.TimeoutSeconds, + KeyValueStore = new PlayerPrefsUnityKeyValueStore(), + LoggerSink = new UnityDebugLoggerSink() + }; + + _client = RudderUnityClientFactory.Create(options); + } + catch (Exception exception) + { + _client = null; + State = RudderState.Failed; + LastError = exception; + _readyCompletion.TrySetException(exception); + Debug.LogError( + "[Rudder] Failed to initialize the Rudder SDK. Check the RudderConfiguration asset " + + $"(ProjectKey and BaseUrl must be set). {exception.Message}"); + throw; + } + + State = RudderState.Ready; + Debug.Log("[Rudder] Rudder SDK initialized."); + _readyCompletion.TrySetResult(_client); + Initialized?.Invoke(_client); + return _client; } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] @@ -95,75 +139,16 @@ namespace RudderSdk.Unity DontDestroyOnLoad(gameObject); } - private void Start() - { - if (_instance == this && _configuration != null && State == RudderState.NotInitialized) - _ = InitializeAsync(); - } - - private async Task InitializeAsync() - { - State = RudderState.Initializing; - LastError = null; - - try - { - var options = new RudderUnityClientOptions - { - BaseUrl = _configuration.BaseUrl, - RealtimeUrl = _configuration.RealtimeUrl, - ProjectKey = _configuration.ProjectKey, - TimeoutSeconds = _configuration.TimeoutSeconds, - KeyValueStore = new PlayerPrefsUnityKeyValueStore(), - LoggerSink = new UnityDebugLoggerSink() - }; - - _client = RudderUnityClientFactory.Create(options); - - try - { - await _client.Scenario.RestoreAsync(); - } - catch (Exception restoreException) - { - Debug.LogWarning("[Rudder] Failed to restore scenario state: " + restoreException.Message); - } - } - catch (Exception exception) - { - _client = null; - State = RudderState.Failed; - LastError = exception; - _readyCompletion.TrySetException(exception); - Debug.LogError( - "[Rudder] Failed to initialize the Rudder SDK. Check the RudderConfiguration asset " + - $"(ProjectKey and BaseUrl must be set). {exception.Message}"); - return; - } - - State = RudderState.Ready; - Debug.Log("[Rudder] Rudder SDK initialized."); - _readyCompletion.TrySetResult(_client); - Initialized?.Invoke(_client); - } - private void Update() { _client?.Update(Time.deltaTime); } - private void OnApplicationQuit() - { - DisconnectRealtime(); - } - private void OnDestroy() { if (_instance != this) return; - DisconnectRealtime(); - _instance = null; _client = null; State = RudderState.NotInitialized; @@ -172,25 +157,6 @@ namespace RudderSdk.Unity _readyCompletion = NewCompletion(); } - private void DisconnectRealtime() - { - var client = _client; - if (client == null) - return; - - try - { - _ = client.Realtime.DisconnectAsync().ContinueWith( - task => Debug.LogWarning( - "[Rudder] Realtime disconnect failed: " + task.Exception?.GetBaseException().Message), - TaskContinuationOptions.OnlyOnFaulted); - } - catch (Exception exception) - { - Debug.LogWarning("[Rudder] Realtime disconnect failed: " + exception.Message); - } - } - private static TaskCompletionSource NewCompletion() { return new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/Packages/rudder.sdk/Runtime/Sources/RudderConfiguration.cs b/Packages/rudder.sdk/Runtime/Sources/RudderConfiguration.cs index 2552bb2..4f2d78f 100644 --- a/Packages/rudder.sdk/Runtime/Sources/RudderConfiguration.cs +++ b/Packages/rudder.sdk/Runtime/Sources/RudderConfiguration.cs @@ -11,9 +11,6 @@ namespace RudderSdk.Unity [Tooltip("API base URL, e.g. https://api.rudder.build.")] public string BaseUrl = "https://api.rudder.build"; - [Tooltip("Realtime websocket URL. The relay is not deployed yet; set this only once it is.")] - public string RealtimeUrl = "wss://api.rudder.build/api/realtime/ws"; - [Tooltip("HTTP request timeout in seconds.")] [Min(1)] public int TimeoutSeconds = 10; diff --git a/Packages/rudder.sdk/Runtime/Plugins/WebGL.meta b/Packages/rudder.sdk/Samples~/Authentication.meta similarity index 77% rename from Packages/rudder.sdk/Runtime/Plugins/WebGL.meta rename to Packages/rudder.sdk/Samples~/Authentication.meta index 5726a39..6fa03e3 100644 --- a/Packages/rudder.sdk/Runtime/Plugins/WebGL.meta +++ b/Packages/rudder.sdk/Samples~/Authentication.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 2dda93a7ed40941b3a831824b938fe46 +guid: 53ede345941d4634ac87be4a7bbda9cd folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Packages/rudder.sdk/Samples~/Authentication/Authentication.unity b/Packages/rudder.sdk/Samples~/Authentication/Authentication.unity new file mode 100644 index 0000000..9f0c6b9 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Authentication/Authentication.unity @@ -0,0 +1,303 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 0000000000000000d000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &100001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 100003} + - component: {fileID: 100002} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &100002 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &100003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &200001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 200003} + - component: {fileID: 200002} + m_Layer: 0 + m_Name: Sample + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &200002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: ef14aea70c3b46e0a15e09dce6e0e810, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.AuthenticationSample + nickname: Player + region: global + language: en +--- !u!4 &200003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &300001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 300003} + - component: {fileID: 300002} + m_Layer: 0 + m_Name: Rudder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &300002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder + _configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2} +--- !u!4 &300003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 100003} + - {fileID: 300003} + - {fileID: 200003} diff --git a/Packages/rudder.sdk/Samples~/Authentication/Authentication.unity.meta b/Packages/rudder.sdk/Samples~/Authentication/Authentication.unity.meta new file mode 100644 index 0000000..22bd274 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Authentication/Authentication.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 90eddd30462947fca53cffee662972fa +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Authentication/AuthenticationSample.cs b/Packages/rudder.sdk/Samples~/Authentication/AuthenticationSample.cs new file mode 100644 index 0000000..4a874d0 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Authentication/AuthenticationSample.cs @@ -0,0 +1,87 @@ +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using RudderSdk.Unity; + +namespace RudderSdk.Unity.Samples +{ + public class AuthenticationSample : MonoBehaviour + { + [SerializeField] string nickname = "Player"; + [SerializeField] string region = "global"; + [SerializeField] string language = "en"; + + readonly SamplePage _page = new SamplePage( + "Authentication", + "Initialize the SDK, sign in with the device id, then load the player profile."); + + void OnGUI() + { + _page.Draw(() => + { + GUILayout.BeginHorizontal(); + _page.Label("Nickname"); + nickname = GUILayout.TextField(nickname, GUILayout.MaxWidth(200f)); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + if (_page.Button("Login with device")) + _page.Run(this, Login); + if (_page.Button("Load profile")) + _page.Run(this, LoadProfile); + if (_page.Button("Logout")) + Logout(); + GUILayout.EndHorizontal(); + }); + } + + async Task Login() + { + var client = Rudder.Initialize(); + _page.Log("Auth.LoginWithDeviceAsync(" + region + ", " + language + ", " + nickname + ")"); + await client.Auth.LoginWithDeviceAsync(region, language, nickname); + _page.SetStatus("Signed in"); + await LoadProfile(); + } + + async Task LoadProfile() + { + var client = Rudder.Initialize(); + _page.Log("Player.GetProfileAsync()"); + var profile = await client.Player.GetProfileAsync(); + var player = profile?.Player; + var text = new StringBuilder(); + text.AppendLine("Player id: " + (player?.Id ?? "—")); + text.AppendLine("Nickname: " + (player?.Nickname ?? "—")); + text.AppendLine("Region: " + (player?.Region ?? "—")); + text.AppendLine("Language: " + (player?.Language ?? "—")); + if (profile?.Wallets == null || profile.Wallets.Count == 0) + { + text.Append("Wallets: none"); + } + else + { + text.Append("Wallets:"); + foreach (var wallet in profile.Wallets) + text.Append("\n " + wallet.Balance + " " + wallet.Currency); + } + + _page.SetOutput(text.ToString()); + _page.SetStatus("Profile loaded"); + } + + void Logout() + { + if (Rudder.State != RudderState.Ready) + { + _page.SetStatus("SDK is not initialized.", true); + return; + } + + Rudder.Client.Auth.Logout(); + _page.SetOutput(string.Empty); + _page.SetStatus("Signed out"); + _page.Log("Auth.Logout()"); + } + } +} diff --git a/Packages/rudder.sdk/Samples~/Authentication/AuthenticationSample.cs.meta b/Packages/rudder.sdk/Samples~/Authentication/AuthenticationSample.cs.meta new file mode 100644 index 0000000..7eabd1a --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Authentication/AuthenticationSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef14aea70c3b46e0a15e09dce6e0e810 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Runtime/Sources/Realtime.meta b/Packages/rudder.sdk/Samples~/Leaderboards.meta similarity index 77% rename from Packages/rudder.sdk/Runtime/Sources/Realtime.meta rename to Packages/rudder.sdk/Samples~/Leaderboards.meta index 47f0bd6..754932f 100644 --- a/Packages/rudder.sdk/Runtime/Sources/Realtime.meta +++ b/Packages/rudder.sdk/Samples~/Leaderboards.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 3c34eb0ba13fe47eab84ba9a30d8bdb6 +guid: 770e3347e0604c359d5ca59689007d42 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Packages/rudder.sdk/Samples~/Leaderboards/Leaderboards.unity b/Packages/rudder.sdk/Samples~/Leaderboards/Leaderboards.unity new file mode 100644 index 0000000..fd84442 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Leaderboards/Leaderboards.unity @@ -0,0 +1,304 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 0000000000000000d000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &100001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 100003} + - component: {fileID: 100002} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &100002 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &100003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &200001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 200003} + - component: {fileID: 200002} + m_Layer: 0 + m_Name: Sample + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &200002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6cbf51073b96453cba7f019ab7b790be, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.LeaderboardsSample + nickname: Player + region: global + language: en + slug: cozy-collector-score +--- !u!4 &200003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &300001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 300003} + - component: {fileID: 300002} + m_Layer: 0 + m_Name: Rudder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &300002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder + _configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2} +--- !u!4 &300003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 100003} + - {fileID: 300003} + - {fileID: 200003} diff --git a/Packages/rudder.sdk/Samples~/Leaderboards/Leaderboards.unity.meta b/Packages/rudder.sdk/Samples~/Leaderboards/Leaderboards.unity.meta new file mode 100644 index 0000000..2931c49 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Leaderboards/Leaderboards.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7e30e842ebc54b63a9885d5200c91557 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Leaderboards/LeaderboardsSample.cs b/Packages/rudder.sdk/Samples~/Leaderboards/LeaderboardsSample.cs new file mode 100644 index 0000000..e0f4c93 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Leaderboards/LeaderboardsSample.cs @@ -0,0 +1,93 @@ +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using RudderSdk.Unity; + +namespace RudderSdk.Unity.Samples +{ + public class LeaderboardsSample : MonoBehaviour + { + [SerializeField] string nickname = "Player"; + [SerializeField] string region = "global"; + [SerializeField] string language = "en"; + [SerializeField] string slug = "cozy-collector-score"; + + readonly SamplePage _page = new SamplePage( + "Leaderboards", + "Submit a score to a board by slug, then list the top entries."); + + string _scoreText = "10"; + + void Start() + { + _page.Run(this, async () => + { + await Rudder.Initialize().Auth.LoginWithDeviceAsync(region, language, nickname); + _page.Log("Signed in"); + await List(); + }); + } + + void OnGUI() + { + _page.Draw(() => + { + GUILayout.BeginHorizontal(); + _page.Label("Score"); + _scoreText = GUILayout.TextField(_scoreText, GUILayout.MaxWidth(120f)); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + if (_page.Button("Submit")) + _page.Run(this, Submit); + if (_page.Button("List top 10")) + _page.Run(this, List); + GUILayout.EndHorizontal(); + }); + } + + async Task Submit() + { + if (!int.TryParse(_scoreText, out var score)) + { + _page.SetStatus("Score must be an integer.", true); + return; + } + + var client = Rudder.Initialize(); + var board = client.Leaderboards.FindBySlug(slug); + _page.Log("Leaderboards.FindBySlug(\"" + slug + "\").SubmitAsync(" + score + ")"); + await board.SubmitAsync(score); + _page.SetStatus("Submitted " + score); + await List(); + } + + async Task List() + { + var client = Rudder.Initialize(); + var board = client.Leaderboards.FindBySlug(slug); + _page.Log("FindBySlug(\"" + slug + "\").ListAsync(10)"); + var entries = await board.ListAsync(10); + + var text = new StringBuilder(); + text.AppendLine(slug); + if (entries == null || entries.Count == 0) + { + text.Append("No entries yet."); + } + else + { + foreach (var entry in entries) + { + var name = entry.PlayerName; + if (string.IsNullOrEmpty(name)) + name = entry.PlayerId; + text.AppendLine("#" + entry.Rank + " " + name + " " + entry.Score); + } + } + + _page.SetOutput(text.ToString()); + _page.SetStatus("Listed " + (entries == null ? 0 : entries.Count) + " entries"); + } + } +} diff --git a/Packages/rudder.sdk/Samples~/Leaderboards/LeaderboardsSample.cs.meta b/Packages/rudder.sdk/Samples~/Leaderboards/LeaderboardsSample.cs.meta new file mode 100644 index 0000000..c31c023 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Leaderboards/LeaderboardsSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6cbf51073b96453cba7f019ab7b790be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/README.md b/Packages/rudder.sdk/Samples~/README.md new file mode 100644 index 0000000..e59bf8e --- /dev/null +++ b/Packages/rudder.sdk/Samples~/README.md @@ -0,0 +1,20 @@ +# Rudder feature samples + +Small scenes that show one Rudder API each. Open a scene, assign a +`RudderConfiguration` if the field is empty, press Play. + +| Scene | Shows | +| --- | --- | +| `Authentication/Authentication.unity` | `Rudder.Initialize`, `Auth.LoginWithDeviceAsync`, `Player.GetProfileAsync`, `Auth.Logout` | +| `RemoteConfig/RemoteConfig.unity` | `RemoteConfig.LoadAsync`, `Get(key, fallback)` | +| `Storage/Storage.unity` | `Storage.GetAsync`, `SaveAsync`, `DeleteAsync` | +| `StoreInventory/StoreInventory.unity` | `Stores.ListAsync`, `PurchaseAsync`, `Inventory.GetAsync` | +| `Leaderboards/Leaderboards.unity` | `Leaderboards.FindBySlug`, `SubmitAsync`, `ListAsync` | +| `Scenarios/Scenarios.unity` | `Scenario.TriggerAsync` and `Effects.OnNotification` / `Effects.OnStoreOffer` / … | + +Each scene has a `Rudder` component using `Assets/LiveOpsLocal.asset`. +Login uses the device id owned by the SDK (PlayerPrefs). + +Stage data that makes the later scenes interesting is listed in the project +README (store `cozy-camp-shop`, leaderboard `cozy-collector-score`, scenario +events `player_login` / `demo_round_finished`). diff --git a/Packages/rudder.sdk/Samples~/README.md.meta b/Packages/rudder.sdk/Samples~/README.md.meta new file mode 100644 index 0000000..a511e76 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 94f8aa6a797a488581772bb70594a153 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/RemoteConfig.meta b/Packages/rudder.sdk/Samples~/RemoteConfig.meta new file mode 100644 index 0000000..6f51cbf --- /dev/null +++ b/Packages/rudder.sdk/Samples~/RemoteConfig.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 964c9816fce94c8392cc1c85d5d21a83 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfig.unity b/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfig.unity new file mode 100644 index 0000000..9fae6d5 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfig.unity @@ -0,0 +1,305 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 0000000000000000d000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &100001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 100003} + - component: {fileID: 100002} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &100002 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &100003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &200001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 200003} + - component: {fileID: 200002} + m_Layer: 0 + m_Name: Sample + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &200002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 75288bf5899a4ee2a805b1de8d640fc0, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.RemoteConfigSample + nickname: Player + region: global + language: en + roundSecondsKey: demo_round_seconds + playerSpeedKey: demo_player_speed +--- !u!4 &200003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &300001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 300003} + - component: {fileID: 300002} + m_Layer: 0 + m_Name: Rudder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &300002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder + _configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2} +--- !u!4 &300003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 100003} + - {fileID: 300003} + - {fileID: 200003} diff --git a/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfig.unity.meta b/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfig.unity.meta new file mode 100644 index 0000000..2527f88 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfig.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 232ad1be9e3840e9ae3f64b58db8992d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfigSample.cs b/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfigSample.cs new file mode 100644 index 0000000..d44eea8 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfigSample.cs @@ -0,0 +1,60 @@ +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using RudderSdk.Unity; + +namespace RudderSdk.Unity.Samples +{ + public class RemoteConfigSample : MonoBehaviour + { + [SerializeField] string nickname = "Player"; + [SerializeField] string region = "global"; + [SerializeField] string language = "en"; + [SerializeField] string roundSecondsKey = "demo_round_seconds"; + [SerializeField] string playerSpeedKey = "demo_player_speed"; + + readonly SamplePage _page = new SamplePage( + "Remote Config", + "Load the project's remote config cache, then read typed values with fallbacks."); + + void Start() + { + _page.Run(this, async () => + { + await Rudder.Initialize().Auth.LoginWithDeviceAsync(region, language, nickname); + _page.Log("Signed in"); + await Load(); + }); + } + + void OnGUI() + { + _page.Draw(() => + { + if (_page.Button("Reload")) + _page.Run(this, Load); + }); + } + + async Task Load() + { + var client = Rudder.Initialize(); + _page.Log("RemoteConfig.LoadAsync()"); + var configs = await client.RemoteConfig.LoadAsync(); + + var roundSeconds = client.RemoteConfig.Get(roundSecondsKey, 150f); + var playerSpeed = client.RemoteConfig.Get(playerSpeedKey, 260f); + + var text = new StringBuilder(); + text.AppendLine(roundSecondsKey + " = " + roundSeconds); + text.AppendLine(playerSpeedKey + " = " + playerSpeed); + text.AppendLine(); + text.AppendLine("All keys (" + configs.Count + "):"); + foreach (var pair in configs) + text.AppendLine(" " + pair.Key + " = " + pair.Value.Value + " (" + pair.Value.ValueType + ")"); + + _page.SetOutput(text.ToString()); + _page.SetStatus("Loaded " + configs.Count + " config(s)"); + } + } +} diff --git a/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfigSample.cs.meta b/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfigSample.cs.meta new file mode 100644 index 0000000..bcd3602 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/RemoteConfig/RemoteConfigSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 75288bf5899a4ee2a805b1de8d640fc0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Rudder.Unity.Samples.asmdef b/Packages/rudder.sdk/Samples~/Rudder.Unity.Samples.asmdef new file mode 100644 index 0000000..ffd1e21 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Rudder.Unity.Samples.asmdef @@ -0,0 +1,16 @@ +{ + "name": "Rudder.Unity.Samples", + "rootNamespace": "RudderSdk.Unity.Samples", + "references": [ + "Rudder.Unity" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Packages/rudder.sdk/Samples~/Rudder.Unity.Samples.asmdef.meta b/Packages/rudder.sdk/Samples~/Rudder.Unity.Samples.asmdef.meta new file mode 100644 index 0000000..a27fb26 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Rudder.Unity.Samples.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9b15bdad283e494c9f77a456c0f06d07 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Scenarios.meta b/Packages/rudder.sdk/Samples~/Scenarios.meta new file mode 100644 index 0000000..178e650 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Scenarios.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 15b529225eaf43f093572b9d8938c6fe +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Scenarios/Scenarios.unity b/Packages/rudder.sdk/Samples~/Scenarios/Scenarios.unity new file mode 100644 index 0000000..59dfd94 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Scenarios/Scenarios.unity @@ -0,0 +1,305 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 0000000000000000d000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &100001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 100003} + - component: {fileID: 100002} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &100002 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &100003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &200001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 200003} + - component: {fileID: 200002} + m_Layer: 0 + m_Name: Sample + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &200002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 72ab1e68bc9f48588c945a033026622e, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.ScenariosSample + nickname: Player + region: global + language: en + loginEvent: player_login + roundEvent: demo_round_finished +--- !u!4 &200003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &300001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 300003} + - component: {fileID: 300002} + m_Layer: 0 + m_Name: Rudder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &300002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder + _configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2} +--- !u!4 &300003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 100003} + - {fileID: 300003} + - {fileID: 200003} diff --git a/Packages/rudder.sdk/Samples~/Scenarios/Scenarios.unity.meta b/Packages/rudder.sdk/Samples~/Scenarios/Scenarios.unity.meta new file mode 100644 index 0000000..3ec0862 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Scenarios/Scenarios.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d57ceb109f374f93862dd52f93d5961f +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Scenarios/ScenariosSample.cs b/Packages/rudder.sdk/Samples~/Scenarios/ScenariosSample.cs new file mode 100644 index 0000000..beb5354 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Scenarios/ScenariosSample.cs @@ -0,0 +1,201 @@ +using System; +using System.Threading.Tasks; +using RudderSdk.Core; +using UnityEngine; +using RudderSdk.Unity; + +namespace RudderSdk.Unity.Samples +{ + public class ScenariosSample : MonoBehaviour + { + [SerializeField] string nickname = "Player"; + [SerializeField] string region = "global"; + [SerializeField] string language = "en"; + [SerializeField] string loginEvent = "player_login"; + [SerializeField] string roundEvent = "demo_round_finished"; + + readonly SamplePage _page = new SamplePage( + "Scenarios", + "Subscribe to scenario effects, trigger an event, then resolve nodes from UI."); + + StoreOfferEffect _offer; + bool _subscribed; + + void Start() + { + _page.Run(this, async () => + { + await Rudder.Initialize().Auth.LoginWithDeviceAsync(region, language, nickname); + Subscribe(); + _page.SetStatus("Signed in, listening for scenario effects"); + _page.Log("Subscribed to Effects.On*"); + }); + } + + void OnDestroy() + { + Unsubscribe(); + } + + void OnGUI() + { + _page.Draw(() => + { + GUILayout.BeginHorizontal(); + if (_page.Button("Trigger " + loginEvent)) + _page.Run(this, () => Trigger(loginEvent)); + if (_page.Button("Trigger " + roundEvent)) + _page.Run(this, () => Trigger(roundEvent)); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + if (_page.Button("Buy offer")) + _page.Run(this, BuyOffer); + if (_page.Button("Decline offer")) + DeclineOffer(); + GUILayout.EndHorizontal(); + }); + } + + async Task Trigger(string eventName) + { + var client = Rudder.Initialize(); + Subscribe(); + _page.Log("Scenario.TriggerAsync(\"" + eventName + "\")"); + await client.Scenario.TriggerAsync(eventName); + _page.SetStatus("Triggered " + eventName); + } + + async Task BuyOffer() + { + if (_offer == null) + { + _page.SetStatus("No store-offer effect.", true); + return; + } + + var client = Rudder.Initialize(); + var storeSlug = _offer.Get("storeSlug", "cozy-camp-shop"); + var store = await client.Stores.GetAsync(storeSlug); + var offer = store?.Offers != null && store.Offers.Count > 0 ? store.Offers[0] : null; + if (offer == null) + throw new InvalidOperationException("Store " + storeSlug + " has no offers."); + + _page.Log("Stores.PurchaseAsync(" + storeSlug + ", " + offer.Id + ")"); + var response = await client.Stores.PurchaseAsync(storeSlug, offer.Id); + if (response != null && response.Success != true) + throw new InvalidOperationException(response.Error ?? "Purchase rejected"); + + _offer.Purchase(); + _offer = null; + _page.SetStatus("Offer purchased"); + _page.Log("StoreOfferEffect.Purchase()"); + } + + void DeclineOffer() + { + if (_offer == null) + { + _page.SetStatus("No store-offer effect.", true); + return; + } + + _offer.Decline(); + _offer = null; + _page.SetStatus("Offer declined"); + _page.Log("StoreOfferEffect.Decline()"); + } + + void Subscribe() + { + if (_subscribed || Rudder.State != RudderState.Ready) + return; + + var effects = Rudder.Client.Effects; + effects.OnNotification += OnNotification; + effects.OnStoreOffer += OnStoreOffer; + effects.OnWait += OnWait; + effects.OnQuest += OnQuest; + effects.OnLeaderboard += OnLeaderboard; + effects.OnBattlePass += OnBattlePass; + effects.OnBattlePassLevel += OnBattlePassLevel; + effects.OnScenarioCompleted += OnCompleted; + effects.OnScenarioFailed += OnFailed; + _subscribed = true; + } + + void Unsubscribe() + { + if (!_subscribed || Rudder.State != RudderState.Ready) + return; + + var effects = Rudder.Client.Effects; + effects.OnNotification -= OnNotification; + effects.OnStoreOffer -= OnStoreOffer; + effects.OnWait -= OnWait; + effects.OnQuest -= OnQuest; + effects.OnLeaderboard -= OnLeaderboard; + effects.OnBattlePass -= OnBattlePass; + effects.OnBattlePassLevel -= OnBattlePassLevel; + effects.OnScenarioCompleted -= OnCompleted; + effects.OnScenarioFailed -= OnFailed; + _subscribed = false; + } + + void OnNotification(NotificationEffect effect) + { + var title = effect.Title; + var message = effect.Message; + _page.Log("OnNotification: " + title + " " + message); + _page.SetOutput(title + "\n" + message); + effect.Done(); + } + + void OnStoreOffer(StoreOfferEffect effect) + { + _offer = effect; + var storeSlug = effect.Get("storeSlug", "cozy-camp-shop"); + _page.Log("OnStoreOffer: " + storeSlug); + _page.SetOutput("Store offer from " + storeSlug + "\nUse Buy offer or Decline offer."); + _page.SetStatus("Store offer waiting"); + } + + void OnWait(WaitEffect effect) + { + _page.Log("OnWait until " + effect.DeadlineUtc.ToString("O")); + } + + void OnQuest(QuestEffect effect) + { + _page.Log("OnQuest: " + effect.NodeId); + } + + void OnLeaderboard(LeaderboardEffect effect) + { + _page.Log("OnLeaderboard: " + effect.NodeId); + effect.End(); + } + + void OnBattlePass(BattlePassEffect effect) + { + _page.Log("OnBattlePass: " + effect.NodeId); + } + + void OnBattlePassLevel(BattlePassLevelEffect effect) + { + _page.Log("OnBattlePassLevel: " + effect.NodeId); + } + + void OnCompleted(ScenarioCompletedEffect effect) + { + _page.Log("OnScenarioCompleted: " + effect.RunId); + _page.SetStatus("Scenario completed"); + } + + void OnFailed(ScenarioFailedEffect failed) + { + _page.Log("OnScenarioFailed: " + failed.NodeId + " " + failed.Exception.Message); + _page.SetStatus(failed.Exception.Message, true); + } + } +} diff --git a/Packages/rudder.sdk/Samples~/Scenarios/ScenariosSample.cs.meta b/Packages/rudder.sdk/Samples~/Scenarios/ScenariosSample.cs.meta new file mode 100644 index 0000000..45114aa --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Scenarios/ScenariosSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 72ab1e68bc9f48588c945a033026622e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Shared.meta b/Packages/rudder.sdk/Samples~/Shared.meta new file mode 100644 index 0000000..dafaf4f --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Shared.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4b81c103928547eba13e72280ba03a08 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Shared/SamplePage.cs b/Packages/rudder.sdk/Samples~/Shared/SamplePage.cs new file mode 100644 index 0000000..7d66e51 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Shared/SamplePage.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using UnityEngine; + +namespace RudderSdk.Unity.Samples +{ + sealed class SamplePage + { + const int MaxLogLines = 40; + + readonly string _title; + readonly string _subtitle; + readonly List _logLines = new List(); + string _status = "Idle"; + bool _error; + string _output = string.Empty; + bool _busy; + Vector2 _outputScroll; + Vector2 _logScroll; + GUIStyle _titleStyle; + GUIStyle _bodyStyle; + GUIStyle _statusStyle; + + public SamplePage(string title, string subtitle) + { + _title = title; + _subtitle = subtitle; + } + + public void Draw(Action body) + { + EnsureStyles(); + const float pad = 16f; + GUILayout.BeginArea(new Rect(pad, pad, Screen.width - pad * 2f, Screen.height - pad * 2f)); + GUILayout.Label(_title, _titleStyle); + GUILayout.Label(_subtitle, _bodyStyle); + _statusStyle.normal.textColor = _error ? new Color(1f, 0.45f, 0.4f) : Color.white; + GUILayout.Label(_status, _statusStyle); + GUILayout.Space(8f); + + var enabled = GUI.enabled; + GUI.enabled = enabled && !_busy; + body(); + GUI.enabled = enabled; + + if (!string.IsNullOrEmpty(_output)) + { + GUILayout.Space(8f); + _outputScroll = GUILayout.BeginScrollView(_outputScroll, GUILayout.MinHeight(120f)); + GUILayout.Label(_output, _bodyStyle); + GUILayout.EndScrollView(); + } + + GUILayout.Space(8f); + GUILayout.Label("Log", _titleStyle); + _logScroll = GUILayout.BeginScrollView(_logScroll, GUILayout.MinHeight(100f)); + if (_logLines.Count == 0) + GUILayout.Label("(empty)", _bodyStyle); + else + GUILayout.Label(string.Join("\n", _logLines), _bodyStyle); + GUILayout.EndScrollView(); + GUILayout.EndArea(); + } + + public bool Button(string text) + { + return GUILayout.Button(text, GUILayout.Height(28f), GUILayout.MaxWidth(280f)); + } + + public void Label(string text) + { + GUILayout.Label(text, _bodyStyle); + } + + public void SetStatus(string text, bool error = false) + { + _status = text; + _error = error; + } + + public void SetOutput(string text) + { + _output = text ?? string.Empty; + } + + public void Log(string line) + { + _logLines.Insert(0, DateTime.Now.ToString("HH:mm:ss") + " " + line); + if (_logLines.Count > MaxLogLines) + _logLines.RemoveAt(_logLines.Count - 1); + } + + public async void Run(MonoBehaviour host, Func action) + { + if (_busy) + return; + + _busy = true; + SetStatus("Working…"); + try + { + await action(); + } + catch (Exception exception) + { + await Awaitable.MainThreadAsync(); + if (host == null) + return; + + SetStatus(exception.Message, true); + Log(exception.Message); + } + finally + { + _busy = false; + } + } + + void EnsureStyles() + { + if (_titleStyle != null) + return; + + _titleStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 20, + fontStyle = FontStyle.Bold, + wordWrap = true + }; + _titleStyle.normal.textColor = Color.white; + + _bodyStyle = new GUIStyle(GUI.skin.label) + { + fontSize = 14, + wordWrap = true + }; + _bodyStyle.normal.textColor = Color.white; + + _statusStyle = new GUIStyle(_bodyStyle); + } + } +} diff --git a/Packages/rudder.sdk/Samples~/Shared/SamplePage.cs.meta b/Packages/rudder.sdk/Samples~/Shared/SamplePage.cs.meta new file mode 100644 index 0000000..762eb08 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Shared/SamplePage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b306e86aff644e0adfa093a21ca4d97 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Storage.meta b/Packages/rudder.sdk/Samples~/Storage.meta new file mode 100644 index 0000000..efc3133 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Storage.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f9ab4042bed849c6a532bb712b2b863c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Storage/Storage.unity b/Packages/rudder.sdk/Samples~/Storage/Storage.unity new file mode 100644 index 0000000..dca09de --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Storage/Storage.unity @@ -0,0 +1,305 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 0000000000000000d000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &100001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 100003} + - component: {fileID: 100002} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &100002 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &100003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &200001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 200003} + - component: {fileID: 200002} + m_Layer: 0 + m_Name: Sample + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &200002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8fb25d5ed4f44c4f88b064592d30d17b, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.StorageSample + nickname: Player + region: global + language: en + storageType: sample_save + storageId: main +--- !u!4 &200003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &300001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 300003} + - component: {fileID: 300002} + m_Layer: 0 + m_Name: Rudder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &300002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder + _configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2} +--- !u!4 &300003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 100003} + - {fileID: 300003} + - {fileID: 200003} diff --git a/Packages/rudder.sdk/Samples~/Storage/Storage.unity.meta b/Packages/rudder.sdk/Samples~/Storage/Storage.unity.meta new file mode 100644 index 0000000..cdd7bf1 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Storage/Storage.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0e058d0347144fb5acff8c1d22ab6367 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/Storage/StorageSample.cs b/Packages/rudder.sdk/Samples~/Storage/StorageSample.cs new file mode 100644 index 0000000..45198c5 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Storage/StorageSample.cs @@ -0,0 +1,105 @@ +using System.Threading.Tasks; +using RudderSdk.Core.Models.Storage; +using UnityEngine; +using RudderSdk.Unity; + +namespace RudderSdk.Unity.Samples +{ + public class StorageSample : MonoBehaviour + { + [SerializeField] string nickname = "Player"; + [SerializeField] string region = "global"; + [SerializeField] string language = "en"; + [SerializeField] string storageType = "sample_save"; + [SerializeField] string storageId = "main"; + + readonly SamplePage _page = new SamplePage( + "Storage", + "Read and write a player storage item. Data is an opaque JSON string."); + + string _data = "{\"bestScore\":0}"; + + void Start() + { + _page.Run(this, async () => + { + await Rudder.Initialize().Auth.LoginWithDeviceAsync(region, language, nickname); + _page.Log("Signed in"); + await Load(); + }); + } + + void OnGUI() + { + _page.Draw(() => + { + _page.Label("Data"); + _data = GUILayout.TextArea(_data, GUILayout.MinHeight(72f)); + GUILayout.BeginHorizontal(); + if (_page.Button("Load")) + _page.Run(this, Load); + if (_page.Button("Save")) + _page.Run(this, Save); + if (_page.Button("Delete type")) + _page.Run(this, Delete); + GUILayout.EndHorizontal(); + }); + } + + async Task Load() + { + var client = Rudder.Initialize(); + _page.Log("Storage.GetAsync(type: " + storageType + ")"); + var response = await client.Storage.GetAsync(storageType); + StorageItem item = null; + if (response?.Items != null) + { + foreach (var candidate in response.Items) + { + if (candidate.Id == storageId) + { + item = candidate; + break; + } + } + } + + if (item == null) + { + _page.SetOutput("No item " + storageType + "/" + storageId + " yet."); + _page.SetStatus("Empty"); + return; + } + + _data = item.Data; + _page.SetOutput("Loaded " + item.Type + "/" + item.Id + "\n" + item.Data); + _page.SetStatus("Loaded"); + } + + async Task Save() + { + var client = Rudder.Initialize(); + _page.Log("Storage.SaveAsync(" + storageType + "/" + storageId + ")"); + await client.Storage.SaveAsync(new[] + { + new StorageItem + { + Type = storageType, + Id = storageId, + Data = _data + } + }); + _page.SetOutput("Saved " + storageType + "/" + storageId); + _page.SetStatus("Saved"); + } + + async Task Delete() + { + var client = Rudder.Initialize(); + _page.Log("Storage.DeleteAsync(" + storageType + ")"); + await client.Storage.DeleteAsync(storageType); + _page.SetOutput("Deleted all items of type " + storageType); + _page.SetStatus("Deleted"); + } + } +} diff --git a/Packages/rudder.sdk/Samples~/Storage/StorageSample.cs.meta b/Packages/rudder.sdk/Samples~/Storage/StorageSample.cs.meta new file mode 100644 index 0000000..18e0adc --- /dev/null +++ b/Packages/rudder.sdk/Samples~/Storage/StorageSample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8fb25d5ed4f44c4f88b064592d30d17b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/StoreInventory.meta b/Packages/rudder.sdk/Samples~/StoreInventory.meta new file mode 100644 index 0000000..55df08a --- /dev/null +++ b/Packages/rudder.sdk/Samples~/StoreInventory.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ef7f15e047164a2e97b85f62bd9e38da +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventory.unity b/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventory.unity new file mode 100644 index 0000000..a1fbcae --- /dev/null +++ b/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventory.unity @@ -0,0 +1,303 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 0000000000000000d000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 2 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &100001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 100003} + - component: {fileID: 100002} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!20 &100002 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.12, g: 0.13, b: 0.15, a: 1} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &100003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 100001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &200001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 200003} + - component: {fileID: 200002} + m_Layer: 0 + m_Name: Sample + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &200002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7e51322b24f541848b432de2202ac5b2, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity.Samples::RudderSdk.Unity.Samples.StoreInventorySample + nickname: Player + region: global + language: en +--- !u!4 &200003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 200001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &300001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 300003} + - component: {fileID: 300002} + m_Layer: 0 + m_Name: Rudder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &300002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: efb778515f4ca44289f365faa2acecd7, type: 3} + m_Name: + m_EditorClassIdentifier: Rudder.Unity::RudderSdk.Unity.Rudder + _configuration: {fileID: 11400000, guid: 98d531126f0d748ceacfb85bdd959e81, type: 2} +--- !u!4 &300003 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 300001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 100003} + - {fileID: 300003} + - {fileID: 200003} diff --git a/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventory.unity.meta b/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventory.unity.meta new file mode 100644 index 0000000..b7bd6de --- /dev/null +++ b/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventory.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2c93fb60baaf4d5b936a324c8161529f +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventorySample.cs b/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventorySample.cs new file mode 100644 index 0000000..ec460a3 --- /dev/null +++ b/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventorySample.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using RudderSdk.Core.Models.Stores; +using UnityEngine; +using RudderSdk.Unity; + +namespace RudderSdk.Unity.Samples +{ + public class StoreInventorySample : MonoBehaviour + { + [SerializeField] string nickname = "Player"; + [SerializeField] string region = "global"; + [SerializeField] string language = "en"; + + readonly SamplePage _page = new SamplePage( + "Store & Inventory", + "List store offers, purchase one, then refresh wallets and inventory."); + + IReadOnlyList _stores = Array.Empty(); + + void Start() + { + _page.Run(this, async () => + { + await Rudder.Initialize().Auth.LoginWithDeviceAsync(region, language, nickname); + _page.Log("Signed in"); + await LoadCatalog(); + await LoadInventory(); + }); + } + + void OnGUI() + { + _page.Draw(() => + { + GUILayout.BeginHorizontal(); + if (_page.Button("Refresh catalog")) + _page.Run(this, LoadCatalog); + if (_page.Button("Refresh inventory")) + _page.Run(this, LoadInventory); + GUILayout.EndHorizontal(); + + foreach (var store in _stores) + { + GUILayout.Space(6f); + _page.Label(store.Name + " (" + store.Slug + ")"); + if (store.Offers == null || store.Offers.Count == 0) + { + _page.Label(" no offers"); + continue; + } + + foreach (var offer in store.Offers) + { + var slug = store.Slug; + var offerId = offer.Id; + var name = string.IsNullOrEmpty(offer.Name) ? offer.Id : offer.Name; + var price = offer.Price == null ? "free" : offer.Price.Amount + " " + offer.Price.Currency; + if (_page.Button("Buy " + name + " — " + price)) + _page.Run(this, () => Buy(slug, offerId, name)); + } + } + }); + } + + async Task LoadCatalog() + { + var client = Rudder.Initialize(); + _page.Log("Stores.ListAsync()"); + var stores = await client.Stores.ListAsync(); + _stores = stores ?? Array.Empty(); + _page.SetStatus(_stores.Count == 0 ? "No stores" : "Loaded " + _stores.Count + " store(s)"); + } + + async Task Buy(string storeSlug, string offerId, string name) + { + var client = Rudder.Initialize(); + _page.Log("Stores.PurchaseAsync(" + storeSlug + ", " + offerId + ")"); + var response = await client.Stores.PurchaseAsync(storeSlug, offerId); + if (response != null && response.Success != true) + throw new InvalidOperationException(response.Error ?? "Purchase rejected"); + + _page.SetStatus("Purchased " + name); + await LoadInventory(); + } + + async Task LoadInventory() + { + var client = Rudder.Initialize(); + _page.Log("Inventory.GetAsync() / Player.GetProfileAsync()"); + var items = await client.Inventory.GetAsync(); + var profile = await client.Player.GetProfileAsync(); + + var text = new StringBuilder(); + text.Append("Wallets:"); + if (profile?.Wallets == null || profile.Wallets.Count == 0) + { + text.Append(" none"); + } + else + { + foreach (var wallet in profile.Wallets) + text.Append("\n " + wallet.Balance + " " + wallet.Currency); + } + + text.Append("\n\nInventory:"); + if (items == null || items.Count == 0) + { + text.Append(" empty"); + } + else + { + foreach (var item in items) + { + var name = string.IsNullOrEmpty(item.NameOverride) ? item.Slug : item.NameOverride; + text.Append("\n " + name + " × " + item.Amount); + } + } + + _page.SetOutput(text.ToString()); + } + } +} diff --git a/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventorySample.cs.meta b/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventorySample.cs.meta new file mode 100644 index 0000000..ffaff2d --- /dev/null +++ b/Packages/rudder.sdk/Samples~/StoreInventory/StoreInventorySample.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e51322b24f541848b432de2202ac5b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/rudder.sdk/package.json b/Packages/rudder.sdk/package.json index 954c9a2..5ff0873 100644 --- a/Packages/rudder.sdk/package.json +++ b/Packages/rudder.sdk/package.json @@ -1,8 +1,8 @@ { "name": "rudder.sdk", - "version": "0.3.0", + "version": "0.4.0", "displayName": "Rudder SDK", - "description": "LiveOps SDK for Unity — player auth, remote config, stores, inventory, leaderboards, scenarios, realtime.", + "description": "LiveOps SDK for Unity — player auth, remote config, stores, inventory, leaderboards, scenarios.", "unity": "6000.0", "author": { "name": "Rudder", @@ -15,5 +15,12 @@ "documentationUrl": "https://app.rudder.build", "publishConfig": { "registry": "https://hub.rudder.build/api/packages/rudder/npm/" - } + }, + "samples": [ + { + "displayName": "Feature Samples", + "description": "Small scenes that show one Rudder API each: authentication, remote config, storage, stores and inventory, leaderboards, and scenarios.", + "path": "Samples~" + } + ] } diff --git a/README.md b/README.md index b03a975..5b91162 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,8 @@ -# LiveOps Unity Cozy Collector Demo +# Rudder Unity SDK -This Unity project is a playable port of the Cozy Collector web demo (`liveops-phaser-demo`). It uses the same arcade loop — move around the camp, collect spawning snacks before the round timer runs out — to show how a game can use LiveOps services without shipping a client update. - -The demo is assembled like a regular Unity game: the scene, world, and UI live in baked assets (scene, prefabs, ScriptableObjects, imported sprites), and runtime scripts only contain logic wired through the inspector. All assets are generated by Editor bake scripts under `Tools > LiveOps`. - -## Demonstrated Services - -| Service | Demo use | -| --- | --- | -| Authorization | Device login with the configured project key. | -| Player | Loads player profile and wallets after login; session card shows nickname, region, wallets. | -| Remote Config | Controls round duration, player speed, spawn interval, collectible score; live updates highlight changed keys. | -| Storage | Saves best score and last round state (`cozy_fantasy_save` / `main`). | -| Stores | Shop modal lists the `cozy-camp-shop` catalog; purchases debit wallets. | -| Inventory | Backpack modal lists collected items via `Rudder.Client.Inventory`. | -| Leaderboards | Submits round score to `cozy-collector-score` and shows the top entries in the round-result modal. | -| Scenarios | Sends `player_login` after login and `demo_round_finished` after a round; scenario offers show a Buy/Decline modal, `remote_config_override` applies live. | +This project hosts the `rudder.sdk` UPM package and the scenes that show how +to call it. Agents integrating the SDK into a game should read +`Packages/rudder.sdk/AGENTS.md`. ## Requirements @@ -29,62 +16,45 @@ The demo is assembled like a regular Unity game: the scene, world, and UI live i 2. Copy `Assets/LiveOpsLocal.asset.example` to `Assets/LiveOpsLocal.asset` (the real asset is git-ignored because it holds your project key). 3. Set `ProjectKey` to your project key. -4. Confirm the endpoints: - - `BaseUrl`: `https://api.rudder.build` - - `RealtimeUrl`: `wss://realtime.rudder.build/api/realtime/ws` -5. Bake the demo assets (see below), then open `Assets/LiveOpsExamples/LiveOpsCozyCollector.unity`. -6. Press Play. +4. Confirm `BaseUrl` is `https://api.rudder.build`. +5. Open a feature sample under `Assets/Samples/Rudder SDK/` and press Play. + In this development project the samples are already linked there; do not + import them again from Package Manager. -## Baking The Demo Assets +## Feature Samples -All demo assets are generated from the Unity menu: +Each scene is one Rudder API. The scripts live in +`Packages/rudder.sdk/Samples~` and are the copy-paste examples for the SDK. -```text -Tools > LiveOps > Bake Cozy Collector (All) -``` +| Scene | SDK calls | +| --- | --- | +| `Authentication/Authentication.unity` | `Rudder.Initialize`, `Auth.LoginWithDeviceAsync`, `Player.GetProfileAsync`, `Auth.Logout` | +| `RemoteConfig/RemoteConfig.unity` | `RemoteConfig.LoadAsync`, `Get(key, fallback)` | +| `Storage/Storage.unity` | `Storage.GetAsync`, `SaveAsync`, `DeleteAsync` | +| `StoreInventory/StoreInventory.unity` | `Stores.ListAsync`, `PurchaseAsync`, `Inventory.GetAsync` | +| `Leaderboards/Leaderboards.unity` | `Leaderboards.FindBySlug`, `SubmitAsync`, `ListAsync` | +| `Scenarios/Scenarios.unity` | `Scenario.TriggerAsync`, `Effects.OnNotification`, `Effects.OnStoreOffer`, … | -The bake scripts (`Assets/LiveOpsExamples/Editor/CozyCollectorBake.*.cs`) create or overwrite: - -- `Assets/LiveOpsExamples/CozyCollectorSpriteLibrary.asset` — sprite references imported from `Sprites/`. -- `Assets/LiveOpsExamples/Prefabs/` — snack, score popup, collect burst, and UI row prefabs. -- `Assets/LiveOpsExamples/LiveOpsCozyCollector.unity` — camera, world, canvas, panels; assigns `Assets/LiveOpsLocal.asset` to `Rudder`; adds the scene to Build Settings. - -The bake is idempotent: re-running it rewrites the same asset paths, so run it again after changing runtime scripts or sprites. - -## Stage Data Checklist - -The demo handles missing backend data with visible setup messages, but the strongest run needs these staging entities: +The later scenes are more interesting with this staging data: | Feature | Expected setup | | --- | --- | -| Remote Config | Optional keys: `demo_round_seconds`, `demo_player_speed`, `demo_spawn_interval_ms`, `demo_collectible_score`. Defaults are used when keys are missing. | +| Remote Config | Optional keys: `demo_round_seconds`, `demo_player_speed`. Defaults are used when keys are missing. | | Stores | A store with slug `cozy-camp-shop` with at least one offer. | | Leaderboards | A leaderboard with slug `cozy-collector-score`. | -| Scenarios | Events `player_login` and/or `demo_round_finished`; offer `moonberry-boost` for the scenario offer flow. | -| Storage | No setup required; the demo writes type `cozy_fantasy_save`, id `main`. | +| Scenarios | Events `player_login` and/or `demo_round_finished`. | +| Storage | No setup required; the sample writes type `sample_save`, id `main`. | ## Project Structure ```text -Assets/LiveOpsExamples/ - LiveOpsCozyCollector.unity (baked) - CozyCollectorSpriteLibrary.asset (baked) - Sprites/ (imported from liveops-phaser-demo) - Prefabs/ (baked) - Scripts/ (runtime logic only) - CozyCollectorController.cs - GameWorld.cs, SnackSpawner.cs, Snack.cs, ScorePopupView.cs - CozyCollectorConsts.cs, CozyCollectorSpriteLibrary.cs - UI/ (panels and row views) - Sandbox/ (legacy sandbox controllers) - Editor/ - CozyCollectorBake.*.cs (asset/prefab/scene/UI bake) +Assets/Samples/Rudder SDK/ (symlink to the package Samples~) +Packages/rudder.sdk/ + Runtime/ (Rudder.Unity + Rudder.Core.dll) + Samples~/ (feature sample scenes) Assets/LiveOpsLocal.asset (git-ignored; copy from LiveOpsLocal.asset.example) -Packages/rudder.sdk/ (Rudder SDK UPM package: Runtime/Sources + Runtime/Plugins/Rudder.Core.dll) ``` -Runtime scripts never construct UI or scene objects; everything visual comes from the baked scene and prefabs, with references wired via `[SerializeField]`. - ## Updating Core DLLs Build the C# SDK from the sibling repository and copy the assembly into the diff --git a/skills/rudder-unity-sdk/SKILL.md b/skills/rudder-unity-sdk/SKILL.md index 14fa291..4295bec 100644 --- a/skills/rudder-unity-sdk/SKILL.md +++ b/skills/rudder-unity-sdk/SKILL.md @@ -1,6 +1,6 @@ --- name: rudder-unity-sdk -description: Use when working with the Rudder Unity SDK rudder.sdk in a Unity project — install, bootstrap, auth, remote config, storage, stores, inventory, leaderboards, quests, battle pass, scenarios, realtime. +description: Use when working with the Rudder Unity SDK rudder.sdk in a Unity project — install, bootstrap, auth, remote config, storage, stores, inventory, leaderboards, quests, battle pass, scenarios. --- # Rudder Unity SDK (`rudder.sdk`) @@ -13,7 +13,7 @@ adapters in `RudderSdk.Unity`. Public API surface: `RudderState`, platform adapters. - `RudderSdk.Core` — `RudderClient` and feature services (`Auth`, `Player`, `RemoteConfig`, `Storage`, `ProjectStorage`, `Stores`, `Inventory`, - `Leaderboards`, `Quests`, `BattlePass`, `Scenario`, `Realtime`). + `Leaderboards`, `Quests`, `BattlePass`, `Scenario`, `Effects`). - `RudderSdk.Core.Models.*` — DTOs (`PlayerProfile`, `Wallet`, `Offer`, `Store`, `StorageItem`, `RankEntry`, `Quest`, `Reward`, ...). @@ -47,7 +47,7 @@ the package does not bundle Newtonsoft.Json): 1. Create the configuration asset: `Assets > Create > Rudder > Configuration`. 2. Set `ProjectKey` (required). Defaults: `BaseUrl` `https://api.rudder.build`, - `RealtimeUrl` `wss://api.rudder.build/api/realtime/ws`, `TimeoutSeconds` 10. + `TimeoutSeconds` 10. 3. Add the `Rudder` component to a startup scene object and assign the asset. `Rudder` is `[DefaultExecutionOrder(-1500), DisallowMultipleComponent]`, uses `DontDestroyOnLoad`, and pumps `client.Update(Time.deltaTime)` every @@ -58,7 +58,7 @@ the package does not bundle Newtonsoft.Json): using RudderSdk.Unity; var client = Rudder.Initialize(); // synchronous, idempotent -await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player"); +await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player"); ``` `Rudder.Initialize()` throws if the component or configuration is missing. @@ -79,9 +79,7 @@ you with `PlayerPrefsUnityKeyValueStore` + `UnityDebugLoggerSink`: | `ITokenStore` | `UnityTokenStoreAdapter` | tokens in PlayerPrefs | | `IDeviceIdProvider` | `UnityDeviceIdProvider` | GUID in PlayerPrefs (`rudder_device_id`) — not `SystemInfo.deviceUniqueIdentifier` | | `IClock` | `UnityClock` | `DateTimeOffset.UtcNow` | -| `IPlanStateStore` | `UnityPlanStateStore` | scenario runs persist in PlayerPrefs | | `IRudderLogger` | `UnityLoggerAdapter` → `IUnityLoggerSink` | default `UnityDebugLoggerSink` → `Debug.Log*` | -| `IRealtimeTransportFactory` | `UnityRealtimeTransportFactory` | WebGL: `JsWebSocketAdapter` (jslib); else `NativeWebSocketAdapter` | ## Capabilities @@ -90,15 +88,14 @@ then read the reference file for exact signatures. | Task | API | Reference | | --- | --- | --- | -| Init, sign in, profile, wallets, errors | `Rudder.Initialize`, `client.AuthorizeWithDeviceAsync`, `Auth.*`, `Player.GetProfileAsync` | [reference/bootstrap.md](reference/bootstrap.md), [reference/authentication.md](reference/authentication.md) | +| Init, sign in, profile, wallets, errors | `Rudder.Initialize`, `client.Auth.LoginWithDeviceAsync`, `Auth.*`, `Player.GetProfileAsync` | [reference/bootstrap.md](reference/bootstrap.md), [reference/authentication.md](reference/authentication.md) | | Remote tunables | `RemoteConfig.LoadAsync`, `Get(key, fallback)`, `GetAsync` | [reference/remote-config.md](reference/remote-config.md) | | Player cloud save / project storage | `Storage.GetAsync`/`SaveAsync`/`DeleteAsync`, `ProjectStorage.*` | [reference/storage.md](reference/storage.md) | | Shop + inventory | `Stores.ListAsync`/`GetAsync`/`PurchaseAsync`, `Inventory.GetAsync` | [reference/stores-inventory.md](reference/stores-inventory.md) | | High scores | `Leaderboards.FindBySlug(slug)` → `SubmitAsync`/`ListAsync` | [reference/leaderboards.md](reference/leaderboards.md) | -| LiveOps scenarios | `Scenario.TriggerAsync`, `OnNotification`/`OnStoreOffer`/... | [reference/scenarios.md](reference/scenarios.md) | +| LiveOps scenarios | `Scenario.TriggerAsync`, `Effects.OnNotification`/`OnStoreOffer`/... | [reference/scenarios.md](reference/scenarios.md) | | Global quests | `Quests.ListAsync`/`ClaimAsync`/`ReportProgressAsync` | [reference/quests.md](reference/quests.md) | -| Battle pass | `BattlePass.*` with scenario/node ids, `BattlePassSession` | [reference/battlepass.md](reference/battlepass.md) | -| Realtime websocket | `Realtime.ConnectAsync`/`DisconnectAsync` (relay not deployed) | [reference/realtime.md](reference/realtime.md) | +| Battle pass | `BattlePass.*` with scenario/node ids, `BattlePassEffect` | [reference/battlepass.md](reference/battlepass.md) | ## Hard rules diff --git a/skills/rudder-unity-sdk/reference/authentication.md b/skills/rudder-unity-sdk/reference/authentication.md index e3fc0ee..608706d 100644 --- a/skills/rudder-unity-sdk/reference/authentication.md +++ b/skills/rudder-unity-sdk/reference/authentication.md @@ -7,16 +7,16 @@ and `Samples~/Authentication/AuthenticationSample.cs`. ```csharp var client = Rudder.Initialize(); -await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player"); +await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player"); ``` -`RudderClient.AuthorizeWithDeviceAsync(string region, string language, +`AuthService.LoginWithDeviceAsync(string region, string language, string nickname = null, CancellationToken cancellationToken = default)` -returns `LoginViaDeviceResponse` (`AccessToken`, `RefreshToken`). It performs -device login and then restores persisted scenario runs. Tokens are saved to -PlayerPrefs by the Unity adapter. +returns `LoginViaDeviceResponse` (`AccessToken`, `RefreshToken`). Tokens are +saved to PlayerPrefs by the Unity adapter. Pending scenario effects are +fetched after sign-in by the effects heartbeat. -Lower-level alternative on `client.Auth` (login only, no scenario restore): +Auth service: - `Task LoginWithDeviceAsync(string region, string language, string nickname = null, CancellationToken = default)` - `Task RefreshAsync()` — explicit token refresh. diff --git a/skills/rudder-unity-sdk/reference/battlepass.md b/skills/rudder-unity-sdk/reference/battlepass.md index 1b7593a..516ee3f 100644 --- a/skills/rudder-unity-sdk/reference/battlepass.md +++ b/skills/rudder-unity-sdk/reference/battlepass.md @@ -3,31 +3,31 @@ `client.BattlePass` (`BattlePassService`), verified against the bundled `Rudder.Core.dll`. There is no sample scene for this feature. -Progress is tied to a scenario battle-pass node. Prefer `BattlePassSession` -delivered by `Scenario.OnBattlePass` — it binds `scenarioId`/`nodeId`/ +Progress is tied to a scenario battle-pass node. Prefer `BattlePassEffect` +delivered by `Effects.OnBattlePass` — it binds `scenarioId`/`nodeId`/ `runId` for you. Direct `BattlePassService` calls need those ids manually. -## BattlePassSession (preferred) +## BattlePassEffect (preferred) ```csharp -client.Scenario.OnBattlePass += async session => +client.Effects.OnBattlePass += async effect => { - var progress = await session.GetProgressAsync(); - await session.AddXpAsync("kills", 100); - var claim = await session.ClaimRewardAsync(level, BattlePassService.TrackFree); + var progress = await effect.GetProgressAsync(); + await effect.AddXpAsync("kills", 100); + var claim = await effect.ClaimRewardAsync(level, BattlePassService.TrackFree); }; ``` - `Task GetProgressAsync(CancellationToken = default)` - `Task AddXpAsync(string source, long amount, CancellationToken = default)` - `Task ClaimRewardAsync(int level, string track, CancellationToken = default)` -- `Task PurchasePremiumAsync(CancellationToken = default)` -- `LevelUp()` / `LevelUpAsync()`, `MaxLevel()` / `MaxLevelAsync()`, - `Complete()` / `CompleteAsync()` — resolve the scenario node. -- `Get(key, fallback)`, `Id`, `Context` like other sessions. +- `Task PurchasePremiumAsync(CancellationToken = default)` — posts `onPremiumPurchase` on success +- `LevelUp()` / `LevelUpAsync()` — posts `onLevelUp` +- `End()` / `EndAsync()` — posts `onComplete` +- `Get(key, fallback)`, `RunId`, `ScenarioId`, `NodeId`, `Data` like other effects -`BattlePassLevelSession` (from `OnBattlePassLevel`) has `Claim()` / -`ClaimAsync()` and a `Level` property. +`BattlePassLevelEffect` (from `OnBattlePassLevel`) has `Claim()` / +`ClaimAsync()` (posts `onComplete`) and a `Level` property. ## BattlePassService (direct) @@ -46,11 +46,10 @@ Request models carry `ScenarioId`, `NodeId`, and (except progress) `PremiumOwned` (`bool?`), `ClaimedTiers` (`List`: `Level`, `Track`). - `AddBattlePassXpResponse` — `Xp` (`long?`), `Level` (`int?`), - `LeveledUp` (`bool?`), `MaxLevel` (`bool?`), `Plan` (`ExecutionPlan`). + `LeveledUp` (`bool?`), `MaxLevel` (`bool?`). - `ClaimBattlePassRewardResponse` — `Success` (`bool?`), `AlreadyClaimed` (`bool?`), `Granted` (`List`), `Error`. -- `PurchaseBattlePassPremiumResponse` — `Success` (`bool?`), `Error`, - `Plan` (`ExecutionPlan`). +- `PurchaseBattlePassPremiumResponse` — `Success` (`bool?`), `Error`. Business failures come back in the response (`Success != true`, `Error`) with codes from `RudderErrorCodes` (`LevelNotReached`, diff --git a/skills/rudder-unity-sdk/reference/bootstrap.md b/skills/rudder-unity-sdk/reference/bootstrap.md index da62161..3ad483d 100644 --- a/skills/rudder-unity-sdk/reference/bootstrap.md +++ b/skills/rudder-unity-sdk/reference/bootstrap.md @@ -12,7 +12,6 @@ Create via `Assets > Create > Rudder > Configuration`. | --- | --- | --- | --- | | `ProjectKey` | `string` | — | Required. Issued in the Rudder admin panel. Empty key logs a warning in `OnValidate`. | | `BaseUrl` | `string` | `https://api.rudder.build` | API base URL. | -| `RealtimeUrl` | `string` | `wss://api.rudder.build/api/realtime/ws` | Realtime websocket URL. The relay is not deployed yet; leave as-is. | | `TimeoutSeconds` | `int` (`[Min(1)]`) | `10` | HTTP request timeout. | Keep the asset with the real key out of git (this repo uses the git-ignored @@ -23,8 +22,8 @@ Keep the asset with the real key out of git (this repo uses the git-ignored `[DefaultExecutionOrder(-1500), DisallowMultipleComponent]`. Put it on a startup scene object, assign the configuration asset. `Awake` keeps the first instance (`DontDestroyOnLoad`) and destroys duplicates. `Update` pumps -`_client.Update(Time.deltaTime)` (scenarios and realtime need it — do not -call `client.Update` yourself). On quit/destroy it disconnects realtime. +`_client.Update(Time.deltaTime)` (effects heartbeat and wait-deadline checks +need it — do not call `client.Update` yourself). Static API: @@ -56,18 +55,21 @@ public class Bootstrap : MonoBehaviour private async void Start() { var client = Rudder.Initialize(); - await client.AuthorizeWithDeviceAsync("global", "en", nickname: "Player"); + await client.Auth.LoginWithDeviceAsync("global", "en", nickname: "Player"); } } ``` -`AuthorizeWithDeviceAsync` logs in with the SDK device id (a GUID persisted -in PlayerPrefs under `rudder_device_id`) and restores persisted scenario -runs. It is the first awaited call; afterwards use `client.*` / -`Rudder.Client.*`. Tokens persist in PlayerPrefs, so a second launch can call -it again (it refreshes the session). See +`LoginWithDeviceAsync` logs in with the SDK device id (a GUID persisted +in PlayerPrefs under `rudder_device_id`). It is the first awaited call; +afterwards use `client.*` / `Rudder.Client.*`. Tokens persist in PlayerPrefs, +so a second launch can call it again (it refreshes the session). See [authentication.md](authentication.md) for the Auth service. +Pending scenario effects are fetched after sign-in by the effects heartbeat +(`client.Update`, pumped by `Rudder`). Subscribe to `client.Effects.On*` +before `Scenario.TriggerAsync`. + ## Errors All API errors are `RudderApiException` subclasses from `RudderSdk.Core`: @@ -85,5 +87,5 @@ token refresh — game code does not implement that retry. Catch and surface Removed APIs (do not use): `Rudder.Initialize(configuration)`, `Rudder.Auth`-style static service shortcuts, `client.Scenarios`, -`BuyAsync`, `LoginViaDeviceAsync`, `Scenario.RestoreAsync` (folded into -`AuthorizeWithDeviceAsync`). +`BuyAsync`, `LoginViaDeviceAsync`, `AuthorizeWithDeviceAsync`, +`Scenario.RestoreAsync`. diff --git a/skills/rudder-unity-sdk/reference/quests.md b/skills/rudder-unity-sdk/reference/quests.md index cb58ad1..0e4c90f 100644 --- a/skills/rudder-unity-sdk/reference/quests.md +++ b/skills/rudder-unity-sdk/reference/quests.md @@ -1,7 +1,7 @@ # Quests (global) `client.Quests` (`QuestsService`) — the player's global quest list, distinct -from scenario `OnQuest` nodes (`QuestSession`, see +from scenario `OnQuest` nodes (`QuestEffect`, see [scenarios.md](scenarios.md)). Verified against the bundled `Rudder.Core.dll`. There is no sample scene for this feature. diff --git a/skills/rudder-unity-sdk/reference/realtime.md b/skills/rudder-unity-sdk/reference/realtime.md deleted file mode 100644 index b8f966d..0000000 --- a/skills/rudder-unity-sdk/reference/realtime.md +++ /dev/null @@ -1,36 +0,0 @@ -# Realtime - -`client.Realtime` (`RealtimeService`), verified against the bundled -`Rudder.Core.dll` and -`Packages/rudder.sdk/Runtime/Sources/Realtime/`. - -**The realtime relay is not deployed yet.** `ConnectAsync` is available, but -do not build product features on it unless the user explicitly asks. - -```csharp -var session = await client.Realtime.ConnectAsync(); -session.MessageReceived += data => { /* ArraySegment payload */ }; -await session.SendAsync(payload); -await client.Realtime.DisconnectAsync(); -``` - -API: - -- `Task ConnectAsync(TimeSpan? timeout = null, CancellationToken = default)` — connects to `RudderConfiguration.RealtimeUrl` (default `wss://api.rudder.build/api/realtime/ws`). -- `Task ConnectAsync(Uri uri, TimeSpan? timeout = null, CancellationToken = default)` — explicit URI. -- `Task DisconnectAsync(CancellationToken = default)` — also called by the - `Rudder` component on quit/destroy. -- `RealtimeSession Session { get; }`, `bool IsConnected { get; }`. -- `void Update(float deltaTime)` — pumped by the `Rudder` component. - -`RealtimeSession`: - -- `Task SendAsync(byte[] payload, CancellationToken = default)` -- `Task DisconnectAsync(CancellationToken = default)` -- `bool IsConnected { get; }` -- `event Action Closed`, `event Action Error`, - `event Action> MessageReceived`. - -Transport selection is automatic (`UnityRealtimeTransportFactory`): -WebGL builds use a JavaScript websocket via `RudderWebSocket.jslib` -(`JsWebSocketAdapter`); all other platforms use `NativeWebSocketAdapter`. diff --git a/skills/rudder-unity-sdk/reference/remote-config.md b/skills/rudder-unity-sdk/reference/remote-config.md index 49195a7..17487b2 100644 --- a/skills/rudder-unity-sdk/reference/remote-config.md +++ b/skills/rudder-unity-sdk/reference/remote-config.md @@ -21,6 +21,5 @@ API: `Description`, `Environment`, `ProjectId`, `Active` (`bool?`), `CreatedAt`, `UpdatedAt`. -Scenario `OnConfigChanged` sessions mean the cache was already patched — -re-read with `Get`, do not reload. A missing key returns the fallback; the -key must exist in the dashboard to have a server value. +A missing key returns the fallback; the key must exist in the dashboard to +have a server value. diff --git a/skills/rudder-unity-sdk/reference/scenarios.md b/skills/rudder-unity-sdk/reference/scenarios.md index 5ed4785..28fa417 100644 --- a/skills/rudder-unity-sdk/reference/scenarios.md +++ b/skills/rudder-unity-sdk/reference/scenarios.md @@ -1,52 +1,47 @@ # Scenarios -`client.Scenario` (`ScenarioService`) — the LiveOps plan runtime. Verified -against the bundled `Rudder.Core.dll` and +Scenario graphs execute on the server. `client.Scenario` (`ScenarioService`) +only triggers an event; pending nodes surface through `client.Effects` +(`EffectsService`). Verified against the bundled `Rudder.Core.dll` and `Samples~/Scenarios/ScenariosSample.cs`. ## Flow -Subscribe to `On*` events **before** triggering, then trigger an event name +Subscribe to `Effects.On*` **before** triggering, then trigger an event name configured in the dashboard (examples: `player_login`, `demo_round_finished`). Unsubscribe on destroy. ```csharp -client.Scenario.OnNotification += session => { /* show UI, then session.Complete() */ }; -var runs = await client.Scenario.TriggerAsync("player_login"); +client.Effects.OnNotification += effect => { /* show UI, then effect.Done() */ }; +await client.Scenario.TriggerAsync("player_login"); ``` API: -- `Task> TriggerAsync(string eventName, CancellationToken = default)` — returns the runs it started. -- `Task RestoreAsync(CancellationToken = default)` — restore persisted runs. - Already called by `client.AuthorizeWithDeviceAsync`; do not call both. -- `void Update(float deltaTime)` — pumped by the `Rudder` component. -- `IReadOnlyList ActiveRuns { get; }` — `PlanRun` exposes `RunId`, - `PlanId`, `ScenarioId`, `UserId`, `ActiveNodeIds`, `Plan`. +- `Task ScenarioService.TriggerAsync(string eventName, CancellationToken = default)` — posts the event and ingests any effects the response contains. Does not return runs. +- `void RudderClient.Update(float deltaTime)` — pumped by the `Rudder` component. Drives the 30s pending-effects heartbeat and wait-deadline checks. Do not call it from game code. -Run state persists in PlayerPrefs (`UnityPlanStateStore`), so sessions -survive restarts. +After `Auth.LoginWithDeviceAsync`, the heartbeat fetches pending effects for +the signed-in player. Effects that arrive later (or after a wait deadline) +are dispatched the same way. -## Events and sessions +## Events and effects -All session types expose `Id`, `ScenarioNodeContext Context`, and -`T Get(string key, T defaultValue = default)` for node data. -`ScenarioNodeContext` exposes `Run`, `Node`, `RunId`, `PlanId`, `ScenarioId`, -`NodeId`, `Type`, `Data` (`JObject`), `AsObjectDictionary()`. +All effect types expose `RunId`, `ScenarioId`, `NodeId`, `Data` (`JObject`), +and `T Get(string key, T defaultValue = default)` for node data. -| Event | Session | Game must | +| Event | Effect | Game must | | --- | --- | --- | -| `OnNotification` | `NotificationSession` | Show UI, then `Complete()` / `CompleteAsync()`. | -| `OnStoreOffer` | `StoreOfferSession` | Show offer (`session.Data`, `session.Get("storeSlug", ...)`); on buy call `Stores.PurchaseAsync` then `Purchase()` / `PurchaseAsync()`, else `Decline()` / `DeclineAsync()`. `IsResolved` marks resolution. | -| `OnConfigChanged` | `ConfigChangedSession` | Re-read `RemoteConfig.Get` — patches are already applied to the cache. | -| `OnWait` | `WaitSession` | Deadline only (`DeadlineUtc`); the runtime completes it. | -| `OnQuest` | `QuestSession` | Scenario quest node: `AddProgress(counterKey, amount)` / `AddProgressAsync`, `Complete()` / `CompleteAsync()`, `Fail()` / `FailAsync()`. Not `client.Quests`. | -| `OnLeaderboard` | `LeaderboardSession` | `End()` / `EndAsync()`, `RewardClaimed()` / `RewardClaimedAsync()`. | -| `OnBattlePass` | `BattlePassSession` | Drive the session — see [battlepass.md](battlepass.md). | -| `OnBattlePassLevel` | `BattlePassLevelSession` | `Claim()` / `ClaimAsync()` for the level reward; `Level` prop. | -| `OnScenarioCompleted` | `PlanRun` | Log / surface. | -| `OnScenarioFailed` | `ScenarioFailedEvent` | `Run`, `NodeId`, `Exception`. Log / surface. | +| `OnNotification` | `NotificationEffect` | Show UI (`Title`, `Message`), then `Done()` / `DoneAsync()`. | +| `OnStoreOffer` | `StoreOfferEffect` | Show offer (`StoreSlug`, `Get("storeSlug", ...)`); on buy call `Stores.PurchaseAsync` then `Purchase()` / `PurchaseAsync()`, else `Decline()` / `DeclineAsync()`. `IsResolved` marks resolution. | +| `OnWait` | `WaitEffect` | Deadline only (`DeadlineUtc`); the server advances the run. | +| `OnQuest` | `QuestEffect` | Scenario quest node: `ReportProgress(objectiveId, amount)` / `ReportProgressAsync`. The server auto-completes the node once every objective is satisfied. Not `client.Quests`. | +| `OnLeaderboard` | `LeaderboardEffect` | `End()` / `EndAsync()`, `Claim()` / `ClaimAsync()`. `IsResolved` marks resolution. | +| `OnBattlePass` | `BattlePassEffect` | Drive the effect — see [battlepass.md](battlepass.md). | +| `OnBattlePassLevel` | `BattlePassLevelEffect` | `Claim()` / `ClaimAsync()` for the level reward; `Level` prop. | +| `OnScenarioCompleted` | `ScenarioCompletedEffect` | `RunId`, `ScenarioId`. Log / surface. | +| `OnScenarioFailed` | `ScenarioFailedEffect` | `RunId`, `ScenarioId`, `NodeId`, `Exception`. Log / surface. | Async and sync variants are equivalent; the sync variants fire-and-forget the same server callback. The event name must match a scenario trigger -configured in the dashboard — an unknown event simply starts zero runs. +configured in the dashboard — an unknown event simply produces no effects.