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);
+ }
+}