Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ada47f9
add pr summary workflow
tudddorrr Jul 3, 2026
306c144
remove the deprecated GetStat() function
tudddorrr Jul 3, 2026
90fd19f
Merge pull request #215 from TaloDev/remove-deprecated-get-stat
tudddorrr Jul 3, 2026
7bbf077
remove the deprecated GetChannels(int page) signature
tudddorrr Jul 3, 2026
8c3b278
Merge pull request #216 from TaloDev/remove-deprecated-get-channels-w…
tudddorrr Jul 3, 2026
dda5428
remove the deprecated CreatePrivate function
tudddorrr Jul 4, 2026
e84263f
Merge pull request #217 from TaloDev/remove-deprecated-create-private…
tudddorrr Jul 4, 2026
bfa8250
remove the deprecated Create overload for channels
tudddorrr Jul 4, 2026
b0bb073
Merge pull request #218 from TaloDev/remove-deprecated-create-channel…
tudddorrr Jul 4, 2026
f95e8ca
remove the deprecated GetEntriesForCurrentPlayer() functions
tudddorrr Jul 4, 2026
8d3980c
Merge pull request #219 from TaloDev/remove-deprecated-get-entries-fo…
tudddorrr Jul 4, 2026
832b60b
remove the deprecated GetCacheEntriesForCurrentPlayer() function
tudddorrr Jul 4, 2026
edd82ad
Merge pull request #220 from TaloDev/remove-deprecated-get-cached-ent…
tudddorrr Jul 4, 2026
4ccb057
remove the deprecated GetEntries overload
tudddorrr Jul 4, 2026
fc6adf6
Merge pull request #221 from TaloDev/remove-deprecated-get-entries-si…
tudddorrr Jul 4, 2026
b0828b7
replace ChannelStoragePropError with RejectedProp
tudddorrr Jul 4, 2026
25b34c4
Merge pull request #222 from TaloDev/replace-channel-storage-prop-error
tudddorrr Jul 4, 2026
7015d86
return player alias from OnIdentified event
tudddorrr Jul 4, 2026
240ca42
Merge pull request #223 from TaloDev/identify-player-alias
tudddorrr Jul 4, 2026
b008629
include error code in OnIdentificationFailed event
tudddorrr Jul 4, 2026
fbe4f61
Merge pull request #224 from TaloDev/identification-failed-errorcode
tudddorrr Jul 4, 2026
7db6eef
make exceptions slightly more consistent
tudddorrr Jul 4, 2026
4b21a7c
move exceptions to their own folder
tudddorrr Jul 4, 2026
0a28956
Merge pull request #225 from TaloDev/move-exceptions
tudddorrr Jul 4, 2026
953f601
json manipulation tests
tudddorrr Jul 20, 2026
86859e4
make channels.update use the update options pattern
tudddorrr Jul 21, 2026
eab934c
Merge pull request #226 from TaloDev/channel-update-options
tudddorrr Jul 21, 2026
bfc3872
lazy load offline path for player alias
tudddorrr Jul 21, 2026
ba361a8
remove redundant defaults for channel update options
tudddorrr Jul 21, 2026
97b03f0
formatting for storage demo props
tudddorrr Jul 21, 2026
cf0563a
update pr summary model/condition
tudddorrr Aug 4, 2026
927d4ff
update pr summary
tudddorrr Aug 4, 2026
c65b010
allow player + save debounced updates to be handled synchronously
tudddorrr Aug 4, 2026
af738f5
Merge pull request #227 from TaloDev/flush-debounced-updates
tudddorrr Aug 4, 2026
12ceb2d
summarise just my prs
tudddorrr Aug 4, 2026
f3640b1
remove prop rejection events, keep them inline with update results
tudddorrr Aug 4, 2026
861db3c
Merge pull request #228 from TaloDev/remove-prop-rejection-events
tudddorrr Aug 4, 2026
fda55ad
remove leading debounces
tudddorrr Aug 5, 2026
e60b914
make default debounce time 0.5 seconds
tudddorrr Aug 5, 2026
1fb437b
Merge pull request #229 from TaloDev/remove-leading-debounce
tudddorrr Aug 5, 2026
4e6f3b6
Merge pull request #230 from TaloDev/default-debounce-0.5
tudddorrr Aug 5, 2026
91b6f09
restrict ci workflows to develop and pull requests, drop pr summary
tudddorrr Aug 19, 2026
70e2c3a
1.0.0
tudddorrr Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Build check

