From 5ce9388223a15e4f52bbd7b0416221098a6f5ea6 Mon Sep 17 00:00:00 2001 From: Daniel Widgren Date: Tue, 7 Apr 2026 00:22:12 +0200 Subject: [PATCH] feat: sync API surface with asobi backend --- Runtime/Api/AsobiDirectMessages.cs | 23 ++++++ Runtime/Api/AsobiInventory.cs | 12 ++- Runtime/Api/AsobiMatches.cs | 13 +++- Runtime/Api/AsobiMatchmaker.cs | 4 +- Runtime/Api/AsobiNotifications.cs | 17 ++++- Runtime/Api/AsobiSocial.cs | 40 ++++++++-- Runtime/Api/AsobiTournaments.cs | 12 ++- Runtime/Api/AsobiWorlds.cs | 36 +++++++++ Runtime/AsobiClient.cs | 4 + Runtime/Models/DirectMessageModels.cs | 35 +++++++++ Runtime/Models/EconomyModels.cs | 7 ++ Runtime/Models/MatchModels.cs | 2 + Runtime/Models/RealtimeModels.cs | 28 +++++++ Runtime/Models/SocialModels.cs | 29 ++++++++ Runtime/Models/WorldModels.cs | 30 ++++++++ Runtime/WebSocket/AsobiRealtime.cs | 102 +++++++++++++++++++++++++- 16 files changed, 372 insertions(+), 22 deletions(-) create mode 100644 Runtime/Api/AsobiDirectMessages.cs create mode 100644 Runtime/Api/AsobiWorlds.cs create mode 100644 Runtime/Models/DirectMessageModels.cs create mode 100644 Runtime/Models/WorldModels.cs diff --git a/Runtime/Api/AsobiDirectMessages.cs b/Runtime/Api/AsobiDirectMessages.cs new file mode 100644 index 0000000..814aab3 --- /dev/null +++ b/Runtime/Api/AsobiDirectMessages.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Asobi +{ + public class AsobiDirectMessages + { + readonly AsobiClient _client; + internal AsobiDirectMessages(AsobiClient client) => _client = client; + + public Task SendAsync(string recipientId, string content) + { + var req = new SendDmRequest { recipient_id = recipientId, content = content }; + return _client.Http.Post("/api/v1/dm", req); + } + + public Task GetHistoryAsync(string playerId, int limit = 50) + { + var query = new Dictionary { { "limit", limit.ToString() } }; + return _client.Http.Get($"/api/v1/dm/{playerId}/history", query); + } + } +} diff --git a/Runtime/Api/AsobiInventory.cs b/Runtime/Api/AsobiInventory.cs index 5a6279b..1ca65c5 100644 --- a/Runtime/Api/AsobiInventory.cs +++ b/Runtime/Api/AsobiInventory.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; namespace Asobi @@ -7,15 +8,18 @@ public class AsobiInventory readonly AsobiClient _client; internal AsobiInventory(AsobiClient client) => _client = client; - public Task ListAsync() + public Task ListAsync(int? limit = null) { - return _client.Http.Get("/api/v1/inventory"); + Dictionary query = null; + if (limit.HasValue) + query = new Dictionary { { "limit", limit.Value.ToString() } }; + return _client.Http.Get("/api/v1/inventory", query); } - public Task ConsumeAsync(string itemId, int quantity = 1) + public Task ConsumeAsync(string itemId, int quantity = 1) { var req = new ConsumeRequest { item_id = itemId, quantity = quantity }; - return _client.Http.Post("/api/v1/inventory/consume", req); + return _client.Http.Post("/api/v1/inventory/consume", req); } } } diff --git a/Runtime/Api/AsobiMatches.cs b/Runtime/Api/AsobiMatches.cs index 2b7c1e0..5d9b0b7 100644 --- a/Runtime/Api/AsobiMatches.cs +++ b/Runtime/Api/AsobiMatches.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; namespace Asobi @@ -7,9 +8,17 @@ public class AsobiMatches readonly AsobiClient _client; internal AsobiMatches(AsobiClient client) => _client = client; - public async Task ListAsync() + public async Task ListAsync(string mode = null, string status = null, int? limit = null) { - var raw = await _client.Http.GetRaw("/api/v1/matches"); + Dictionary query = null; + if (mode != null || status != null || limit.HasValue) + { + query = new Dictionary(); + if (mode != null) query["mode"] = mode; + if (status != null) query["status"] = status; + if (limit.HasValue) query["limit"] = limit.Value.ToString(); + } + var raw = await _client.Http.GetRaw("/api/v1/matches", query); return JsonHelper.ParseMatchList(raw); } diff --git a/Runtime/Api/AsobiMatchmaker.cs b/Runtime/Api/AsobiMatchmaker.cs index bf97aa0..d6344d9 100644 --- a/Runtime/Api/AsobiMatchmaker.cs +++ b/Runtime/Api/AsobiMatchmaker.cs @@ -7,9 +7,9 @@ public class AsobiMatchmaker readonly AsobiClient _client; internal AsobiMatchmaker(AsobiClient client) => _client = client; - public Task AddAsync(string mode = "default") + public Task AddAsync(string mode = "default", string properties = null, string[] party = null) { - var req = new MatchmakerRequest { mode = mode }; + var req = new MatchmakerRequest { mode = mode, properties = properties, party = party }; return _client.Http.Post("/api/v1/matchmaker", req); } diff --git a/Runtime/Api/AsobiNotifications.cs b/Runtime/Api/AsobiNotifications.cs index 3293539..cc53668 100644 --- a/Runtime/Api/AsobiNotifications.cs +++ b/Runtime/Api/AsobiNotifications.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; namespace Asobi @@ -7,15 +8,23 @@ public class AsobiNotifications readonly AsobiClient _client; internal AsobiNotifications(AsobiClient client) => _client = client; - public async Task ListAsync() + public async Task ListAsync(bool? read = null, int? limit = null) { - var raw = await _client.Http.GetRaw("/api/v1/notifications"); + Dictionary query = null; + if (read.HasValue || limit.HasValue) + { + query = new Dictionary(); + if (read.HasValue) query["read"] = read.Value.ToString().ToLower(); + if (limit.HasValue) query["limit"] = limit.Value.ToString(); + } + var raw = await _client.Http.GetRaw("/api/v1/notifications", query); return JsonHelper.ParseNotificationList(raw); } - public Task MarkReadAsync(string notificationId) + public async Task MarkReadAsync(string notificationId) { - return _client.Http.Put($"/api/v1/notifications/{notificationId}/read"); + var raw = await _client.Http.PutRaw($"/api/v1/notifications/{notificationId}/read", "{}"); + return JsonHelper.ParseNotification(raw); } public Task DeleteAsync(string notificationId) diff --git a/Runtime/Api/AsobiSocial.cs b/Runtime/Api/AsobiSocial.cs index 6c02e8e..67515c5 100644 --- a/Runtime/Api/AsobiSocial.cs +++ b/Runtime/Api/AsobiSocial.cs @@ -24,16 +24,20 @@ public Task AddFriendAsync(string friendId) return _client.Http.Post("/api/v1/friends", req); } - public Task AcceptFriendAsync(string friendId) + public Task UpdateFriendAsync(string friendId, string status) { - var req = new UpdateFriendRequest { status = "accepted" }; + var req = new UpdateFriendRequest { status = status }; return _client.Http.Put($"/api/v1/friends/{friendId}", req); } + public Task AcceptFriendAsync(string friendId) + { + return UpdateFriendAsync(friendId, "accepted"); + } + public Task BlockFriendAsync(string friendId) { - var req = new UpdateFriendRequest { status = "blocked" }; - return _client.Http.Put($"/api/v1/friends/{friendId}", req); + return UpdateFriendAsync(friendId, "blocked"); } public Task RemoveFriendAsync(string friendId) @@ -65,16 +69,40 @@ public Task JoinGroupAsync(string groupId) return _client.Http.Post($"/api/v1/groups/{groupId}/join"); } + public Task UpdateGroupAsync(string groupId, UpdateGroupRequest update) + { + return _client.Http.Put($"/api/v1/groups/{groupId}", update); + } + public Task LeaveGroupAsync(string groupId) { return _client.Http.Post($"/api/v1/groups/{groupId}/leave"); } + public Task GetGroupMembersAsync(string groupId) + { + return _client.Http.Get($"/api/v1/groups/{groupId}/members"); + } + + public Task UpdateMemberRoleAsync(string groupId, string playerId, string role) + { + var req = new UpdateMemberRoleRequest { role = role }; + return _client.Http.Put($"/api/v1/groups/{groupId}/members/{playerId}/role", req); + } + + public Task RemoveMemberAsync(string groupId, string playerId) + { + return _client.Http.Delete($"/api/v1/groups/{groupId}/members/{playerId}"); + } + // --- Chat --- - public Task GetChatHistoryAsync(string channelId) + public Task GetChatHistoryAsync(string channelId, int? limit = null) { - return _client.Http.Get($"/api/v1/chat/{channelId}/history"); + Dictionary query = null; + if (limit.HasValue) + query = new Dictionary { { "limit", limit.Value.ToString() } }; + return _client.Http.Get($"/api/v1/chat/{channelId}/history", query); } } } diff --git a/Runtime/Api/AsobiTournaments.cs b/Runtime/Api/AsobiTournaments.cs index 6f91a2d..db08ceb 100644 --- a/Runtime/Api/AsobiTournaments.cs +++ b/Runtime/Api/AsobiTournaments.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; namespace Asobi @@ -7,9 +8,16 @@ public class AsobiTournaments readonly AsobiClient _client; internal AsobiTournaments(AsobiClient client) => _client = client; - public Task ListAsync() + public Task ListAsync(string status = null, int? limit = null) { - return _client.Http.Get("/api/v1/tournaments"); + Dictionary query = null; + if (status != null || limit.HasValue) + { + query = new Dictionary(); + if (status != null) query["status"] = status; + if (limit.HasValue) query["limit"] = limit.Value.ToString(); + } + return _client.Http.Get("/api/v1/tournaments", query); } public Task GetAsync(string tournamentId) diff --git a/Runtime/Api/AsobiWorlds.cs b/Runtime/Api/AsobiWorlds.cs new file mode 100644 index 0000000..0c1c23d --- /dev/null +++ b/Runtime/Api/AsobiWorlds.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Asobi +{ + public class AsobiWorlds + { + readonly AsobiClient _client; + internal AsobiWorlds(AsobiClient client) => _client = client; + + public Task ListAsync(string mode = null, bool? hasCapacity = null) + { + Dictionary query = null; + if (mode != null || hasCapacity.HasValue) + { + query = new Dictionary(); + if (mode != null) + query["mode"] = mode; + if (hasCapacity.HasValue) + query["has_capacity"] = hasCapacity.Value.ToString().ToLower(); + } + return _client.Http.Get("/api/v1/worlds", query); + } + + public Task GetAsync(string worldId) + { + return _client.Http.Get($"/api/v1/worlds/{worldId}"); + } + + public Task CreateAsync(string mode) + { + var req = new CreateWorldRequest { mode = mode }; + return _client.Http.Post("/api/v1/worlds", req); + } + } +} diff --git a/Runtime/AsobiClient.cs b/Runtime/AsobiClient.cs index bdf3404..7cce9ea 100644 --- a/Runtime/AsobiClient.cs +++ b/Runtime/AsobiClient.cs @@ -19,6 +19,8 @@ public class AsobiClient : IDisposable public AsobiStorage Storage { get; } public AsobiIAP IAP { get; } public AsobiVotes Votes { get; } + public AsobiWorlds Worlds { get; } + public AsobiDirectMessages DirectMessages { get; } public AsobiRealtime Realtime { get; } internal HttpClient Http { get; } @@ -59,6 +61,8 @@ public AsobiClient(AsobiConfig config) Storage = new AsobiStorage(this); IAP = new AsobiIAP(this); Votes = new AsobiVotes(this); + Worlds = new AsobiWorlds(this); + DirectMessages = new AsobiDirectMessages(this); Realtime = new AsobiRealtime(this); } diff --git a/Runtime/Models/DirectMessageModels.cs b/Runtime/Models/DirectMessageModels.cs new file mode 100644 index 0000000..4129f5b --- /dev/null +++ b/Runtime/Models/DirectMessageModels.cs @@ -0,0 +1,35 @@ +using System; + +namespace Asobi +{ + [Serializable] + public class DirectMessage + { + public string id; + public string sender_id; + public string recipient_id; + public string content; + public long sent_at; + } + + [Serializable] + public class DirectMessageListResponse + { + public DirectMessage[] messages; + public string channel_id; + } + + [Serializable] + public class SendDmRequest + { + public string recipient_id; + public string content; + } + + [Serializable] + public class DmSendResponse + { + public bool success; + public string channel_id; + } +} diff --git a/Runtime/Models/EconomyModels.cs b/Runtime/Models/EconomyModels.cs index 9c65ccc..d01066e 100644 --- a/Runtime/Models/EconomyModels.cs +++ b/Runtime/Models/EconomyModels.cs @@ -89,4 +89,11 @@ public class ConsumeRequest public string item_id; public int quantity; } + + [Serializable] + public class ConsumeResponse + { + public bool success; + public int remaining_quantity; + } } diff --git a/Runtime/Models/MatchModels.cs b/Runtime/Models/MatchModels.cs index eb33c99..2d163be 100644 --- a/Runtime/Models/MatchModels.cs +++ b/Runtime/Models/MatchModels.cs @@ -26,6 +26,8 @@ public class MatchListResponse public class MatchmakerRequest { public string mode; + public string properties; + public string[] party; } [Serializable] diff --git a/Runtime/Models/RealtimeModels.cs b/Runtime/Models/RealtimeModels.cs index f8f7904..3719c0f 100644 --- a/Runtime/Models/RealtimeModels.cs +++ b/Runtime/Models/RealtimeModels.cs @@ -47,6 +47,14 @@ internal class WsMatchmakerPayload public string mode; } + [Serializable] + internal class WsMatchmakerAddPayload + { + public string mode; + public string properties; + public string[] party; + } + [Serializable] internal class WsMatchmakerRemovePayload { @@ -59,6 +67,26 @@ internal class WsPresencePayload public string status; } + [Serializable] + internal class WsWorldIdPayload + { + public string world_id; + } + + [Serializable] + internal class WsWorldListPayload + { + public string mode; + public bool has_capacity; + } + + [Serializable] + internal class WsDmSendPayload + { + public string recipient_id; + public string content; + } + [Serializable] public class WsConnectedPayload { diff --git a/Runtime/Models/SocialModels.cs b/Runtime/Models/SocialModels.cs index dbe9c50..7444166 100644 --- a/Runtime/Models/SocialModels.cs +++ b/Runtime/Models/SocialModels.cs @@ -53,6 +53,35 @@ public class CreateGroupRequest public bool open; } + [Serializable] + public class UpdateGroupRequest + { + public string name; + public string description; + public int max_members; + public bool open; + } + + [Serializable] + public class GroupMember + { + public string player_id; + public string role; + public string joined_at; + } + + [Serializable] + public class GroupMembersResponse + { + public GroupMember[] members; + } + + [Serializable] + public class UpdateMemberRoleRequest + { + public string role; + } + [Serializable] public class ChatMessage { diff --git a/Runtime/Models/WorldModels.cs b/Runtime/Models/WorldModels.cs new file mode 100644 index 0000000..34c4040 --- /dev/null +++ b/Runtime/Models/WorldModels.cs @@ -0,0 +1,30 @@ +using System; + +namespace Asobi +{ + [Serializable] + public class WorldInfo + { + public string world_id; + public string status; + public int player_count; + public int max_players; + public string mode; + public int grid_size; + public long started_at; + public string[] players; + public bool HasCapacity => player_count < max_players; + } + + [Serializable] + public class WorldListResponse + { + public WorldInfo[] worlds; + } + + [Serializable] + public class CreateWorldRequest + { + public string mode; + } +} diff --git a/Runtime/WebSocket/AsobiRealtime.cs b/Runtime/WebSocket/AsobiRealtime.cs index d90fb2d..bcd22c0 100644 --- a/Runtime/WebSocket/AsobiRealtime.cs +++ b/Runtime/WebSocket/AsobiRealtime.cs @@ -29,6 +29,13 @@ public class AsobiRealtime : IDisposable public event Action OnVoteTally; public event Action OnVoteResult; public event Action OnVoteVetoed; + public event Action OnWorldTick; + public event Action OnWorldJoined; + public event Action OnWorldLeft; + public event Action OnWorldEvent; + public event Action OnDmMessage; + public event Action OnDmSent; + public event Action OnPresenceUpdated; public event Action OnError; internal AsobiRealtime(AsobiClient client) => _client = client; @@ -47,6 +54,11 @@ public async Task ConnectAsync() await SendAsync("session.connect", payload); } + public Task SendHeartbeatAsync() + { + return SendAsync("session.heartbeat", "{}"); + } + public Task SendMatchInputAsync(string data) { var payload = JsonUtility.ToJson(new WsMatchInputPayload { data = data }); @@ -82,9 +94,14 @@ public Task SendChatMessageAsync(string channelId, string content) return SendFireAndForget("chat.send", payload); } - public Task AddToMatchmakerAsync(string mode = "default") + public Task AddToMatchmakerAsync(string mode = "default", string properties = null, string[] party = null) { - var payload = JsonUtility.ToJson(new WsMatchmakerPayload { mode = mode }); + var payload = JsonUtility.ToJson(new WsMatchmakerAddPayload + { + mode = mode, + properties = properties, + party = party + }); return SendAsync("matchmaker.add", payload); } @@ -119,6 +136,62 @@ public Task UpdatePresenceAsync(string status = "online") return SendAsync("presence.update", payload); } + // --- World --- + + public Task WorldListAsync(string mode = null, bool? hasCapacity = null) + { + string payload; + if (mode != null || hasCapacity.HasValue) + { + var parts = new System.Collections.Generic.List(); + if (mode != null) parts.Add($"\"mode\":\"{mode}\""); + if (hasCapacity.HasValue) parts.Add($"\"has_capacity\":{(hasCapacity.Value ? "true" : "false")}"); + payload = "{" + string.Join(",", parts) + "}"; + } + else + { + payload = "{}"; + } + return SendAsync("world.list", payload); + } + + public Task WorldCreateAsync(string mode) + { + var payload = JsonUtility.ToJson(new WsMatchmakerPayload { mode = mode }); + return SendAsync("world.create", payload); + } + + public Task WorldFindOrCreateAsync(string mode) + { + var payload = JsonUtility.ToJson(new WsMatchmakerPayload { mode = mode }); + return SendAsync("world.find_or_create", payload); + } + + public Task WorldJoinAsync(string worldId) + { + var payload = $"{{\"world_id\":\"{worldId}\"}}"; + return SendAsync("world.join", payload); + } + + public Task WorldLeaveAsync() + { + return SendAsync("world.leave", "{}"); + } + + public Task WorldInputAsync(string data) + { + var payload = JsonUtility.ToJson(new WsMatchInputPayload { data = data }); + return SendFireAndForget("world.input", payload); + } + + // --- DM --- + + public Task SendDmAsync(string recipientId, string content) + { + var payload = $"{{\"recipient_id\":\"{recipientId}\",\"content\":\"{content}\"}}"; + return SendFireAndForget("dm.send", payload); + } + public async Task DisconnectAsync() { if (_ws == null) return; @@ -238,6 +311,26 @@ void HandleMessage(string raw) case "match.vote_vetoed": OnVoteVetoed?.Invoke(raw); break; + case "world.tick": + OnWorldTick?.Invoke(raw); + break; + case "world.joined": + OnWorldJoined?.Invoke(raw); + break; + case "world.left": + OnWorldLeft?.Invoke(raw); + break; + case "dm.message": + OnDmMessage?.Invoke(raw); + break; + case "dm.sent": + OnDmSent?.Invoke(raw); + break; + case "presence.updated": + OnPresenceUpdated?.Invoke(raw); + break; + case "session.heartbeat": + break; case "error": OnError?.Invoke(raw); break; @@ -247,6 +340,11 @@ void HandleMessage(string raw) var eventName = msg.type.Substring(6); OnMatchEvent?.Invoke(eventName, raw); } + else if (msg.type != null && msg.type.StartsWith("world.")) + { + var eventName = msg.type.Substring(6); + OnWorldEvent?.Invoke(eventName, raw); + } break; } }