From 99019963caf7a51b5c2a691febfc61d532c4a27a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 27 Jun 2026 12:01:27 -0400 Subject: [PATCH] feat(guild): controller scaffolding + /guild/info baseline - 5 common DTOs under Models/Dtos/Common/Guild/ (GuildDetailDto, GuildUserBaseDto, GuildMemberInfoDto, ChatUserDto, ChatMessageDto) - 28 per-endpoint request/response DTOs under Models/Dtos/Guild/ and Models/Dtos/GuildChat/ with full [MessagePackObject]/[Key]/ [JsonPropertyName] attribute mirrors on every property - GuildController (22 actions): /guild/info returns prod-shape with config-backed max_member_num/max_sub_leader_num/usable_stamp_list; 21 stubs return empty/default DTOs - GuildChatController (7 stubs), /guild_chat/messages stub returns wait_interval=10 per config - 29 new routing smoke test cases (100 total, all pass) - GuildWireShape literal-JSON test for GuildInfoResponse (1/1 pass) - GameConfigurationJsonbTests updated for 17th GuildConfig section - Total: 1422 tests pass, 0 errors Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/GuildChatController.cs | 50 +++++++ .../Controllers/GuildController.cs | 129 ++++++++++++++++++ .../Dtos/Common/Guild/ChatMessageDto.cs | 54 ++++++++ .../Models/Dtos/Common/Guild/ChatUserDto.cs | 39 ++++++ .../Dtos/Common/Guild/GuildDetailDto.cs | 41 ++++++ .../Dtos/Common/Guild/GuildMemberInfoDto.cs | 52 +++++++ .../Dtos/Common/Guild/GuildUserBaseDto.cs | 40 ++++++ .../Dtos/Guild/GuildCancelInviteRequest.cs | 22 +++ .../Guild/GuildCancelJoinRequestRequest.cs | 22 +++ .../Dtos/Guild/GuildChangeRoleRequest.cs | 26 ++++ .../Dtos/Guild/GuildChangeRoleResponse.cs | 16 +++ .../Models/Dtos/Guild/GuildCreateRequest.cs | 29 ++++ .../Dtos/Guild/GuildEmblemListResponse.cs | 21 +++ .../Dtos/Guild/GuildFriendListResponse.cs | 53 +++++++ .../Models/Dtos/Guild/GuildInfoResponse.cs | 51 +++++++ .../Models/Dtos/Guild/GuildInviteRequest.cs | 26 ++++ .../Dtos/Guild/GuildInviteUserListResponse.cs | 56 ++++++++ .../Guild/GuildInvitedGuildListRequest.cs | 22 +++ .../Guild/GuildInvitedGuildListResponse.cs | 57 ++++++++ .../Dtos/Guild/GuildJoinEndpointRequest.cs | 30 ++++ .../Guild/GuildJoinRequestAcceptRequest.cs | 25 ++++ .../Dtos/Guild/GuildJoinRequestListRequest.cs | 27 ++++ .../Guild/GuildJoinRequestListResponse.cs | 52 +++++++ .../Models/Dtos/Guild/GuildJoinResponse.cs | 18 +++ .../Dtos/Guild/GuildOthersInfoRequest.cs | 22 +++ .../Dtos/Guild/GuildOthersInfoResponse.cs | 15 ++ .../Dtos/Guild/GuildRejectInviteRequest.cs | 22 +++ .../Guild/GuildRejectJoinRequestRequest.cs | 25 ++++ .../Models/Dtos/Guild/GuildRemoveRequest.cs | 22 +++ .../Dtos/Guild/GuildSearchGuildRequest.cs | 34 +++++ .../Dtos/Guild/GuildSearchGuildResponse.cs | 13 ++ .../Guild/GuildUpdateDescriptionRequest.cs | 21 +++ .../Dtos/Guild/GuildUpdateEmblemRequest.cs | 23 ++++ .../Models/Dtos/Guild/GuildUpdateRequest.cs | 29 ++++ .../Models/Dtos/Guild/GuildUpdateResponse.cs | 23 ++++ .../Dtos/GuildChat/GuildChatAddDeckRequest.cs | 26 ++++ .../GuildChat/GuildChatAddReplayRequest.cs | 22 +++ .../Dtos/GuildChat/GuildChatDeckLogRequest.cs | 18 +++ .../GuildChat/GuildChatDeckLogResponse.cs | 26 ++++ .../GuildChat/GuildChatDeleteDeckRequest.cs | 26 ++++ .../GuildChat/GuildChatDeleteDeckResponse.cs | 25 ++++ .../GuildChat/GuildChatMessagesRequest.cs | 30 ++++ .../GuildChat/GuildChatMessagesResponse.cs | 31 +++++ .../Dtos/GuildChat/GuildChatPostRequest.cs | 26 ++++ .../Dtos/GuildChat/GuildChatPostResponse.cs | 22 +++ .../GuildChat/GuildChatReplayDetailRequest.cs | 22 +++ .../GuildChatReplayDetailResponse.cs | 19 +++ .../Models/GameConfigurationJsonbTests.cs | 7 +- SVSim.UnitTests/RoutingSmokeTests.cs | 32 +++++ SVSim.UnitTests/Wire/GuildWireShape.cs | 40 ++++++ 50 files changed, 1576 insertions(+), 3 deletions(-) create mode 100644 SVSim.EmulatedEntrypoint/Controllers/GuildChatController.cs create mode 100644 SVSim.EmulatedEntrypoint/Controllers/GuildController.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/ChatMessageDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/ChatUserDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildDetailDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildMemberInfoDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildUserBaseDto.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCancelInviteRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCancelJoinRequestRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildChangeRoleRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildChangeRoleResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCreateRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildEmblemListResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildFriendListResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInfoResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInviteRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInviteUserListResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInvitedGuildListRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInvitedGuildListResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinEndpointRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestAcceptRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestListRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestListResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildOthersInfoRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildOthersInfoResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRejectInviteRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRejectJoinRequestRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRemoveRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildSearchGuildRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildSearchGuildResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateDescriptionRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateEmblemRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatAddDeckRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatAddReplayRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeckLogRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeckLogResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeleteDeckRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeleteDeckResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatMessagesRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatMessagesResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatPostRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatPostResponse.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailRequest.cs create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailResponse.cs create mode 100644 SVSim.UnitTests/Wire/GuildWireShape.cs diff --git a/SVSim.EmulatedEntrypoint/Controllers/GuildChatController.cs b/SVSim.EmulatedEntrypoint/Controllers/GuildChatController.cs new file mode 100644 index 00000000..1d0b7db4 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Controllers/GuildChatController.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Mvc; +using SVSim.Database.Services.Guild; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; +using SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; + +namespace SVSim.EmulatedEntrypoint.Controllers; + +/// /guild_chat/* — 7 endpoints. See docs/api-spec/endpoints/post-login/guild_chat-*.md. +[Route("guild_chat")] +public sealed class GuildChatController : SVSimController +{ + private readonly IGuildChatService _chat; + + public GuildChatController(IGuildChatService chat) => _chat = chat; + + [HttpPost("messages")] + public Task> Messages([FromBody] GuildChatMessagesRequest req, CancellationToken ct) + => Task.FromResult>(new GuildChatMessagesResponse + { + MaintenanceCardList = new(), + Users = new(), + ChatMessage = new(), + WaitInterval = 10, + }); + + [HttpPost("post")] + public Task> Post([FromBody] GuildChatPostRequest req, CancellationToken ct) + => Task.FromResult>(new GuildChatPostResponse()); + + [HttpPost("add_deck")] + public Task> AddDeck([FromBody] GuildChatAddDeckRequest req, CancellationToken ct) + => Task.FromResult>(new EmptyResponse()); + + [HttpPost("delete_deck")] + public Task> DeleteDeck([FromBody] GuildChatDeleteDeckRequest req, CancellationToken ct) + => Task.FromResult>(new EmptyResponse()); + + [HttpPost("add_replay")] + public Task> AddReplay([FromBody] GuildChatAddReplayRequest req, CancellationToken ct) + => Task.FromResult>(new EmptyResponse()); + + [HttpPost("replay_detail")] + public Task> ReplayDetail([FromBody] GuildChatReplayDetailRequest req, CancellationToken ct) + => Task.FromResult>(new GuildChatReplayDetailResponse()); + + [HttpPost("deck_log")] + public Task> DeckLog([FromBody] GuildChatDeckLogRequest req, CancellationToken ct) + => Task.FromResult>(new GuildChatDeckLogResponse()); +} diff --git a/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs b/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs new file mode 100644 index 00000000..60fc3a2e --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Controllers/GuildController.cs @@ -0,0 +1,129 @@ +using Microsoft.AspNetCore.Mvc; +using SVSim.Database.Models.Config; +using SVSim.Database.Services; +using SVSim.Database.Services.Guild; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; +using SVSim.EmulatedEntrypoint.Models.Dtos.Guild; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; + +namespace SVSim.EmulatedEntrypoint.Controllers; + +/// /guild/* — 22 endpoints. See docs/api-spec/endpoints/post-login/guild-*.md. +[Route("guild")] +public sealed class GuildController : SVSimController +{ + private readonly IGuildService _guild; + private readonly IGameConfigService _configs; + + public GuildController(IGuildService guild, IGameConfigService configs) + { + _guild = guild; + _configs = configs; + } + + [HttpPost("info")] + public async Task> Info([FromBody] BaseRequest _, CancellationToken ct) + { + if (!TryGetViewerId(out var viewerId)) return Unauthorized(); + var cfg = _configs.Get(); + var view = await _guild.GetMyGuildAsync(viewerId, ct); + var resp = new GuildInfoResponse + { + MaxMemberNum = cfg.MaxMemberNum, + MaxSubLeaderNum = cfg.MaxSubLeaderNum, + UsableStampList = cfg.UsableStampList.ConvertAll(i => i.ToString()), + }; + if (view is null) + { + resp.GuildStatus = 0; // NOT_JOINING + } + else + { + resp.GuildStatus = 2; // JOINING — fully populated path lands in Task 7 + // Task 7 populates `Guild`, JoinRequestCount, InviteCount. + } + return resp; + } + + // ===== 21 remaining stubs — each returns the response DTO with defaults ===== + + [HttpPost("create")] + public Task> Create([FromBody] GuildCreateRequest req, CancellationToken ct) => Stub(); + + [HttpPost("breakup")] + public Task> Breakup([FromBody] BaseRequest _, CancellationToken ct) => Stub(); + + [HttpPost("update")] + public Task> Update([FromBody] GuildUpdateRequest req, CancellationToken ct) + => Task.FromResult>(new GuildUpdateResponse()); + + [HttpPost("update_description")] + public Task> UpdateDescription([FromBody] GuildUpdateDescriptionRequest req, CancellationToken ct) => Stub(); + + [HttpPost("update_emblem")] + public Task> UpdateEmblem([FromBody] GuildUpdateEmblemRequest req, CancellationToken ct) + => Task.FromResult>(new GuildUpdateResponse()); + + [HttpPost("search_guild")] + public Task> SearchGuild([FromBody] GuildSearchGuildRequest req, CancellationToken ct) + => Task.FromResult>(new GuildSearchGuildResponse { List = new() }); + + [HttpPost("emblem_list")] + public Task> EmblemList([FromBody] BaseRequest _, CancellationToken ct) + => Task.FromResult>(new GuildEmblemListResponse { EmblemList = new() }); + + [HttpPost("others_info")] + public Task> OthersInfo([FromBody] GuildOthersInfoRequest req, CancellationToken ct) + => Task.FromResult>(new GuildOthersInfoResponse()); + + [HttpPost("friend_list")] + public Task> FriendList([FromBody] BaseRequest _, CancellationToken ct) + => Task.FromResult>(new GuildFriendListResponse { Friends = new() }); + + [HttpPost("invite_user_list")] + public Task> InviteUserList([FromBody] BaseRequest _, CancellationToken ct) + => Task.FromResult>(new GuildInviteUserListResponse { Users = new() }); + + [HttpPost("invited_guild_list")] + public Task> InvitedGuildList([FromBody] GuildInvitedGuildListRequest req, CancellationToken ct) + => Task.FromResult>(new GuildInvitedGuildListResponse { List = new() }); + + [HttpPost("invite")] + public Task> Invite([FromBody] GuildInviteRequest req, CancellationToken ct) => Stub(); + + [HttpPost("cancel_invite")] + public Task> CancelInvite([FromBody] GuildCancelInviteRequest req, CancellationToken ct) => Stub(); + + [HttpPost("reject_invite")] + public Task> RejectInvite([FromBody] GuildRejectInviteRequest req, CancellationToken ct) => Stub(); + + [HttpPost("join")] + public Task> Join([FromBody] GuildJoinEndpointRequest req, CancellationToken ct) + => Task.FromResult>(new GuildJoinResponse { GuildStatus = 0 }); + + [HttpPost("cancel_join_request")] + public Task> CancelJoinRequest([FromBody] GuildCancelJoinRequestRequest req, CancellationToken ct) => Stub(); + + [HttpPost("join_request_list")] + public Task> JoinRequestList([FromBody] GuildJoinRequestListRequest req, CancellationToken ct) + => Task.FromResult>(new GuildJoinRequestListResponse { Users = new() }); + + [HttpPost("join_request_accept")] + public Task> JoinRequestAccept([FromBody] GuildJoinRequestAcceptRequest req, CancellationToken ct) => Stub(); + + [HttpPost("reject_join_request")] + public Task> RejectJoinRequest([FromBody] GuildRejectJoinRequestRequest req, CancellationToken ct) => Stub(); + + [HttpPost("leave")] + public Task> Leave([FromBody] BaseRequest _, CancellationToken ct) => Stub(); + + [HttpPost("remove")] + public Task> Remove([FromBody] GuildRemoveRequest req, CancellationToken ct) => Stub(); + + [HttpPost("change_role")] + public Task> ChangeRole([FromBody] GuildChangeRoleRequest req, CancellationToken ct) + => Task.FromResult>(new GuildChangeRoleResponse { Members = new() }); + + private static Task> Stub() => + Task.FromResult>(new EmptyResponse()); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/ChatMessageDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/ChatMessageDto.cs new file mode 100644 index 00000000..325e25e1 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/ChatMessageDto.cs @@ -0,0 +1,54 @@ +using MessagePack; +using System.Text.Json; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +/// +/// Wire shape for a single chat message in /guild_chat/messages `chat_message[]` +/// and /gathering_chat/messages. Matches guild_chat-messages.md ChatMessage interface. +/// +/// deck / replay / room are JsonElement? so arbitrary payloads can pass through in Phase 5 +/// without premature typing (TODO task-17: replace with typed DTOs when replay subsystem is documented). +/// +[MessagePackObject] +public class ChatMessageDto +{ + [JsonPropertyName("viewer_id"), Key("viewer_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long ViewerId { get; set; } + + [JsonPropertyName("message_id"), Key("message_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long MessageId { get; set; } + + /// eMessageType: 0=NORMAL 1=STAMP 2=DECK 3=JOIN 4=LEAVE 5=REPLAY etc. + [JsonPropertyName("message_type"), Key("message_type"), JsonConverter(typeof(StringifiedIntConverter))] + public int MessageType { get; set; } + + [JsonPropertyName("create_time"), Key("create_time"), JsonConverter(typeof(StringifiedLongConverter))] + public long CreateTime { get; set; } + + /// Text body for NORMAL, stringified stamp id for STAMP, etc. + [JsonPropertyName("message"), Key("message")] + public string Message { get; set; } = ""; + + /// Present when message_type = DECK (2). Inline DeckLogData payload. + [JsonPropertyName("deck"), Key("deck")] + public JsonElement? Deck { get; set; } + + /// Present when message_type = REPLAY (5). Inline ReplayInfoItem payload. + [JsonPropertyName("replay"), Key("replay")] + public JsonElement? Replay { get; set; } + + /// Present when message_type = ROOM_MATCH (10). Inline room-invite payload. + [JsonPropertyName("room"), Key("room")] + public JsonElement? Room { get; set; } + + /// GATHERING_TOURNAMENT_ROOM extras — only meaningful for /gathering_chat. + [JsonPropertyName("viewer_id1"), Key("viewer_id1")] + public long? ViewerId1 { get; set; } + + /// GATHERING_TOURNAMENT_ROOM extras — only meaningful for /gathering_chat. + [JsonPropertyName("viewer_id2"), Key("viewer_id2")] + public long? ViewerId2 { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/ChatUserDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/ChatUserDto.cs new file mode 100644 index 00000000..822fcd12 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/ChatUserDto.cs @@ -0,0 +1,39 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +/// +/// Generic UserInfoBase shape for chat contexts — used in /guild_chat/messages `users[]` +/// and shared with /gathering_chat. Matches types.ts.md#user-profile-types ChatUser (UserInfoBase). +/// +[MessagePackObject] +public class ChatUserDto +{ + [JsonPropertyName("viewer_id"), Key("viewer_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long ViewerId { get; set; } + + [JsonPropertyName("name"), Key("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("emblem_id"), Key("emblem_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long EmblemId { get; set; } + + [JsonPropertyName("country_code"), Key("country_code")] + public string CountryCode { get; set; } = ""; + + [JsonPropertyName("rank"), Key("rank"), JsonConverter(typeof(StringifiedIntConverter))] + public int Rank { get; set; } + + [JsonPropertyName("degree_id"), Key("degree_id"), JsonConverter(typeof(StringifiedIntConverter))] + public int DegreeId { get; set; } + + /// Optional — omit when the viewer has no friend relationship with this user. + [JsonPropertyName("is_friend"), Key("is_friend")] + public int? IsFriend { get; set; } + + /// Optional — omit when no pending friend apply. + [JsonPropertyName("is_friend_apply"), Key("is_friend_apply")] + public int? IsFriendApply { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildDetailDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildDetailDto.cs new file mode 100644 index 00000000..e223837e --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildDetailDto.cs @@ -0,0 +1,41 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +/// +/// Mirrors GuildDetailInfo.cs:143-156. Wire ints are emitted as stringified. +/// member_num is included on the wire only when this dto represents a search-result row +/// (the /guild/info "detail" inside `guild` object also includes it). +/// +[MessagePackObject] +public class GuildDetailDto +{ + [JsonPropertyName("guild_id"), Key("guild_id"), JsonConverter(typeof(StringifiedIntConverter))] + public int GuildId { get; set; } + + [JsonPropertyName("guild_name"), Key("guild_name")] + public string GuildName { get; set; } = ""; + + [JsonPropertyName("description"), Key("description")] + public string Description { get; set; } = ""; + + [JsonPropertyName("guild_emblem_id"), Key("guild_emblem_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long GuildEmblemId { get; set; } + + [JsonPropertyName("join_condition"), Key("join_condition"), JsonConverter(typeof(StringifiedIntConverter))] + public int JoinCondition { get; set; } + + [JsonPropertyName("activity"), Key("activity"), JsonConverter(typeof(StringifiedIntConverter))] + public int Activity { get; set; } + + [JsonPropertyName("member_num"), Key("member_num"), JsonConverter(typeof(StringifiedIntConverter))] + public int MemberNum { get; set; } + + [JsonPropertyName("leader_name"), Key("leader_name")] + public string LeaderName { get; set; } = ""; + + [JsonPropertyName("leader_viewer_id"), Key("leader_viewer_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long LeaderViewerId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildMemberInfoDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildMemberInfoDto.cs new file mode 100644 index 00000000..61d81418 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildMemberInfoDto.cs @@ -0,0 +1,52 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +/// +/// GuildMember wire shape — extends UserInfoBase (flat-spread) with is_official_mark_displayed + role. +/// Matches GuildMemberInfo.cs:43-50 and types.ts.md#guild-types GuildMember. +/// role: 0 = REGULAR, 1 = LEADER, 2 = SUB_LEADER. +/// +[MessagePackObject] +public class GuildMemberInfoDto +{ + [JsonPropertyName("viewer_id"), Key("viewer_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long ViewerId { get; set; } + + [JsonPropertyName("name"), Key("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("emblem_id"), Key("emblem_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long EmblemId { get; set; } + + [JsonPropertyName("country_code"), Key("country_code")] + public string CountryCode { get; set; } = ""; + + [JsonPropertyName("rank"), Key("rank"), JsonConverter(typeof(StringifiedIntConverter))] + public int Rank { get; set; } + + [JsonPropertyName("degree_id"), Key("degree_id"), JsonConverter(typeof(StringifiedIntConverter))] + public int DegreeId { get; set; } + + /// Optional — omit when the caller has no friend relationship with this member. + [JsonPropertyName("is_friend"), Key("is_friend")] + public int? IsFriend { get; set; } + + /// Optional — omit when no pending friend apply. + [JsonPropertyName("is_friend_apply"), Key("is_friend_apply")] + public int? IsFriendApply { get; set; } + + /// + /// Whether the official-account badge is shown for this member. + /// Client reads this unconditionally from GuildMemberInfo.Parse — always emit it. + /// + [JsonPropertyName("is_official_mark_displayed"), Key("is_official_mark_displayed"), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public int IsOfficialMarkDisplayed { get; set; } + + /// 0 = REGULAR, 1 = LEADER, 2 = SUB_LEADER. Stringified. + [JsonPropertyName("role"), Key("role"), JsonConverter(typeof(StringifiedIntConverter))] + public int Role { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildUserBaseDto.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildUserBaseDto.cs new file mode 100644 index 00000000..fe8cff04 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/Guild/GuildUserBaseDto.cs @@ -0,0 +1,40 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +/// +/// UserInfoBase flat-spread fields as used in guild contexts (GuildMemberInfo.cs, invite lists, etc.). +/// viewer_id / name / emblem_id / country_code / rank / degree_id / is_friend? / is_friend_apply?. +/// Matches types.ts.md#user-profile-types UserInfoBase. +/// +[MessagePackObject] +public class GuildUserBaseDto +{ + [JsonPropertyName("viewer_id"), Key("viewer_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long ViewerId { get; set; } + + [JsonPropertyName("name"), Key("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("emblem_id"), Key("emblem_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long EmblemId { get; set; } + + [JsonPropertyName("country_code"), Key("country_code")] + public string CountryCode { get; set; } = ""; + + [JsonPropertyName("rank"), Key("rank"), JsonConverter(typeof(StringifiedIntConverter))] + public int Rank { get; set; } + + [JsonPropertyName("degree_id"), Key("degree_id"), JsonConverter(typeof(StringifiedIntConverter))] + public int DegreeId { get; set; } + + /// Optional — omit when the caller has no friend relationship with this user. + [JsonPropertyName("is_friend"), Key("is_friend")] + public int? IsFriend { get; set; } + + /// Optional — omit when the caller has no pending friend apply to this user. + [JsonPropertyName("is_friend_apply"), Key("is_friend_apply")] + public int? IsFriendApply { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCancelInviteRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCancelInviteRequest.cs new file mode 100644 index 00000000..92515a9a --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCancelInviteRequest.cs @@ -0,0 +1,22 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/cancel_invite. Leader/sub-leader retracts an outstanding invite. +[MessagePackObject] +public class GuildCancelInviteRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// Invite id from /guild/invite_user_list. + [JsonPropertyName("invite_id"), Key("invite_id")] + public long InviteId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCancelJoinRequestRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCancelJoinRequestRequest.cs new file mode 100644 index 00000000..1d78894c --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCancelJoinRequestRequest.cs @@ -0,0 +1,22 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Request for POST /guild/cancel_join_request. +/// No fields — server identifies the pending request from the calling user's state. +/// Uses explicit DTO (not BaseRequest) so we can add fields if needed in later tasks. +/// +[MessagePackObject] +public class GuildCancelJoinRequestRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildChangeRoleRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildChangeRoleRequest.cs new file mode 100644 index 00000000..2441b979 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildChangeRoleRequest.cs @@ -0,0 +1,26 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/change_role. Change a member's role (promote/demote/transfer leadership). +[MessagePackObject] +public class GuildChangeRoleRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// ViewerId of the member whose role is changing. + [JsonPropertyName("target_viewer_id"), Key("target_viewer_id")] + public long TargetViewerId { get; set; } + + /// New role: 0 = REGULAR, 1 = LEADER, 2 = SUB_LEADER. + [JsonPropertyName("role_id"), Key("role_id")] + public int RoleId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildChangeRoleResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildChangeRoleResponse.cs new file mode 100644 index 00000000..809d4468 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildChangeRoleResponse.cs @@ -0,0 +1,16 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Response for POST /guild/change_role. +/// Returns the full updated member list so the client can redraw without re-fetching /guild/info. +/// +[MessagePackObject] +public class GuildChangeRoleResponse +{ + [JsonPropertyName("members"), Key("members")] + public List Members { get; set; } = new(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCreateRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCreateRequest.cs new file mode 100644 index 00000000..7299ce37 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildCreateRequest.cs @@ -0,0 +1,29 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/create. +[MessagePackObject] +public class GuildCreateRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + [JsonPropertyName("guild_name"), Key("guild_name")] + public string GuildName { get; set; } = ""; + + /// GuildDetailInfo.ActivityType enum (1..16). + [JsonPropertyName("activity"), Key("activity")] + public int Activity { get; set; } + + /// GuildDetailInfo.JoinConditionType enum (1 FREE, 2 APPROVAL, 3 ONLY_INVITE). + [JsonPropertyName("join_condition"), Key("join_condition")] + public int JoinCondition { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildEmblemListResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildEmblemListResponse.cs new file mode 100644 index 00000000..044db11c --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildEmblemListResponse.cs @@ -0,0 +1,21 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Response for POST /guild/emblem_list. List of emblem entries the leader can select. +[MessagePackObject] +public class GuildEmblemListResponse +{ + [JsonPropertyName("guild_emblem_list"), Key("guild_emblem_list")] + public List EmblemList { get; set; } = new(); +} + +/// One emblem entry. emblem_id is long per spec. +[MessagePackObject] +public class GuildEmblemEntry +{ + [JsonPropertyName("emblem_id"), Key("emblem_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long EmblemId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildFriendListResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildFriendListResponse.cs new file mode 100644 index 00000000..aa8b1bf3 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildFriendListResponse.cs @@ -0,0 +1,53 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Response for POST /guild/friend_list. +/// NOTE: per guild-friend_list.md, `data` is an array at the root — BUT we return this +/// as a wrapper for now; the controller returns the List directly. +/// TODO(task-14): verify array-root shape via capture; may need a custom response wrapper. +/// +[MessagePackObject] +public class GuildFriendListResponse +{ + // Placeholder — see controller; actual wire shape is array-at-root per spec. + // The controller returns List directly for now. + [JsonPropertyName("friends"), Key("friends")] + public List Friends { get; set; } = new(); +} + +/// InviteCandidate — UserInfoBase + is_join_guild flag. +[MessagePackObject] +public class GuildInviteCandidateDto +{ + [JsonPropertyName("viewer_id"), Key("viewer_id"), JsonConverter(typeof(SVSim.EmulatedEntrypoint.Models.Dtos.Common.StringifiedLongConverter))] + public long ViewerId { get; set; } + + [JsonPropertyName("name"), Key("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("emblem_id"), Key("emblem_id"), JsonConverter(typeof(SVSim.EmulatedEntrypoint.Models.Dtos.Common.StringifiedLongConverter))] + public long EmblemId { get; set; } + + [JsonPropertyName("country_code"), Key("country_code")] + public string CountryCode { get; set; } = ""; + + [JsonPropertyName("rank"), Key("rank"), JsonConverter(typeof(SVSim.EmulatedEntrypoint.Models.Dtos.Common.StringifiedIntConverter))] + public int Rank { get; set; } + + [JsonPropertyName("degree_id"), Key("degree_id"), JsonConverter(typeof(SVSim.EmulatedEntrypoint.Models.Dtos.Common.StringifiedIntConverter))] + public int DegreeId { get; set; } + + [JsonPropertyName("is_friend"), Key("is_friend")] + public int? IsFriend { get; set; } + + [JsonPropertyName("is_friend_apply"), Key("is_friend_apply")] + public int? IsFriendApply { get; set; } + + /// true => friend is already in a guild and can't be invited. + [JsonPropertyName("is_join_guild"), Key("is_join_guild")] + public bool IsJoinGuild { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInfoResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInfoResponse.cs new file mode 100644 index 00000000..05098e0b --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInfoResponse.cs @@ -0,0 +1,51 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Mirrors GuildInfo.cs:68-86. Always-present: max_member_num, max_sub_leader_num, +/// guild_status, usable_stamp_list. Conditionally present (client `Keys.Contains` -gated): +/// guild (detail + members), join_request_count, invite_count. +/// +[MessagePackObject] +public class GuildInfoResponse +{ + [JsonPropertyName("max_member_num"), Key("max_member_num"), JsonConverter(typeof(StringifiedIntConverter)), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public int MaxMemberNum { get; set; } + + [JsonPropertyName("max_sub_leader_num"), Key("max_sub_leader_num"), JsonConverter(typeof(StringifiedIntConverter)), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public int MaxSubLeaderNum { get; set; } + + [JsonPropertyName("guild_status"), Key("guild_status"), JsonConverter(typeof(StringifiedIntConverter)), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public int GuildStatus { get; set; } + + /// Stamp ids the viewer can use in chat. Stringified ints (capture: ["100001",...]). + [JsonPropertyName("usable_stamp_list"), Key("usable_stamp_list"), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List UsableStampList { get; set; } = new(); + + [JsonPropertyName("guild"), Key("guild")] + public GuildBundle? Guild { get; set; } + + [JsonPropertyName("join_request_count"), Key("join_request_count"), JsonConverter(typeof(StringifiedIntConverter))] + public int? JoinRequestCount { get; set; } + + [JsonPropertyName("invite_count"), Key("invite_count"), JsonConverter(typeof(StringifiedIntConverter))] + public int? InviteCount { get; set; } +} + +[MessagePackObject] +public class GuildBundle +{ + [JsonPropertyName("detail"), Key("detail")] + public GuildDetailDto Detail { get; set; } = new(); + + [JsonPropertyName("members"), Key("members")] + public List Members { get; set; } = new(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInviteRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInviteRequest.cs new file mode 100644 index 00000000..5116e060 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInviteRequest.cs @@ -0,0 +1,26 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/invite. Leader/sub-leader sends invite to a user. +[MessagePackObject] +public class GuildInviteRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// ViewerId of the user to invite. + [JsonPropertyName("invited_viewer_id"), Key("invited_viewer_id")] + public long InvitedViewerId { get; set; } + + /// Free-text invite message. Client hardcodes "" — always empty on wire. + [JsonPropertyName("invite_message"), Key("invite_message")] + public string InviteMessage { get; set; } = ""; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInviteUserListResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInviteUserListResponse.cs new file mode 100644 index 00000000..e09a5390 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInviteUserListResponse.cs @@ -0,0 +1,56 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Response for POST /guild/invite_user_list. +/// Lists outstanding invites the guild has sent out (leader/sub-leader view). +/// +[MessagePackObject] +public class GuildInviteUserListResponse +{ + [JsonPropertyName("list"), Key("list")] + public List Users { get; set; } = new(); +} + +/// +/// OutgoingInvite — UserInfoBase fields flat-spread + invite_id + invite_time. +/// Matches guild-invite_user_list.md OutgoingInvite interface. +/// +[MessagePackObject] +public class GuildOutgoingInviteDto +{ + [JsonPropertyName("viewer_id"), Key("viewer_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long ViewerId { get; set; } + + [JsonPropertyName("name"), Key("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("emblem_id"), Key("emblem_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long EmblemId { get; set; } + + [JsonPropertyName("country_code"), Key("country_code")] + public string CountryCode { get; set; } = ""; + + [JsonPropertyName("rank"), Key("rank"), JsonConverter(typeof(StringifiedIntConverter))] + public int Rank { get; set; } + + [JsonPropertyName("degree_id"), Key("degree_id"), JsonConverter(typeof(StringifiedIntConverter))] + public int DegreeId { get; set; } + + [JsonPropertyName("is_friend"), Key("is_friend")] + public int? IsFriend { get; set; } + + [JsonPropertyName("is_friend_apply"), Key("is_friend_apply")] + public int? IsFriendApply { get; set; } + + /// Invite id — pass to /guild/cancel_invite to retract. + [JsonPropertyName("invite_id"), Key("invite_id")] + public long InviteId { get; set; } + + /// Unix seconds when the invite was sent. + [JsonPropertyName("invite_time"), Key("invite_time")] + public long InviteTime { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInvitedGuildListRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInvitedGuildListRequest.cs new file mode 100644 index 00000000..389ff882 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInvitedGuildListRequest.cs @@ -0,0 +1,22 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/invited_guild_list. Lists pending invites the user has received. +[MessagePackObject] +public class GuildInvitedGuildListRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// Pagination page index. This list is the only one the client actually scrolls. + [JsonPropertyName("page"), Key("page")] + public int Page { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInvitedGuildListResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInvitedGuildListResponse.cs new file mode 100644 index 00000000..d3187548 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildInvitedGuildListResponse.cs @@ -0,0 +1,57 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Response for POST /guild/invited_guild_list. +/// Each entry is GuildDetail fields + invite_id as flat siblings on the wire. +/// Per spec: client constructs GuildDetailInfo + reads invite_id from the SAME object. +/// +[MessagePackObject] +public class GuildInvitedGuildListResponse +{ + [JsonPropertyName("list"), Key("list")] + public List List { get; set; } = new(); +} + +/// +/// ReceivedInvite — GuildDetail fields flat-spread + invite_id. +/// invite_id and detail fields are siblings, not nested (per guild-invited_guild_list.md). +/// +[MessagePackObject] +public class GuildReceivedInviteDto +{ + [JsonPropertyName("guild_id"), Key("guild_id"), JsonConverter(typeof(StringifiedIntConverter))] + public int GuildId { get; set; } + + [JsonPropertyName("guild_name"), Key("guild_name")] + public string GuildName { get; set; } = ""; + + [JsonPropertyName("description"), Key("description")] + public string Description { get; set; } = ""; + + [JsonPropertyName("guild_emblem_id"), Key("guild_emblem_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long GuildEmblemId { get; set; } + + [JsonPropertyName("join_condition"), Key("join_condition"), JsonConverter(typeof(StringifiedIntConverter))] + public int JoinCondition { get; set; } + + [JsonPropertyName("activity"), Key("activity"), JsonConverter(typeof(StringifiedIntConverter))] + public int Activity { get; set; } + + [JsonPropertyName("member_num"), Key("member_num"), JsonConverter(typeof(StringifiedIntConverter))] + public int MemberNum { get; set; } + + [JsonPropertyName("leader_name"), Key("leader_name")] + public string LeaderName { get; set; } = ""; + + [JsonPropertyName("leader_viewer_id"), Key("leader_viewer_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long LeaderViewerId { get; set; } + + /// Invite id — pass to /guild/join (from_invite=true) or /guild/reject_invite. + [JsonPropertyName("invite_id"), Key("invite_id")] + public long InviteId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinEndpointRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinEndpointRequest.cs new file mode 100644 index 00000000..33bd2b62 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinEndpointRequest.cs @@ -0,0 +1,30 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Request for POST /guild/join. +/// NOTE: named GuildJoinEndpointRequest (not GuildJoinRequest) to avoid collision with +/// SVSim.Database.Entities.Guild.GuildJoinRequest which is an EF entity. +/// +[MessagePackObject] +public class GuildJoinEndpointRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + [JsonPropertyName("guild_id"), Key("guild_id"), JsonConverter(typeof(StringifiedIntConverter))] + public int GuildId { get; set; } + + /// true = consuming an open invite (from /guild/invited_guild_list). + [JsonPropertyName("from_invite"), Key("from_invite")] + public bool FromInvite { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestAcceptRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestAcceptRequest.cs new file mode 100644 index 00000000..e19ee5b6 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestAcceptRequest.cs @@ -0,0 +1,25 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Request for POST /guild/join_request_accept. +/// GuildJoinRequestActionParam shape — single field request_viewer_id. +/// +[MessagePackObject] +public class GuildJoinRequestAcceptRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// ViewerId of the user whose join request is being accepted. + [JsonPropertyName("request_viewer_id"), Key("request_viewer_id")] + public long RequestViewerId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestListRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestListRequest.cs new file mode 100644 index 00000000..3f280d0e --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestListRequest.cs @@ -0,0 +1,27 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Request for POST /guild/join_request_list. +/// KeysetPagination shape — client always sends page=0, oldest_time=0. +/// +[MessagePackObject] +public class GuildJoinRequestListRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + [JsonPropertyName("page"), Key("page")] + public int Page { get; set; } + + [JsonPropertyName("oldest_time"), Key("oldest_time")] + public long OldestTime { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestListResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestListResponse.cs new file mode 100644 index 00000000..33e2f572 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinRequestListResponse.cs @@ -0,0 +1,52 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Response for POST /guild/join_request_list (leader/sub-leader view). +[MessagePackObject] +public class GuildJoinRequestListResponse +{ + [JsonPropertyName("list"), Key("list")] + public List Users { get; set; } = new(); +} + +/// +/// JoinRequest entry — UserInfoBase fields flat-spread + is_official_mark_displayed + request_time. +/// Matches guild-join_request_list.md JoinRequest interface. +/// +[MessagePackObject] +public class GuildJoinRequestEntryDto +{ + [JsonPropertyName("viewer_id"), Key("viewer_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long ViewerId { get; set; } + + [JsonPropertyName("name"), Key("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("emblem_id"), Key("emblem_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long EmblemId { get; set; } + + [JsonPropertyName("country_code"), Key("country_code")] + public string CountryCode { get; set; } = ""; + + [JsonPropertyName("rank"), Key("rank"), JsonConverter(typeof(StringifiedIntConverter))] + public int Rank { get; set; } + + [JsonPropertyName("degree_id"), Key("degree_id"), JsonConverter(typeof(StringifiedIntConverter))] + public int DegreeId { get; set; } + + [JsonPropertyName("is_friend"), Key("is_friend")] + public int? IsFriend { get; set; } + + [JsonPropertyName("is_friend_apply"), Key("is_friend_apply")] + public int? IsFriendApply { get; set; } + + [JsonPropertyName("is_official_mark_displayed"), Key("is_official_mark_displayed")] + public int IsOfficialMarkDisplayed { get; set; } + + /// Unix seconds when the request was submitted. Don't send millisecond timestamps. + [JsonPropertyName("request_time"), Key("request_time")] + public long RequestTime { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinResponse.cs new file mode 100644 index 00000000..6012f930 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildJoinResponse.cs @@ -0,0 +1,18 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Response for POST /guild/join. +/// Returns the user's new guild_status; client calls /guild/info next when status is JOINING. +/// +[MessagePackObject] +public class GuildJoinResponse +{ + /// eGUILD_STATUS the user is now in. Stringified. + [JsonPropertyName("guild_status"), Key("guild_status"), JsonConverter(typeof(StringifiedIntConverter)), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public int GuildStatus { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildOthersInfoRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildOthersInfoRequest.cs new file mode 100644 index 00000000..afbfd58f --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildOthersInfoRequest.cs @@ -0,0 +1,22 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/others_info. Browse another guild's public detail. +[MessagePackObject] +public class GuildOthersInfoRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + [JsonPropertyName("guild_id"), Key("guild_id"), JsonConverter(typeof(StringifiedIntConverter))] + public int GuildId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildOthersInfoResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildOthersInfoResponse.cs new file mode 100644 index 00000000..7b2cc081 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildOthersInfoResponse.cs @@ -0,0 +1,15 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Response for POST /guild/others_info. Only the detail sub-tree — no member list (privacy). +/// Reuses GuildDetailSubTree (same shape as /guild/update response). +/// +[MessagePackObject] +public class GuildOthersInfoResponse +{ + [JsonPropertyName("guild"), Key("guild")] + public GuildDetailSubTree Guild { get; set; } = new(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRejectInviteRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRejectInviteRequest.cs new file mode 100644 index 00000000..8aa4112b --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRejectInviteRequest.cs @@ -0,0 +1,22 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/reject_invite. User declines an incoming invite. +[MessagePackObject] +public class GuildRejectInviteRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// Invite id from /guild/invited_guild_list. + [JsonPropertyName("invite_id"), Key("invite_id")] + public long InviteId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRejectJoinRequestRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRejectJoinRequestRequest.cs new file mode 100644 index 00000000..487b62cb --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRejectJoinRequestRequest.cs @@ -0,0 +1,25 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Request for POST /guild/reject_join_request. +/// GuildJoinRequestActionParam shape — single field request_viewer_id. +/// +[MessagePackObject] +public class GuildRejectJoinRequestRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// ViewerId of the user whose join request is being rejected. + [JsonPropertyName("request_viewer_id"), Key("request_viewer_id")] + public long RequestViewerId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRemoveRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRemoveRequest.cs new file mode 100644 index 00000000..61c98ee5 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildRemoveRequest.cs @@ -0,0 +1,22 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/remove. Kick a member (leader/sub-leader only). +[MessagePackObject] +public class GuildRemoveRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// ViewerId of the member to remove. Field is `remove_viewer_id`, not `viewer_id`. + [JsonPropertyName("remove_viewer_id"), Key("remove_viewer_id")] + public long RemoveViewerId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildSearchGuildRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildSearchGuildRequest.cs new file mode 100644 index 00000000..a3033f0a --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildSearchGuildRequest.cs @@ -0,0 +1,34 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/search_guild. +[MessagePackObject] +public class GuildSearchGuildRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// Name query. Empty string = match-all. + [JsonPropertyName("guild_name"), Key("guild_name")] + public string GuildName { get; set; } = ""; + + /// ActivityType filter (1..16). 0 = any. + [JsonPropertyName("activity"), Key("activity")] + public int Activity { get; set; } + + /// JoinConditionType filter (1..3). 0 = any. + [JsonPropertyName("join_condition"), Key("join_condition")] + public int JoinCondition { get; set; } + + /// Member-count bucket filter. 0 = any, 1 = small, 2 = medium, 3 = large. + [JsonPropertyName("member_condition_range"), Key("member_condition_range")] + public int MemberConditionRange { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildSearchGuildResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildSearchGuildResponse.cs new file mode 100644 index 00000000..5f4b3aa1 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildSearchGuildResponse.cs @@ -0,0 +1,13 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Response for POST /guild/search_guild. Flat list of guild details matching the filter. +[MessagePackObject] +public class GuildSearchGuildResponse +{ + [JsonPropertyName("list"), Key("list")] + public List List { get; set; } = new(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateDescriptionRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateDescriptionRequest.cs new file mode 100644 index 00000000..0e1fd203 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateDescriptionRequest.cs @@ -0,0 +1,21 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/update_description. Leader-only. +[MessagePackObject] +public class GuildUpdateDescriptionRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + [JsonPropertyName("description"), Key("description")] + public string Description { get; set; } = ""; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateEmblemRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateEmblemRequest.cs new file mode 100644 index 00000000..aac5272e --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateEmblemRequest.cs @@ -0,0 +1,23 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/update_emblem. Leader-only. emblem_id is long. +[MessagePackObject] +public class GuildUpdateEmblemRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// Emblem id from /guild/emblem_list. long per spec. + [JsonPropertyName("emblem_id"), Key("emblem_id"), JsonConverter(typeof(StringifiedLongConverter))] + public long EmblemId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateRequest.cs new file mode 100644 index 00000000..fdfe36f5 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateRequest.cs @@ -0,0 +1,29 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// Request for POST /guild/update. Leader-only. +[MessagePackObject] +public class GuildUpdateRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + [JsonPropertyName("guild_name"), Key("guild_name")] + public string GuildName { get; set; } = ""; + + /// ActivityType enum (1..16). + [JsonPropertyName("activity"), Key("activity")] + public int Activity { get; set; } + + /// JoinConditionType enum (1..3). + [JsonPropertyName("join_condition"), Key("join_condition")] + public int JoinCondition { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateResponse.cs new file mode 100644 index 00000000..9bcf4206 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Guild/GuildUpdateResponse.cs @@ -0,0 +1,23 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +/// +/// Response for POST /guild/update and POST /guild/update_emblem. +/// Returns the full updated guild detail sub-tree; client merges without re-fetching members. +/// +[MessagePackObject] +public class GuildUpdateResponse +{ + [JsonPropertyName("guild"), Key("guild")] + public GuildDetailSubTree Guild { get; set; } = new(); +} + +[MessagePackObject] +public class GuildDetailSubTree +{ + [JsonPropertyName("detail"), Key("detail")] + public GuildDetailDto Detail { get; set; } = new(); +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatAddDeckRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatAddDeckRequest.cs new file mode 100644 index 00000000..9007071a --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatAddDeckRequest.cs @@ -0,0 +1,26 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// Request for POST /guild_chat/add_deck. Shares a deck snapshot to guild chat. +[MessagePackObject] +public class GuildChatAddDeckRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// API-side Format value of the deck being shared. + [JsonPropertyName("deck_format"), Key("deck_format")] + public int DeckFormat { get; set; } + + /// Slot number of the deck being shared (within the user's regular deck slots). + [JsonPropertyName("deck_no"), Key("deck_no")] + public int DeckNo { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatAddReplayRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatAddReplayRequest.cs new file mode 100644 index 00000000..0b30615a --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatAddReplayRequest.cs @@ -0,0 +1,22 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// Request for POST /guild_chat/add_replay. Shares a battle replay to guild chat. +[MessagePackObject] +public class GuildChatAddReplayRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// Battle id of the replay being shared. long per spec. + [JsonPropertyName("battle_id"), Key("battle_id")] + public long BattleId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeckLogRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeckLogRequest.cs new file mode 100644 index 00000000..3ba345e5 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeckLogRequest.cs @@ -0,0 +1,18 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// Request for POST /guild_chat/deck_log. No fields — returns all shared decks by format. +[MessagePackObject] +public class GuildChatDeckLogRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeckLogResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeckLogResponse.cs new file mode 100644 index 00000000..ce113930 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeckLogResponse.cs @@ -0,0 +1,26 @@ +using MessagePack; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// +/// Response for POST /guild_chat/deck_log. +/// deck_log keys are stringified API-side Format ints (e.g. "1", "2"). +/// TODO(task-17): replace deck_log with typed DTOs when DeckLogData shape is fully specified. +/// Crossover/MyRotation keys must be OMITTED (not empty arrays) when those formats are disabled. +/// +[MessagePackObject] +public class GuildChatDeckLogResponse +{ + [JsonPropertyName("maintenance_card_list"), Key("maintenance_card_list"), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List MaintenanceCardList { get; set; } = new(); + + /// + /// Decks shared in chat, bucketed by API-side Format value (stringified int keys). + /// JsonElement pass-through for now — Task 17 will type the inner DeckLogEntry shape. + /// + [JsonPropertyName("deck_log"), Key("deck_log")] + public JsonElement? DeckLog { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeleteDeckRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeleteDeckRequest.cs new file mode 100644 index 00000000..16ced5dc --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeleteDeckRequest.cs @@ -0,0 +1,26 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// Request for POST /guild_chat/delete_deck. Deletes a previously-shared deck from the chat archive. +[MessagePackObject] +public class GuildChatDeleteDeckRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// API-side Format of the deck being deleted. + [JsonPropertyName("deck_format"), Key("deck_format")] + public int DeckFormat { get; set; } + + /// Message id of the chat message that originally shared the deck. + [JsonPropertyName("message_id"), Key("message_id")] + public long MessageId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeleteDeckResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeleteDeckResponse.cs new file mode 100644 index 00000000..23067075 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatDeleteDeckResponse.cs @@ -0,0 +1,25 @@ +using MessagePack; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// +/// Response for POST /guild_chat/delete_deck. +/// Returns refreshed deck_log after deletion. +/// TODO(task-17): replace deck_log with typed DeckLogData DTOs when deck-log shape is fully documented. +/// +[MessagePackObject] +public class GuildChatDeleteDeckResponse +{ + [JsonPropertyName("maintenance_card_list"), Key("maintenance_card_list"), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List MaintenanceCardList { get; set; } = new(); + + /// + /// Refreshed deck log keyed by API-side Format int (stringified, e.g. "1", "2"). + /// Using JsonElement for now; Task 17 will type this properly. + /// + [JsonPropertyName("deck_log"), Key("deck_log")] + public JsonElement? DeckLog { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatMessagesRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatMessagesRequest.cs new file mode 100644 index 00000000..57f410b0 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatMessagesRequest.cs @@ -0,0 +1,30 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// Request for POST /guild_chat/messages. Polling endpoint for guild chat. +[MessagePackObject] +public class GuildChatMessagesRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// Anchor message id. 0 = latest log head. + [JsonPropertyName("start_message_id"), Key("start_message_id")] + public long StartMessageId { get; set; } + + /// Chat.eRequestDirection: 1=OLD 2=NEW 3=BOTH. + [JsonPropertyName("direction"), Key("direction")] + public int Direction { get; set; } + + /// Client's current polling interval in seconds. + [JsonPropertyName("wait_interval"), Key("wait_interval")] + public int WaitInterval { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatMessagesResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatMessagesResponse.cs new file mode 100644 index 00000000..bfeabdff --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatMessagesResponse.cs @@ -0,0 +1,31 @@ +using MessagePack; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Guild; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// Response for POST /guild_chat/messages. +[MessagePackObject] +public class GuildChatMessagesResponse +{ + /// Card ids currently disabled for maintenance — client refreshes its global list. + [JsonPropertyName("maintenance_card_list"), Key("maintenance_card_list"), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List MaintenanceCardList { get; set; } = new(); + + /// Users referenced by messages in this batch — deduplicated catalog. + [JsonPropertyName("users"), Key("users"), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List Users { get; set; } = new(); + + /// Messages ordered oldest-to-newest. + [JsonPropertyName("chat_message"), Key("chat_message"), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public List ChatMessage { get; set; } = new(); + + /// Server-driven polling interval in seconds. Client uses this for the next poll. + [JsonPropertyName("wait_interval"), Key("wait_interval"), JsonConverter(typeof(StringifiedIntConverter)), + JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public int WaitInterval { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatPostRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatPostRequest.cs new file mode 100644 index 00000000..e4806758 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatPostRequest.cs @@ -0,0 +1,26 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// Request for POST /guild_chat/post. Posts a text message or stamp to guild chat. +[MessagePackObject] +public class GuildChatPostRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// eMessageType: 0=NORMAL (text body), 1=STAMP (stringified stamp id). + [JsonPropertyName("type"), Key("type")] + public int Type { get; set; } + + /// Text body for NORMAL; stringified stamp id for STAMP. + [JsonPropertyName("message"), Key("message")] + public string Message { get; set; } = ""; +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatPostResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatPostResponse.cs new file mode 100644 index 00000000..43322ff0 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatPostResponse.cs @@ -0,0 +1,22 @@ +using MessagePack; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// +/// Response for POST /guild_chat/post. +/// achieved_info and reward_list are optional; only present when posting triggered a mission completion. +/// TODO(task-16): populate achieved_info + reward_list shapes when chat missions are implemented. +/// +[MessagePackObject] +public class GuildChatPostResponse +{ + /// Optional. Present when posting triggered a mission completion. + [JsonPropertyName("achieved_info"), Key("achieved_info")] + public JsonElement? AchievedInfo { get; set; } + + /// Optional. Goods deltas from any rewards in achieved_info. + [JsonPropertyName("reward_list"), Key("reward_list")] + public JsonElement? RewardList { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailRequest.cs new file mode 100644 index 00000000..195ea188 --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailRequest.cs @@ -0,0 +1,22 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// Request for POST /guild_chat/replay_detail. Fetches full replay payload for a shared replay. +[MessagePackObject] +public class GuildChatReplayDetailRequest +{ + [JsonPropertyName("viewer_id"), Key("viewer_id")] + public string ViewerId { get; set; } = ""; + + [JsonPropertyName("steam_id"), Key("steam_id")] + public ulong SteamId { get; set; } + + [JsonPropertyName("steam_session_ticket"), Key("steam_session_ticket")] + public string SteamSessionTicket { get; set; } = ""; + + /// Message id of the REPLAY chat message. + [JsonPropertyName("message_id"), Key("message_id")] + public long MessageId { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailResponse.cs new file mode 100644 index 00000000..7e71e3dd --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/GuildChat/GuildChatReplayDetailResponse.cs @@ -0,0 +1,19 @@ +using MessagePack; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.GuildChat; + +/// +/// Response for POST /guild_chat/replay_detail. +/// Shape is opaque pending replay-subsystem documentation. +/// TODO(task-17): enumerate fields from Wizard/ReplayDetailInfo.cs when replay subsystem is documented. +/// +[MessagePackObject] +public class GuildChatReplayDetailResponse +{ + // Placeholder — replay detail shape is undocumented until we capture a live trace. + // Using JsonElement pass-through for now. + [JsonPropertyName("_todo"), Key("_todo")] + public JsonElement? Placeholder { get; set; } +} diff --git a/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs b/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs index 506ab09e..a855899d 100644 --- a/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs +++ b/SVSim.UnitTests/Models/GameConfigurationJsonbTests.cs @@ -25,14 +25,15 @@ public class GameConfigurationJsonbTests var rows = await db.GameConfigs.AsNoTracking().ToListAsync(); var byName = rows.ToDictionary(r => r.SectionName); - // One row per [ConfigSection]-marked POCO (16 sections today: Player, DefaultGrants, + // One row per [ConfigSection]-marked POCO (17 sections today: Player, DefaultGrants, // DefaultLoadout, Challenge, Rotation, PackRates, MyRotationSchedule, Story, ResourceConfig, - // Freeplay, ArenaTwoPick, Matching, CardMasterConfig, ColosseumSeason, ColosseumRounds, LoginBonus). + // Freeplay, ArenaTwoPick, Matching, CardMasterConfig, ColosseumSeason, ColosseumRounds, + // LoginBonus, Guild). Assert.That(byName.Keys, Is.EquivalentTo(new[] { "Player", "DefaultGrants", "DefaultLoadout", "Challenge", "Rotation", "PackRates", "MyRotationSchedule", "Story", "ResourceConfig", "Freeplay", "ArenaTwoPick", "Matching", - "CardMasterConfig", "ColosseumSeason", "ColosseumRounds", "LoginBonus", + "CardMasterConfig", "ColosseumSeason", "ColosseumRounds", "LoginBonus", "Guild", })); var resources = JsonSerializer.Deserialize(byName["ResourceConfig"].ValueJson)!; diff --git a/SVSim.UnitTests/RoutingSmokeTests.cs b/SVSim.UnitTests/RoutingSmokeTests.cs index 6abfb36a..101957fa 100644 --- a/SVSim.UnitTests/RoutingSmokeTests.cs +++ b/SVSim.UnitTests/RoutingSmokeTests.cs @@ -146,6 +146,38 @@ public class RoutingSmokeTests [TestCase("/rotation_free_battle/finish")] [TestCase("/unlimited_free_battle/finish")] [TestCase("/free_battle/force_finish")] + // Guild endpoints (Task 5) + [TestCase("/guild/info")] + [TestCase("/guild/create")] + [TestCase("/guild/breakup")] + [TestCase("/guild/update")] + [TestCase("/guild/update_description")] + [TestCase("/guild/update_emblem")] + [TestCase("/guild/search_guild")] + [TestCase("/guild/emblem_list")] + [TestCase("/guild/others_info")] + [TestCase("/guild/friend_list")] + [TestCase("/guild/invite_user_list")] + [TestCase("/guild/invited_guild_list")] + [TestCase("/guild/invite")] + [TestCase("/guild/cancel_invite")] + [TestCase("/guild/reject_invite")] + [TestCase("/guild/join")] + [TestCase("/guild/cancel_join_request")] + [TestCase("/guild/join_request_list")] + [TestCase("/guild/join_request_accept")] + [TestCase("/guild/reject_join_request")] + [TestCase("/guild/leave")] + [TestCase("/guild/remove")] + [TestCase("/guild/change_role")] + // GuildChat endpoints (Task 5) + [TestCase("/guild_chat/messages")] + [TestCase("/guild_chat/post")] + [TestCase("/guild_chat/add_deck")] + [TestCase("/guild_chat/delete_deck")] + [TestCase("/guild_chat/add_replay")] + [TestCase("/guild_chat/replay_detail")] + [TestCase("/guild_chat/deck_log")] public async Task Authenticated_route_resolves(string path) { using var factory = new TestFactory(); diff --git a/SVSim.UnitTests/Wire/GuildWireShape.cs b/SVSim.UnitTests/Wire/GuildWireShape.cs new file mode 100644 index 00000000..a4aaf49b --- /dev/null +++ b/SVSim.UnitTests/Wire/GuildWireShape.cs @@ -0,0 +1,40 @@ +using NUnit.Framework; +using System.Text.Json; +using SVSim.EmulatedEntrypoint.Models.Dtos.Guild; + +namespace SVSim.UnitTests.Wire; + +[TestFixture] +public class GuildWireShape +{ + private static readonly JsonSerializerOptions Opts = new() + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + }; + + [Test] + public void GuildInfo_non_joined_serializes_to_prod_shape() + { + var resp = new GuildInfoResponse + { + MaxMemberNum = 30, + MaxSubLeaderNum = 2, + GuildStatus = 0, + UsableStampList = Enumerable.Range(100001, 20).Select(i => i.ToString()).ToList(), + }; + var json = JsonSerializer.Serialize(resp, Opts); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + Assert.That(root.GetProperty("max_member_num").GetString(), Is.EqualTo("30")); + Assert.That(root.GetProperty("max_sub_leader_num").GetString(), Is.EqualTo("2")); + Assert.That(root.GetProperty("guild_status").GetString(), Is.EqualTo("0")); + Assert.That(root.GetProperty("usable_stamp_list").EnumerateArray().Select(e => e.GetString()).First(), Is.EqualTo("100001")); + + // The four nullable fields MUST NOT appear when null: + Assert.That(root.TryGetProperty("guild", out _), Is.False); + Assert.That(root.TryGetProperty("join_request_count", out _), Is.False); + Assert.That(root.TryGetProperty("invite_count", out _), Is.False); + } +}