diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml index 9299695f..66981748 100644 --- a/.github/workflows/build-check.yml +++ b/.github/workflows/build-check.yml @@ -1,6 +1,9 @@ name: Build check -on: push +on: + push: + branches: [develop] + pull_request: jobs: build-check: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7b6c9f4..2ee5b9a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,9 @@ -name: Unity tests +name: Unit tests -on: push +on: + push: + branches: [develop] + pull_request: jobs: test: diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml deleted file mode 100644 index 5193aa14..00000000 --- a/.github/workflows/opencode.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: opencode - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -jobs: - opencode: - runs-on: ubuntu-latest - - if: | - contains(github.event.comment.body, ' /oc') || - startsWith(github.event.comment.body, '/oc') || - contains(github.event.comment.body, ' /opencode') || - startsWith(github.event.comment.body, '/opencode') - - timeout-minutes: 10 - - permissions: - id-token: write - contents: read - pull-requests: read - issues: read - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - persist-credentials: false - - - name: Run opencode - uses: anomalyco/opencode/github@latest - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - with: - model: opencode-go/deepseek-v4-flash diff --git a/AGENTS.md b/AGENTS.md index 385b7252..b61e975e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,18 +45,22 @@ The SDK supports offline operation via `TaloSettings.offlineMode`. When offline, Events are batched and flushed on application quit/pause/focus loss. On WebGL, events flush every `webGLEventFlushRate` seconds (default 30s) due to platform limitations. ### Debouncing -Player updates and save updates are debounced to prevent excessive API calls during rapid property changes. APIs that need debouncing inherit from `DebouncedAPI` (a generic base class) and define a `DebouncedOperation` enum for type-safe operation keys. The base class uses a dictionary to track multiple debounced operations independently. +Player updates and save updates are debounced to prevent excessive API calls during rapid property changes. APIs that need debouncing inherit from `DebouncedAPI` and define a `DebouncedOperation` enum for type-safe operation keys. The base class uses a dictionary to track multiple debounced operations independently. -The debounce is **leading and trailing**: the first call fires immediately (leading), and if further calls arrive during the debounce window they are coalesced into a single trailing call executed after the window closes. The window is defined by `debounceTimerSeconds` (default: 1s) and resets on each subsequent call. +The debounce is **trailing**: calls are coalesced into a single API call executed after the debounce window closes. The window is defined by `debounceTimerSeconds` (default: 1s) and resets on each subsequent call. All callers in the same window share the same `Task` result. To add debouncing to an API: 1. Define a public `enum DebouncedOperation` with your debounced operations -2. Inherit from `DebouncedAPI` +2. Inherit from `DebouncedAPI` 3. Call `Debounce(DebouncedOperation.YourOperation)` to queue an operation 4. Implement `ExecuteDebouncedOperation(DebouncedOperation operation)` with a switch statement 5. The base class's `ProcessPendingUpdates()` is called by `TaloManager.Update()` every frame -Example: `PlayersAPI` defines `enum DebouncedOperation { Update }` and inherits from `DebouncedAPI`. When `Player.SetProp()` is called, it calls `Debounce(DebouncedOperation.Update)`. The first call fires immediately; subsequent calls within the debounce window result in a single trailing API call at the end of the window. +Example: `PlayersAPI` defines `enum DebouncedOperation { Update }` and inherits from `DebouncedAPI`. When `Player.SetProp()` is called, it calls `Debounce(DebouncedOperation.Update)`. The first call opens a window; subsequent calls within the window extend it. A single trailing API call fires at the end of the window. + +#### Debounced update completion signals + +`Player.SetProp(...)` returns `Task`, `Talo.Saves.UpdateCurrentSave()` returns `Task`. All callers in the same debounce window share the same settle result. `FlushUpdates()` returns `FlushResult` (`NothingPending`, `Success`, `Failure`). `OnPlayerUpdated(bool)` / `OnSaveUpdated(bool, GameSave)` fire after each settle. `TaloManager.OnApplicationQuit()` flushes pending updates automatically. ## Key Configuration diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs index 7470e078..88ad0da9 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs @@ -9,7 +9,7 @@ namespace TaloGameServices public class BaseAPI { // automatically updated with a pre-commit hook - private const string ClientVersion = "0.60.2"; + private const string ClientVersion = "1.0.0"; protected string baseUrl; @@ -145,7 +145,14 @@ protected async Task Call( return await Call(uri, method, content, headers, continuity); } - throw new PlayerAuthException(errorCode, new Exception(message)); + if (uri.AbsolutePath.Contains("/v1/players/auth/")) + { + throw new PlayerAuthException(errorCode, new Exception(message)); + } + else + { + throw new RequestException(www.responseCode, new Exception(message), www.downloadHandler.text); + } } } } diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/ChannelsAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/ChannelsAPI.cs index a6391583..8aa81692 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/ChannelsAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/ChannelsAPI.cs @@ -67,9 +67,19 @@ public class CreateChannelOptions { public string name; public (string, string)[] props = Array.Empty<(string, string)>(); - public bool autoCleanup = false; - public bool isPrivate = false; - public bool temporaryMembership = false; + public bool autoCleanup; + public bool isPrivate; + public bool temporaryMembership; + } + + public class UpdateChannelOptions + { + public string name; + public int newOwnerAliasId = -1; + public (string, string)[] props; + public bool? autoCleanup = null; + public bool? isPrivate = null; + public bool? temporaryMembership = null; } public enum ChannelLeavingReason @@ -86,8 +96,7 @@ public class ChannelsAPI : BaseAPI public event Action OnOwnershipTransferred; public event Action OnChannelDeleted; public event Action OnChannelUpdated; - public event Action OnChannelPropsRejected; - public event Action OnChannelStoragePropsFailedToSet; + public event Action OnChannelStoragePropsFailedToSet; public event Action OnChannelStoragePropsUpdated; private readonly ChannelStorageManager _storageManager = new (); @@ -147,12 +156,6 @@ public async Task GetChannels(GetChannelsOptions options return res; } - [Obsolete("Use GetChannels(GetChannelsOptions options) instead.")] - public async Task GetChannels(int page) - { - return await GetChannels(new GetChannelsOptions { page = page }); - } - public async Task GetSubscribedChannels(GetSubscribedChannelsOptions options = null) { Talo.IdentityCheck(); @@ -166,7 +169,7 @@ public async Task GetSubscribedChannels(GetSubscribedChannelsOptions return res.channels; } - private async Task SendCreateChannelRequest(CreateChannelOptions options) + private async Task SendCreateChannelRequest(CreateChannelOptions options) { Talo.IdentityCheck(); @@ -187,50 +190,24 @@ private async Task SendCreateChannelRequest(CreateChannelOptions option var json = await Call(uri, "POST", content); var res = JsonUtility.FromJson(json); - return res.channel; + return new ChannelUpsertResult(true, res.channel); } catch (RequestException ex) { if (ex.IsBadRequest()) { - RejectedProp.TryEmit(ex.responseBody, OnChannelPropsRejected); + return new ChannelUpsertResult(false, null, RejectedProp.FromJson(ex.responseBody)); } throw; } } - public async Task Create(CreateChannelOptions options) + public async Task Create(CreateChannelOptions options) { options ??= new CreateChannelOptions(); return await SendCreateChannelRequest(options); } - [Obsolete("Use Create(CreateChannelOptions options) instead.")] - public async Task Create(string name, bool autoCleanup = false, params (string, string)[] propTuples) - { - var options = new CreateChannelOptions - { - name = name, - autoCleanup = autoCleanup, - props = propTuples, - isPrivate = false - }; - return await SendCreateChannelRequest(options); - } - - [Obsolete("Use Create(CreateChannelOptions options) instead.")] - public async Task CreatePrivate(string name, bool autoCleanup = false, params (string, string)[] propTuples) - { - var options = new CreateChannelOptions - { - name = name, - autoCleanup = autoCleanup, - props = propTuples, - isPrivate = true - }; - return await SendCreateChannelRequest(options); - } - public async Task Join(int channelId) { Talo.IdentityCheck(); @@ -250,36 +227,37 @@ public async Task Leave(int channelId) await Call(uri, "POST"); } - public async Task Update(int channelId, string name = "", int newOwnerAliasId = -1, params (string, string)[] propTuples) + public async Task Update(int channelId, UpdateChannelOptions options = null) { Talo.IdentityCheck(); - var props = propTuples.Select((propTuple) => new Prop(propTuple)).ToArray(); + options ??= new UpdateChannelOptions(); var uri = new Uri($"{baseUrl}/{channelId}"); - var content = ""; - if (newOwnerAliasId == -1) - { - content = JsonUtility.ToJson(new ChannelsUpdateRequest { name = name, props = props }); - } - else - { - content = JsonUtility.ToJson(new ChannelsUpdateOwnerRequest { name = name, newOwnerAliasId = newOwnerAliasId, props = props }); - } + var props = options.props?.Select((propTuple) => new Prop(propTuple)).ToArray(); + + var content = JsonUtils.BuildObject( + ("name", string.IsNullOrEmpty(options.name) ? null : options.name), + ("ownerAliasId", options.newOwnerAliasId == -1 ? null : options.newOwnerAliasId), + ("props", props), + ("autoCleanup", options.autoCleanup), + ("private", options.isPrivate), + ("temporaryMembership", options.temporaryMembership) + ); try { var json = await Call(uri, "PUT", content); var res = JsonUtility.FromJson(json); - return res.channel; + return new ChannelUpsertResult(true, res.channel); } catch (RequestException ex) { if (ex.IsBadRequest()) { - RejectedProp.TryEmit(ex.responseBody, OnChannelPropsRejected); + return new ChannelUpsertResult(false, null, RejectedProp.FromJson(ex.responseBody)); } throw; } @@ -421,5 +399,19 @@ public async Task ListStorageProps(int channelId, string[] return Array.Empty(); } + + public class ChannelUpsertResult + { + public bool Success { get; } + public Channel Channel { get; } + public RejectedProp[] RejectedProps { get; } + + public ChannelUpsertResult(bool success, Channel channel, RejectedProp[] rejectedProps = null) + { + Success = success; + Channel = channel; + RejectedProps = rejectedProps ?? Array.Empty(); + } + } } } diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs index 2bb5735d..c5f0d976 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/DebouncedAPI.cs @@ -5,7 +5,19 @@ namespace TaloGameServices { - public abstract class DebouncedAPI : BaseAPI where TOperation : Enum + public abstract class DebouncedAPIBase : BaseAPI + { + public enum FlushResult + { + NothingPending, + Success, + Failure + } + + protected DebouncedAPIBase(string service) : base(service) { } + } + + public abstract class DebouncedAPI : DebouncedAPIBase where TOperation : Enum { private class DebouncedOperation { @@ -13,10 +25,14 @@ private class DebouncedOperation public bool windowOpen; public bool hasTrailingCallQueued; public bool isExecuting; + public Task currentTask; + public List> pendingTasks = new(); } private readonly Dictionary operations = new(); + protected event Action OnOperationSettled; + protected DebouncedAPI(string service) : base(service) { } private void OpenWindow(DebouncedOperation op) @@ -25,7 +41,7 @@ private void OpenWindow(DebouncedOperation op) op.windowEndTime = Time.realtimeSinceStartup + Talo.Settings.debounceTimerSeconds; } - protected void Debounce(TOperation operation) + protected Task Debounce(TOperation operation) { if (!operations.ContainsKey(operation)) { @@ -34,27 +50,42 @@ protected void Debounce(TOperation operation) var op = operations[operation]; - if (!op.windowOpen && !op.isExecuting) - { - // leading call: fire immediately and open the debounce window - op.hasTrailingCallQueued = false; - op.isExecuting = true; - OpenWindow(op); + var tcs = new TaskCompletionSource(); + op.pendingTasks.Add(tcs); + op.hasTrailingCallQueued = true; + OpenWindow(op); + return tcs.Task; + } - ExecuteDebouncedOperation(operation).ContinueWith((t) => { - op.isExecuting = false; - if (t.IsFaulted) - { - Debug.LogError(t.Exception); - } - }, TaskScheduler.FromCurrentSynchronizationContext()); + private async Task<(bool success, TUpdateResult result)> RunAndSettle(TOperation operation, DebouncedOperation op, List> pending) + { + op.currentTask = ExecuteDebouncedOperation(operation); + + bool success; + TReturnData returnData; + try + { + returnData = await op.currentTask; + success = true; } - else + catch (Exception) + { + returnData = default; + success = false; + } + finally + { + op.isExecuting = false; + } + + OnOperationSettled?.Invoke(success, returnData); + + var result = BuildResult(success, returnData); + foreach (var tcs in pending) { - // window open or request in-flight: queue a trailing call and extend the window - op.hasTrailingCallQueued = true; - OpenWindow(op); + tcs.SetResult(result); } + return (success, result); } public async Task ProcessPendingUpdates() @@ -67,22 +98,16 @@ public async Task ProcessPendingUpdates() var windowClosed = Time.realtimeSinceStartup >= op.windowEndTime; if (windowClosed) { - if (op.hasTrailingCallQueued) + if (op.hasTrailingCallQueued && !op.isExecuting) { - if (!op.isExecuting) - { - // window closed with a trailing call pending: execute it - keysToProcess.Add(kvp.Key); - } - else - { - // leading call still in-flight: delay trailing until it completes - OpenWindow(op); - } + keysToProcess.Add(kvp.Key); + } + else if (op.isExecuting) + { + OpenWindow(op); } else if (op.windowOpen) { - // window closed with no trailing call: reset for the next leading call op.windowOpen = false; } } @@ -93,18 +118,54 @@ public async Task ProcessPendingUpdates() var op = operations[key]; op.hasTrailingCallQueued = false; op.isExecuting = true; - try + + var pending = new List>(op.pendingTasks); + op.pendingTasks.Clear(); + + await RunAndSettle(key, op, pending); + } + } + + public async Task FlushUpdates() + { + var result = FlushResult.NothingPending; + + var keys = new List(operations.Keys); + foreach (var key in keys) + { + var op = operations[key]; + + if (op.isExecuting) { - await ExecuteDebouncedOperation(key); + await op.currentTask; } - finally + + while (op.hasTrailingCallQueued) { - op.isExecuting = false; - op.windowOpen = false; + op.hasTrailingCallQueued = false; + op.isExecuting = true; + + var pending = new List>(op.pendingTasks); + op.pendingTasks.Clear(); + + var (settleSuccess, _) = await RunAndSettle(key, op, pending); + if (settleSuccess) + { + if (result == FlushResult.NothingPending) result = FlushResult.Success; + } + else + { + result = FlushResult.Failure; + } } + + op.windowOpen = false; } + + return result; } - protected abstract Task ExecuteDebouncedOperation(TOperation operation); + protected abstract Task ExecuteDebouncedOperation(TOperation operation); + protected abstract TUpdateResult BuildResult(bool success, TReturnData returnData); } } diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/FeedbackAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/FeedbackAPI.cs index e1e2b4df..17da423d 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/FeedbackAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/FeedbackAPI.cs @@ -7,8 +7,6 @@ namespace TaloGameServices { public class FeedbackAPI : BaseAPI { - public event Action OnPropsRejected; - public FeedbackAPI() : base("v1/game-feedback") { } public async Task GetCategories() @@ -20,7 +18,7 @@ public async Task GetCategories() return res.feedbackCategories; } - public async Task Send(string categoryInternalName, string comment, params (string, string)[] props) + public async Task Send(string categoryInternalName, string comment, params (string, string)[] props) { Talo.IdentityCheck(); @@ -31,15 +29,28 @@ public async Task Send(string categoryInternalName, string comment, params (stri try { await Call(uri, "POST", content); + return new FeedbackSendResult(true); } catch (RequestException ex) { if (ex.IsBadRequest()) { - RejectedProp.TryEmit(ex.responseBody, OnPropsRejected); + return new FeedbackSendResult(false, RejectedProp.FromJson(ex.responseBody)); } throw; } } + + public class FeedbackSendResult + { + public bool Success { get; } + public RejectedProp[] RejectedProps { get; } + + public FeedbackSendResult(bool success, RejectedProp[] rejectedProps = null) + { + Success = success; + RejectedProps = rejectedProps ?? Array.Empty(); + } + } } } diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/LeaderboardsAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/LeaderboardsAPI.cs index 9420c4c1..9f3b54e8 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/LeaderboardsAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/LeaderboardsAPI.cs @@ -45,8 +45,6 @@ public class LeaderboardsAPI : BaseAPI { private readonly LeaderboardEntriesManager _entriesManager = new(); - public event Action OnPropsRejected; - public LeaderboardsAPI() : base("v1/leaderboards") { } public List GetCachedEntries(string internalName, GetCachedEntriesOptions options = null) @@ -60,14 +58,6 @@ public List GetCachedEntries(string internalName, GetCachedEnt ); } - [Obsolete("Use GetCachedEntries(string internalName, GetCachedEntriesOptions options) with the aliasId or playerId option instead.")] - public List GetCachedEntriesForCurrentPlayer(string internalName) - { - Talo.IdentityCheck(); - - return _entriesManager.GetEntries(internalName).FindAll(e => e.playerAlias.id == Talo.CurrentAlias.id); - } - public async Task GetEntries(string internalName, GetEntriesOptions options = null) { options ??= new GetEntriesOptions(); @@ -85,42 +75,7 @@ public async Task GetEntries(string internalName, Ge return res; } - [Obsolete("Use GetEntries(string internalName, GetEntriesOptions options) with the aliasId or playerId option instead.")] - public async Task GetEntriesForCurrentPlayer(string internalName, GetEntriesOptions options = null) - { - Talo.IdentityCheck(); - - options ??= new GetEntriesOptions(); - options.aliasId = Talo.CurrentAlias.id; - - return await GetEntries(internalName, options); - } - - [Obsolete("Use GetEntries(string internalName, GetEntriesOptions options) instead.")] - public async Task GetEntries(string internalName, int page, int aliasId = -1, bool includeArchived = false) - { - return await GetEntries(internalName, new GetEntriesOptions - { - page = page, - aliasId = aliasId, - includeArchived = includeArchived - }); - } - - [Obsolete("Use GetEntries(string internalName, GetEntriesOptions options) with the aliasId or playerId option instead.")] - public async Task GetEntriesForCurrentPlayer(string internalName, int page, bool includeArchived = false) - { - Talo.IdentityCheck(); - - return await GetEntries(internalName, new GetEntriesOptions - { - page = page, - aliasId = Talo.CurrentAlias.id, - includeArchived = includeArchived - }); - } - - public async Task<(LeaderboardEntry, bool)> AddEntry(string internalName, float score, params (string, string)[] propTuples) + public async Task AddEntry(string internalName, float score, params (string, string)[] propTuples) { Talo.IdentityCheck(); @@ -136,16 +91,32 @@ public async Task GetEntriesForCurrentPlayer(string var res = JsonUtility.FromJson(json); _entriesManager.UpsertEntry(internalName, res.entry, true); - return (res.entry, res.updated); + return new AddEntryResult(true, res.entry, res.updated); } catch (RequestException ex) { if (ex.IsBadRequest()) { - RejectedProp.TryEmit(ex.responseBody, OnPropsRejected); + return new AddEntryResult(false, null, false, RejectedProp.FromJson(ex.responseBody)); } throw; } } + + public class AddEntryResult + { + public bool Success { get; } + public LeaderboardEntry Entry { get; } + public bool Updated { get; } + public RejectedProp[] RejectedProps { get; } + + public AddEntryResult(bool success, LeaderboardEntry entry, bool updated, RejectedProp[] rejectedProps = null) + { + Success = success; + Entry = entry; + Updated = updated; + RejectedProps = rejectedProps ?? Array.Empty(); + } + } } -} \ No newline at end of file +} diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs index 89c50074..fbc812cd 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/PlayersAPI.cs @@ -9,22 +9,23 @@ public class MergeOptions public string postMergeIdentityService = ""; } - public class PlayersAPI : DebouncedAPI + public class PlayersAPI : DebouncedAPI { public enum DebouncedOperation { Update } - public event Action OnIdentified; + public event Action OnIdentified; public event Action OnIdentificationStarted; - public event Action OnIdentificationFailed; + public event Action OnIdentificationFailed; public event Action OnIdentityCleared; - public event Action OnPropsRejected; + public event Action OnPlayerUpdated; public PlayersAPI() : base("v1/players") { Talo.OnConnectionRestored += OnConnectionRestored; + OnOperationSettled += (success, _) => OnPlayerUpdated?.Invoke(success); } private async void OnConnectionRestored() @@ -43,10 +44,10 @@ private async void OnConnectionRestored() public void InvokeIdentifiedEvent() { - OnIdentified?.Invoke(Talo.CurrentPlayer); + OnIdentified?.Invoke(Talo.CurrentAlias); } - private async Task HandleIdentifySuccess(PlayerAlias alias, string socketToken = "") + private async Task HandleIdentifySuccess(PlayerAlias alias, string socketToken = "") { if (!Talo.IsOffline() && Talo.Socket.IsIdentified()) { @@ -61,10 +62,10 @@ private async Task HandleIdentifySuccess(PlayerAlias alias, string socke InvokeIdentifiedEvent(); - return alias.player; + return alias; } - public async Task Identify(string service, string identifier) + public async Task Identify(string service, string identifier) { OnIdentificationStarted?.Invoke(); @@ -75,44 +76,41 @@ public async Task Identify(string service, string identifier) var uri = new Uri($"{baseUrl}/identify?service={service}&identifier={identifier}"); + PlayersIdentifyResponse res; try { var json = await Call(uri, "GET"); - - var res = JsonUtility.FromJson(json); - var alias = res.alias; - alias.WriteOfflineAlias(); - return await HandleIdentifySuccess(alias, res.socketToken); + res = JsonUtility.FromJson(json); } - catch + catch (Exception ex) { await Talo.PlayerAuth.SessionManager.ClearSession(); - OnIdentificationFailed?.Invoke(); + OnIdentificationFailed?.Invoke(IdentifyException.FromException(ex)); throw; } + + res.alias.WriteOfflineAlias(); + return await HandleIdentifySuccess(res.alias, res.socketToken); } - public async Task IdentifySteam(string ticket, string identityClient = "") + public async Task IdentifySteam(string ticket, string identityClient = "") { if (string.IsNullOrEmpty(identityClient)) { - await Identify("steam", ticket); + return await Identify("steam", ticket); } else { - await Identify("steam", $"{identityClient}:{ticket}"); + return await Identify("steam", $"{identityClient}:{ticket}"); } - - return Talo.CurrentPlayer; } - public async Task IdentifyGooglePlayGames(string authCode) + public async Task IdentifyGooglePlayGames(string authCode) { - await Identify("google_play_games", authCode); - return Talo.CurrentPlayer; + return await Identify("google_play_games", authCode); } - public async Task IdentifyGameCenter( + public async Task IdentifyGameCenter( string publicKeyUrl, byte[] signature, byte[] salt, @@ -132,26 +130,39 @@ string playerId var identifier = Uri.EscapeDataString(JsonUtility.ToJson(payload)); - await Identify("game_center", identifier); - return Talo.CurrentPlayer; + return await Identify("game_center", identifier); } - protected override async Task ExecuteDebouncedOperation(DebouncedOperation operation) + protected override async Task ExecuteDebouncedOperation(DebouncedOperation operation) { - switch (operation) + return operation switch { - case DebouncedOperation.Update: - await Update(); - break; + DebouncedOperation.Update => await RunUpdate(), + _ => null, + }; + } + + protected override PlayerUpdateResult BuildResult(bool success, RejectedProp[] updateData) + { + if (!success) + { + return new PlayerUpdateResult(false); } + return new PlayerUpdateResult(true, updateData); } - public void DebounceUpdate() + public Task DebounceUpdate() { - Debounce(DebouncedOperation.Update); + return Debounce(DebouncedOperation.Update); } public async Task Update() + { + await RunUpdate(); + return Talo.CurrentPlayer; + } + + private async Task RunUpdate() { Talo.IdentityCheck(); @@ -163,12 +174,7 @@ public async Task Update() Talo.CurrentPlayer = res.player; Talo.CurrentAlias.WriteOfflineAlias(); - if (res.rejectedProps != null && res.rejectedProps.Length > 0) - { - OnPropsRejected?.Invoke(res.rejectedProps); - } - - return Talo.CurrentPlayer; + return res.rejectedProps ?? Array.Empty(); } public async Task Merge(string playerId1, string playerId2, MergeOptions options = null) @@ -203,7 +209,7 @@ public async Task Find(string playerId) return res.player; } - private async Task IdentifyOffline(string service, string identifier) + private async Task IdentifyOffline(string service, string identifier) { PlayerAlias offlineAlias; try @@ -213,7 +219,7 @@ private async Task IdentifyOffline(string service, string identifier) catch { PlayerAlias.DeleteOfflineAlias(); - OnIdentificationFailed?.Invoke(); + OnIdentificationFailed?.Invoke(new IdentifyException()); throw new Exception("Failed to parse offline player alias"); } @@ -222,7 +228,7 @@ private async Task IdentifyOffline(string service, string identifier) return await HandleIdentifySuccess(offlineAlias); } - OnIdentificationFailed?.Invoke(); + OnIdentificationFailed?.Invoke(new IdentifyException()); throw new Exception("No offline player alias found"); } @@ -262,5 +268,17 @@ public async Task CreateSocketToken() return ""; } } + + public class PlayerUpdateResult + { + public bool Success { get; } + public RejectedProp[] RejectedProps { get; } + + public PlayerUpdateResult(bool success, RejectedProp[] rejectedProps = null) + { + Success = success; + RejectedProps = rejectedProps ?? Array.Empty(); + } + } } } diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs index 203c2101..06f5ef53 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/SavesAPI.cs @@ -6,7 +6,7 @@ namespace TaloGameServices { - public class SavesAPI : DebouncedAPI + public class SavesAPI : DebouncedAPI { public enum DebouncedOperation { @@ -21,6 +21,7 @@ public enum DebouncedOperation public event Action OnSaveChosen; public event Action OnSaveUnloaded; + public event Action OnSaveUpdated; public GameSave[] All { @@ -38,7 +39,11 @@ public GameSave Current } public SavesAPI() : base("v1/game-saves") - { } + { + OnOperationSettled += (success, save) => { + OnSaveUpdated?.Invoke(success, success ? save : null); + }; + } internal void Setup() { @@ -191,7 +196,7 @@ public async Task CreateSave(string saveName, SaveContent content = nu return savesManager.CreateSave(save); } - protected override async Task ExecuteDebouncedOperation(DebouncedOperation operation) + protected override async Task ExecuteDebouncedOperation(DebouncedOperation operation) { switch (operation) { @@ -199,18 +204,24 @@ protected override async Task ExecuteDebouncedOperation(DebouncedOperation opera var currentSave = savesManager.CurrentSave; if (currentSave != null) { - await UpdateSave(currentSave.id); + return await UpdateSave(currentSave.id); } break; } + return null; + } + + protected override SaveUpdateResult BuildResult(bool success, GameSave updateData) + { + return new SaveUpdateResult(success, success ? updateData : null); } - public void DebounceUpdate() + public Task DebounceUpdate() { - Debounce(DebouncedOperation.Update); + return Debounce(DebouncedOperation.Update); } - public async Task UpdateCurrentSave(string newName = "") + public async Task UpdateCurrentSave(string newName = "") { var currentSave = savesManager.CurrentSave; if (currentSave == null) @@ -221,13 +232,16 @@ public async Task UpdateCurrentSave(string newName = "") // if the save is being renamed, sync it immediately if (!string.IsNullOrEmpty(newName)) { - return await UpdateSave(currentSave.id, newName); + var save = await UpdateSave(currentSave.id, newName); + var success = save != null; + var result = new SaveUpdateResult(success, save); + OnSaveUpdated?.Invoke(success, save); + return result; } // else, update the save locally and queue it for syncing currentSave.content = contentManager.Content; - DebounceUpdate(); - return currentSave; + return await DebounceUpdate(); } public async Task UpdateSave(int saveId, string newName = "") @@ -238,7 +252,10 @@ public async Task UpdateSave(int saveId, string newName = "") if (Talo.IsOffline()) { - if (!string.IsNullOrEmpty(newName)) save.name = newName; + if (!string.IsNullOrEmpty(newName)) + { + save.name = newName; + } save.content = saveContent; save.updatedAt = DateTime.UtcNow.ToString("O"); } @@ -254,7 +271,6 @@ public async Task UpdateSave(int saveId, string newName = "") }); var json = await Call(uri, "PATCH", content); - var res = JsonUtility.FromJson(json); save = res.save; } @@ -293,5 +309,17 @@ public async Task DeleteSave(int saveId, bool unloadIfCurrentSave = false) savesManager.DeleteSave(saveId, unloadIfCurrentSave); } + + public class SaveUpdateResult + { + public bool Success { get; } + public GameSave Save { get; } + + public SaveUpdateResult(bool success, GameSave save = null) + { + Success = success; + Save = save; + } + } } } diff --git a/Assets/Talo Game Services/Talo/Runtime/APIs/StatsAPI.cs b/Assets/Talo Game Services/Talo/Runtime/APIs/StatsAPI.cs index 6856f38c..a58ba34a 100644 --- a/Assets/Talo Game Services/Talo/Runtime/APIs/StatsAPI.cs +++ b/Assets/Talo Game Services/Talo/Runtime/APIs/StatsAPI.cs @@ -18,12 +18,6 @@ public async Task GetStats() return res.stats; } - [Obsolete("Use Find(string internalName) instead.")] - public async Task GetStat(string internalName) - { - return await Find(internalName); - } - public async Task Find(string internalName) { var uri = new Uri($"{baseUrl}/{internalName}"); diff --git a/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs b/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs index 73b4d2a8..df79fb15 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Entities/Player.cs @@ -1,101 +1,108 @@ -using UnityEngine; -using System.Linq; -using System; -using System.Collections.Generic; - -namespace TaloGameServices -{ - [Serializable] - public class Player : EntityWithProps - { - public string id; - public PlayerAlias[] aliases; - public GroupStub[] groups; - public PlayerPresence presence; - - public override string ToString() - { - return JsonUtility.ToJson(this); - } - - public void SetProp(string key, string value, bool update = true) - { - base.SetProp(key, value); - - if (update) - { - Talo.Players.DebounceUpdate(); - } - } - - public void DeleteProp(string key, bool update = true) - { - base.DeleteProp(key); - - if (update) - { - Talo.Players.DebounceUpdate(); - } - } - - public void SetPropArray(string key, IEnumerable values, bool update = true) - { - base.SetPropArray(key, values); - - if (update) - { - Talo.Players.DebounceUpdate(); - } - } - - public void DeletePropArray(string key, bool update = true) - { - base.DeletePropArray(key); - - if (update) - { - Talo.Players.DebounceUpdate(); - } - } - - public void InsertIntoPropArray(string key, string value, bool update = true) - { - base.InsertIntoPropArray(key, value); - - if (update) - { - Talo.Players.DebounceUpdate(); - } - } - - public void RemoveFromPropArray(string key, string value, bool update = true) - { - base.RemoveFromPropArray(key, value); - - if (update) - { - Talo.Players.DebounceUpdate(); - } - } - - public bool IsInGroupID(string groupId) - { - return groups.Any((group) => group.id == groupId); - } - - public bool IsInGroupName(string groupName) - { - return groups.Any((group) => group.name == groupName); - } - - public PlayerAlias GetAlias(string service = "") - { - if (string.IsNullOrEmpty(service)) - { - return aliases.Length > 0 ? aliases[0] : null; - } - - return aliases.FirstOrDefault((alias) => alias.service == service); - } - } -} +using UnityEngine; +using System.Linq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace TaloGameServices +{ + [Serializable] + public class Player : EntityWithProps + { + public string id; + public PlayerAlias[] aliases; + public GroupStub[] groups; + public PlayerPresence presence; + + public override string ToString() + { + return JsonUtility.ToJson(this); + } + + public Task SetProp(string key, string value, bool update = true) + { + base.SetProp(key, value); + + if (update) + { + return Talo.Players.DebounceUpdate(); + } + return Task.FromResult(new PlayersAPI.PlayerUpdateResult(true)); + } + + public Task DeleteProp(string key, bool update = true) + { + base.DeleteProp(key); + + if (update) + { + return Talo.Players.DebounceUpdate(); + } + return Task.FromResult(new PlayersAPI.PlayerUpdateResult(true)); + } + + public Task SetPropArray(string key, IEnumerable values, bool update = true) + { + base.SetPropArray(key, values); + + if (update) + { + return Talo.Players.DebounceUpdate(); + } + return Task.FromResult(new PlayersAPI.PlayerUpdateResult(true)); + } + + public Task DeletePropArray(string key, bool update = true) + { + base.DeletePropArray(key); + + if (update) + { + return Talo.Players.DebounceUpdate(); + } + return Task.FromResult(new PlayersAPI.PlayerUpdateResult(true)); + } + + public Task InsertIntoPropArray(string key, string value, bool update = true) + { + base.InsertIntoPropArray(key, value); + + if (update) + { + return Talo.Players.DebounceUpdate(); + } + return Task.FromResult(new PlayersAPI.PlayerUpdateResult(true)); + } + + public Task RemoveFromPropArray(string key, string value, bool update = true) + { + base.RemoveFromPropArray(key, value); + + if (update) + { + return Talo.Players.DebounceUpdate(); + } + return Task.FromResult(new PlayersAPI.PlayerUpdateResult(true)); + } + + public bool IsInGroupID(string groupId) + { + return groups.Any((group) => group.id == groupId); + } + + public bool IsInGroupName(string groupName) + { + return groups.Any((group) => group.name == groupName); + } + + public PlayerAlias GetAlias(string service = "") + { + if (string.IsNullOrEmpty(service)) + { + return aliases.Length > 0 ? aliases[0] : null; + } + + return aliases.FirstOrDefault((alias) => alias.service == service); + } + } +} diff --git a/Assets/Talo Game Services/Talo/Runtime/Entities/PlayerAlias.cs b/Assets/Talo Game Services/Talo/Runtime/Entities/PlayerAlias.cs index 8712e618..99decce3 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Entities/PlayerAlias.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Entities/PlayerAlias.cs @@ -6,7 +6,9 @@ namespace TaloGameServices [System.Serializable] public class PlayerAlias { - private static readonly string offlineDataPath = Application.persistentDataPath + "/ta.bin"; + private static string _offlineDataPath; + private static string OfflineDataPath => + _offlineDataPath ??= Application.persistentDataPath + "/ta.bin"; public int id; public string service, identifier, displayName; @@ -26,12 +28,12 @@ public void WriteOfflineAlias() } var content = JsonUtility.ToJson(this); - Talo.Crypto.WriteFileContent(offlineDataPath, content); + Talo.Crypto.WriteFileContent(OfflineDataPath, content); } public static bool HasOfflineAlias() { - return Talo.Settings.cachePlayerOnIdentify && File.Exists(offlineDataPath); + return Talo.Settings.cachePlayerOnIdentify && File.Exists(OfflineDataPath); } public static PlayerAlias GetOfflineAlias() @@ -41,16 +43,16 @@ public static PlayerAlias GetOfflineAlias() return null; } - return JsonUtility.FromJson(Talo.Crypto.ReadFileContent(offlineDataPath)); + return JsonUtility.FromJson(Talo.Crypto.ReadFileContent(OfflineDataPath)); } public static void DeleteOfflineAlias() { - if (File.Exists(offlineDataPath)) + if (File.Exists(OfflineDataPath)) { try { - File.Delete(offlineDataPath); + File.Delete(OfflineDataPath); } catch (System.Exception ex) { diff --git a/Assets/Talo Game Services/Talo/Runtime/Entities/RejectedProp.cs b/Assets/Talo Game Services/Talo/Runtime/Entities/RejectedProp.cs index 5f338fc1..9ae7724c 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Entities/RejectedProp.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Entities/RejectedProp.cs @@ -35,14 +35,6 @@ public static RejectedProp[] FromJson(string json) return wrapper?.rejectedProps ?? Array.Empty(); } - public static void TryEmit(string json, Action onRejected) - { - var rejectedProps = FromJson(json); - if (rejectedProps.Length > 0) - { - onRejected?.Invoke(rejectedProps); - } - } } [Serializable] diff --git a/Assets/Talo Game Services/Talo/Runtime/Exceptions.meta b/Assets/Talo Game Services/Talo/Runtime/Exceptions.meta new file mode 100644 index 00000000..4d68c5a7 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Exceptions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fe7598f29ba824a71a1983bf4801be12 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/ContinuityReplayException.cs b/Assets/Talo Game Services/Talo/Runtime/Exceptions/ContinuityReplayException.cs similarity index 92% rename from Assets/Talo Game Services/Talo/Runtime/Utils/ContinuityReplayException.cs rename to Assets/Talo Game Services/Talo/Runtime/Exceptions/ContinuityReplayException.cs index 3ba32991..b646d20d 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Utils/ContinuityReplayException.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Exceptions/ContinuityReplayException.cs @@ -5,7 +5,7 @@ namespace TaloGameServices { public class ContinuityReplayException : Exception { - private List _exceptions; + private readonly List _exceptions; public List Exceptions => _exceptions; public ContinuityReplayException(List exceptions) diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/ContinuityReplayException.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Exceptions/ContinuityReplayException.cs.meta similarity index 100% rename from Assets/Talo Game Services/Talo/Runtime/Utils/ContinuityReplayException.cs.meta rename to Assets/Talo Game Services/Talo/Runtime/Exceptions/ContinuityReplayException.cs.meta diff --git a/Assets/Talo Game Services/Talo/Runtime/Exceptions/IdentifyException.cs b/Assets/Talo Game Services/Talo/Runtime/Exceptions/IdentifyException.cs new file mode 100644 index 00000000..3e019d57 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Exceptions/IdentifyException.cs @@ -0,0 +1,62 @@ +using System; +using UnityEngine; + +namespace TaloGameServices +{ + public enum IdentifyErrorCode + { + UNKNOWN_ERROR, + IDENTIFIER_PROFANITY, + IDENTIFIER_TAKEN + } + + public class IdentifyException : Exception + { + public IdentifyErrorCode ErrorCode { get; } + + public IdentifyException(IdentifyErrorCode code = IdentifyErrorCode.UNKNOWN_ERROR) + : base(code.ToString()) + { + ErrorCode = code; + } + + public IdentifyException(IdentifyErrorCode code, Exception inner) + : base(code.ToString(), inner) + { + ErrorCode = code; + } + + public static IdentifyException FromException(Exception ex) + { + if (ex is RequestException re && !string.IsNullOrEmpty(re.responseBody)) + { + return FromResponse(re.responseBody); + } + + return new IdentifyException(); + } + + private static IdentifyException FromResponse(string body) + { + if (string.IsNullOrEmpty(body)) + { + return new IdentifyException(); + } + + try + { + var parsed = JsonUtility.FromJson(body); + if (parsed != null && !string.IsNullOrEmpty(parsed.errorCode) && + Enum.TryParse(parsed.errorCode, out IdentifyErrorCode code)) + { + return new IdentifyException(code); + } + } + catch + { + } + + return new IdentifyException(); + } + } +} diff --git a/Assets/Talo Game Services/Talo/Runtime/Exceptions/IdentifyException.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Exceptions/IdentifyException.cs.meta new file mode 100644 index 00000000..bd2ff2d3 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Exceptions/IdentifyException.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a6ca927edafd34429a5ae1699cca53b5 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/PlayerAuthException.cs b/Assets/Talo Game Services/Talo/Runtime/Exceptions/PlayerAuthException.cs similarity index 63% rename from Assets/Talo Game Services/Talo/Runtime/Utils/PlayerAuthException.cs rename to Assets/Talo Game Services/Talo/Runtime/Exceptions/PlayerAuthException.cs index e2f94628..ce58ae66 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Utils/PlayerAuthException.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Exceptions/PlayerAuthException.cs @@ -23,26 +23,35 @@ public enum PlayerAuthErrorCode { public class PlayerAuthException : Exception { - public PlayerAuthErrorCode ErrorCode => GetErrorCode(); + public PlayerAuthErrorCode ErrorCode { get; } public PlayerAuthException() { + ErrorCode = PlayerAuthErrorCode.API_ERROR; } public PlayerAuthException(string errorCode) : base(errorCode) { + ErrorCode = ParseErrorCode(errorCode); } public PlayerAuthException(string errorCode, Exception inner) : base(errorCode, inner) { + ErrorCode = ParseErrorCode(errorCode); } - private PlayerAuthErrorCode GetErrorCode() + private static PlayerAuthErrorCode ParseErrorCode(string errorCode) { - var errorCode = string.IsNullOrEmpty(Message) ? "API_ERROR" : Message; - return (PlayerAuthErrorCode)Enum.Parse(typeof(PlayerAuthErrorCode), errorCode); + if (string.IsNullOrEmpty(errorCode)) + { + return PlayerAuthErrorCode.API_ERROR; + } + + return Enum.TryParse(errorCode, out PlayerAuthErrorCode code) + ? code + : PlayerAuthErrorCode.API_ERROR; } } } diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/PlayerAuthException.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Exceptions/PlayerAuthException.cs.meta similarity index 100% rename from Assets/Talo Game Services/Talo/Runtime/Utils/PlayerAuthException.cs.meta rename to Assets/Talo Game Services/Talo/Runtime/Exceptions/PlayerAuthException.cs.meta diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/RequestException.cs b/Assets/Talo Game Services/Talo/Runtime/Exceptions/RequestException.cs similarity index 100% rename from Assets/Talo Game Services/Talo/Runtime/Utils/RequestException.cs rename to Assets/Talo Game Services/Talo/Runtime/Exceptions/RequestException.cs diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/RequestException.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Exceptions/RequestException.cs.meta similarity index 100% rename from Assets/Talo Game Services/Talo/Runtime/Utils/RequestException.cs.meta rename to Assets/Talo Game Services/Talo/Runtime/Exceptions/RequestException.cs.meta diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/SocketException.cs b/Assets/Talo Game Services/Talo/Runtime/Exceptions/SocketException.cs similarity index 63% rename from Assets/Talo Game Services/Talo/Runtime/Utils/SocketException.cs rename to Assets/Talo Game Services/Talo/Runtime/Exceptions/SocketException.cs index 9268a3ec..af632b34 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Utils/SocketException.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Exceptions/SocketException.cs @@ -21,29 +21,38 @@ public class SocketException : Exception private readonly SocketError errorData; public string Req => errorData?.req ?? "unknown"; - public SocketErrorCode ErrorCode => GetErrorCode(); + public SocketErrorCode ErrorCode { get; } public string Cause => errorData?.cause ?? ""; public SocketException() { + ErrorCode = SocketErrorCode.API_ERROR; } public SocketException(SocketError errorData) : base(errorData.message) { this.errorData = errorData; + ErrorCode = ParseErrorCode(errorData?.errorCode); } public SocketException(SocketError errorData, Exception inner) : base(errorData.message, inner) { this.errorData = errorData; + ErrorCode = ParseErrorCode(errorData?.errorCode); } - private SocketErrorCode GetErrorCode() + private static SocketErrorCode ParseErrorCode(string errorCode) { - var errorCode = string.IsNullOrEmpty(errorData?.errorCode) ? "API_ERROR" : errorData.errorCode; - return (SocketErrorCode)Enum.Parse(typeof(SocketErrorCode), errorCode); + if (string.IsNullOrEmpty(errorCode)) + { + return SocketErrorCode.API_ERROR; + } + + return Enum.TryParse(errorCode, out SocketErrorCode code) + ? code + : SocketErrorCode.API_ERROR; } } } diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/SocketException.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Exceptions/SocketException.cs.meta similarity index 100% rename from Assets/Talo Game Services/Talo/Runtime/Utils/SocketException.cs.meta rename to Assets/Talo Game Services/Talo/Runtime/Exceptions/SocketException.cs.meta diff --git a/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateOwnerRequest.cs b/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateOwnerRequest.cs deleted file mode 100644 index 1cabea21..00000000 --- a/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateOwnerRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace TaloGameServices -{ - [System.Serializable] - public class ChannelsUpdateOwnerRequest - { - public string name; - public int newOwnerAliasId; - public Prop[] props; - } -} diff --git a/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateOwnerRequest.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateOwnerRequest.cs.meta deleted file mode 100644 index 65852d30..00000000 --- a/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateOwnerRequest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 6067ea0a81fd1455097a544ce372f3d4 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateRequest.cs b/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateRequest.cs deleted file mode 100644 index db03f51a..00000000 --- a/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace TaloGameServices -{ - [System.Serializable] - public class ChannelsUpdateRequest - { - public string name; - public Prop[] props; - } -} diff --git a/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateRequest.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateRequest.cs.meta deleted file mode 100644 index 82915b2f..00000000 --- a/Assets/Talo Game Services/Talo/Runtime/Requests/ChannelsUpdateRequest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f39bcae3bbdcb47aca569d02e2f27c8e \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Runtime/Responses/ChannelStoragePropsSetResponse.cs b/Assets/Talo Game Services/Talo/Runtime/Responses/ChannelStoragePropsSetResponse.cs index 25eaa6d2..e814a3d5 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Responses/ChannelStoragePropsSetResponse.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Responses/ChannelStoragePropsSetResponse.cs @@ -2,18 +2,10 @@ namespace TaloGameServices { - [Serializable] - public class ChannelStoragePropError - { - public string key; - public string error; - public string message; - } - [Serializable] public class ChannelStoragePropsSetResponse { public Channel channel; - public ChannelStoragePropError[] failedProps; + public RejectedProp[] failedProps; } } diff --git a/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs b/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs index 6f55788e..f36c0e92 100644 --- a/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs +++ b/Assets/Talo Game Services/Talo/Runtime/TaloManager.cs @@ -37,9 +37,24 @@ private void OnDisable() Talo.Events.OnFlushed -= ResetFlushTimer; } - private void OnApplicationQuit() + private async void OnApplicationQuit() { - DoFlush(); + try + { + if (Talo.HasIdentity()) + { + await Talo.Events.Flush(); + await Talo.Players.FlushUpdates(); + if (Talo.Saves.Current != null) + { + await Talo.Saves.FlushUpdates(); + } + } + } + catch (Exception ex) + { + Debug.LogError($"Failed to flush on quit: {ex}"); + } } private void OnApplicationFocus(bool hasFocus) diff --git a/Assets/Talo Game Services/Talo/Runtime/TaloSettings.cs b/Assets/Talo Game Services/Talo/Runtime/TaloSettings.cs index ef6d1e7e..fa7522d2 100644 --- a/Assets/Talo Game Services/Talo/Runtime/TaloSettings.cs +++ b/Assets/Talo Game Services/Talo/Runtime/TaloSettings.cs @@ -36,7 +36,7 @@ public class TaloSettings : ScriptableObject public bool cachePlayerOnIdentify = true; [Tooltip("Number of seconds to wait before sending debounced requests (e.g. player updates, save updates and health checks)")] - public float debounceTimerSeconds = 1f; + public float debounceTimerSeconds = 0.5f; [Tooltip("Enable request verification to prevent replay attacks and tampering - this must also be enabled in the dashboard")] public bool verificationEnabled = false; diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/JsonUtils.cs b/Assets/Talo Game Services/Talo/Runtime/Utils/JsonUtils.cs new file mode 100644 index 00000000..f8cca1a2 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Utils/JsonUtils.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using UnityEngine; + +namespace TaloGameServices +{ + public static class JsonUtils + { + public static string BuildObject(params (string key, object value)[] fields) + { + var parts = new List(); + foreach (var (key, value) in fields) + { + if (value == null) + { + continue; + } + parts.Add($"\"{key}\":{SerializeValue(value)}"); + } + return "{" + string.Join(",", parts) + "}"; + } + + private static string SerializeValue(object value) + { + if (value is Prop p) return SerializeProp(p); + if (value is Prop[] props) return "[" + string.Join(",", props.Select(SerializeProp)) + "]"; + if (value is string s) return JsonEscape(s); + if (value is bool b) return b ? "true" : "false"; + if (value is int i) return i.ToString(); + if (value is long l) return l.ToString(); + if (value is float f) return f.ToString(CultureInfo.InvariantCulture); + if (value is double d) return d.ToString(CultureInfo.InvariantCulture); + return JsonUtility.ToJson(value); + } + + private static string SerializeProp(Prop p) => Prop.SanitiseJson(JsonUtility.ToJson(p)); + + [System.Serializable] + private class JsonString { public string v; } + + private static string JsonEscape(string s) + { + // construct a json string and let JsonUtility handle the escaping + var json = JsonUtility.ToJson(new JsonString { v = s }); + // strip {"v": prefix and trailing } from JsonUtility's {"v":"..."} output + return json.Substring(5, json.Length - 6); + } + } +} diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/JsonUtils.cs.meta b/Assets/Talo Game Services/Talo/Runtime/Utils/JsonUtils.cs.meta new file mode 100644 index 00000000..e68ceea2 --- /dev/null +++ b/Assets/Talo Game Services/Talo/Runtime/Utils/JsonUtils.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dafeaac7aede64f4abf3464c573221f5 \ No newline at end of file diff --git a/Assets/Talo Game Services/Talo/Runtime/Utils/RequestMock.cs b/Assets/Talo Game Services/Talo/Runtime/Utils/RequestMock.cs index 7244f918..f68e4ee6 100644 --- a/Assets/Talo Game Services/Talo/Runtime/Utils/RequestMock.cs +++ b/Assets/Talo Game Services/Talo/Runtime/Utils/RequestMock.cs @@ -13,8 +13,8 @@ private struct RequestHandler public long status; } - private static List _permanentHandlers = new List(); - private static List _oneTimeHandlers = new List(); + private static readonly List _permanentHandlers = new(); + private static readonly List _oneTimeHandlers = new(); private static bool _offline; public static bool Offline diff --git a/Assets/Talo Game Services/Talo/Samples/AuthenticationDemo/Scripts/GameUIController.cs b/Assets/Talo Game Services/Talo/Samples/AuthenticationDemo/Scripts/GameUIController.cs index 09f6eb1f..8d70daa2 100644 --- a/Assets/Talo Game Services/Talo/Samples/AuthenticationDemo/Scripts/GameUIController.cs +++ b/Assets/Talo Game Services/Talo/Samples/AuthenticationDemo/Scripts/GameUIController.cs @@ -19,9 +19,9 @@ private void OnDisable() Talo.Players.OnIdentified -= OnIdentified; } - private void OnIdentified(Player player) + private void OnIdentified(PlayerAlias alias) { - root.Q