on: push
on:
push:
branches: [develop]
pull_request:

jobs:
build-check:
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Unity tests
name: Unit tests

on: push
on:
push:
branches: [develop]
pull_request:

jobs:
test:
Expand Down
38 changes: 0 additions & 38 deletions .github/workflows/opencode.yml

This file was deleted.

12 changes: 8 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TOperation>` (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<TOperation, TReturnData, TUpdateResult>` 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<YourAPI.DebouncedOperation>`
2. Inherit from `DebouncedAPI<YourAPI.DebouncedOperation, TReturnData, TUpdateResult>`
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<PlayersAPI.DebouncedOperation>`. 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<PlayersAPI.DebouncedOperation, RejectedProp[], PlayersAPI.PlayerUpdateResult>`. 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<PlayerUpdateResult>`, `Talo.Saves.UpdateCurrentSave()` returns `Task<SaveUpdateResult>`. 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

Expand Down
11 changes: 9 additions & 2 deletions Assets/Talo Game Services/Talo/Runtime/APIs/BaseAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -145,7 +145,14 @@ protected async Task<string> 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);
}
}
}
}
Expand Down
100 changes: 46 additions & 54 deletions Assets/Talo Game Services/Talo/Runtime/APIs/ChannelsAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -86,8 +96,7 @@ public class ChannelsAPI : BaseAPI
public event Action<Channel, PlayerAlias> OnOwnershipTransferred;
public event Action<Channel> OnChannelDeleted;
public event Action<Channel, string[]> OnChannelUpdated;
public event Action<RejectedProp[]> OnChannelPropsRejected;
public event Action<Channel, ChannelStoragePropError[]> OnChannelStoragePropsFailedToSet;
public event Action<Channel, RejectedProp[]> OnChannelStoragePropsFailedToSet;
public event Action<Channel, ChannelStorageProp[], ChannelStorageProp[]> OnChannelStoragePropsUpdated;

private readonly ChannelStorageManager _storageManager = new ();
Expand Down Expand Up @@ -147,12 +156,6 @@ public async Task<ChannelsIndexResponse> GetChannels(GetChannelsOptions options
return res;
}

[Obsolete("Use GetChannels(GetChannelsOptions options) instead.")]
public async Task<ChannelsIndexResponse> GetChannels(int page)
{
return await GetChannels(new GetChannelsOptions { page = page });
}

public async Task<Channel[]> GetSubscribedChannels(GetSubscribedChannelsOptions options = null)
{
Talo.IdentityCheck();
Expand All @@ -166,7 +169,7 @@ public async Task<Channel[]> GetSubscribedChannels(GetSubscribedChannelsOptions
return res.channels;
}

private async Task<Channel> SendCreateChannelRequest(CreateChannelOptions options)
private async Task<ChannelUpsertResult> SendCreateChannelRequest(CreateChannelOptions options)
{
Talo.IdentityCheck();

Expand All @@ -187,50 +190,24 @@ private async Task<Channel> SendCreateChannelRequest(CreateChannelOptions option
var json = await Call(uri, "POST", content);

var res = JsonUtility.FromJson<ChannelResponse>(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<Channel> Create(CreateChannelOptions options)
public async Task<ChannelUpsertResult> Create(CreateChannelOptions options)
{
options ??= new CreateChannelOptions();
return await SendCreateChannelRequest(options);
}

[Obsolete("Use Create(CreateChannelOptions options) instead.")]
public async Task<Channel> 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<Channel> 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<Channel> Join(int channelId)
{
Talo.IdentityCheck();
Expand All @@ -250,36 +227,37 @@ public async Task Leave(int channelId)
await Call(uri, "POST");
}

public async Task<Channel> Update(int channelId, string name = "", int newOwnerAliasId = -1, params (string, string)[] propTuples)
public async Task<ChannelUpsertResult> 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<ChannelResponse>(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;
}
Expand Down Expand Up @@ -421,5 +399,19 @@ public async Task<ChannelStorageProp[]> ListStorageProps(int channelId, string[]

return Array.Empty<ChannelStorageProp>();
}

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<RejectedProp>();
}
}
}
}
Loading
Loading