using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using SVSim.Database; using SVSim.Database.Entities.Guild; using SVSim.Database.Models; 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.Common.Guild; 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; private readonly SVSimDbContext _db; // Wire error code returned when guild operations fail (non-1 result_code). private const int GuildErrorResultCode = 2; public GuildController(IGuildService guild, IGameConfigService configs, SVSimDbContext db) { _guild = guild; _configs = configs; _db = db; } [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 resp.JoinRequestCount = view.JoinRequestCount; resp.InviteCount = view.InviteCount; var memberDtos = await ToMemberDtoListAsync(view.Members, viewerId, ct); var leaderName = memberDtos.FirstOrDefault(m => m.ViewerId == view.Guild.LeaderViewerId)?.Name ?? ""; resp.Guild = new GuildBundle { Detail = ToDetailDto(view.Guild, view.Members.Count, leaderName), Members = memberDtos, }; } return resp; } // ===== 21 remaining stubs — each returns the response DTO with defaults ===== [HttpPost("create")] public async Task> Create([FromBody] GuildCreateRequest req, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); var res = await _guild.CreateAsync(viewerId, new(req.GuildName, req.Activity, req.JoinCondition), ct); if (res.IsOk) return new EmptyResponse(); return MapErrorToWire(res); } [HttpPost("breakup")] public async Task> Breakup([FromBody] BaseRequest _, CancellationToken ct) { if (!TryGetViewerId(out var v)) return Unauthorized(); var r = await _guild.BreakupAsync(v, ct); return r.IsOk ? new EmptyResponse() : MapErrorToWire(r); } [HttpPost("update")] public async Task> Update([FromBody] GuildUpdateRequest req, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); int? activity = req.Activity != 0 ? req.Activity : null; int? joinCondition = req.JoinCondition != 0 ? req.JoinCondition : null; var r = await _guild.UpdateAsync(viewerId, new(activity, joinCondition), ct); if (!r.IsOk) return WireError(); var m = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct); if (m is null) return WireError(); var guild = await _guild.GetActiveAsync(m.GuildId, ct); if (guild is null) return WireError(); var memberCount = await _db.GuildMembers.CountAsync(x => x.GuildId == guild.GuildId, ct); var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct); return new GuildUpdateResponse { Guild = new GuildDetailSubTree { Detail = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "") } }; } [HttpPost("update_description")] public async Task> UpdateDescription([FromBody] GuildUpdateDescriptionRequest req, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); var r = await _guild.UpdateDescriptionAsync(viewerId, req.Description, ct); return r.IsOk ? new EmptyResponse() : MapErrorToWire(r); } [HttpPost("update_emblem")] public async Task> UpdateEmblem([FromBody] GuildUpdateEmblemRequest req, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); var r = await _guild.UpdateEmblemAsync(viewerId, req.EmblemId, ct); if (!r.IsOk) return WireError(); var m = await _db.GuildMembers.FirstOrDefaultAsync(m => m.ViewerId == viewerId, ct); if (m is null) return WireError(); var guild = await _guild.GetActiveAsync(m.GuildId, ct); if (guild is null) return WireError(); var memberCount = await _db.GuildMembers.CountAsync(x => x.GuildId == guild.GuildId, ct); var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct); return new GuildUpdateResponse { Guild = new GuildDetailSubTree { Detail = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "") } }; } [HttpPost("search_guild")] public Task> SearchGuild([FromBody] GuildSearchGuildRequest req, CancellationToken ct) => Task.FromResult>(new GuildSearchGuildResponse { List = new() }); [HttpPost("emblem_list")] public async Task> EmblemList([FromBody] BaseRequest _, CancellationToken ct) { if (!TryGetViewerId(out var viewerId)) return Unauthorized(); var viewer = await _db.Viewers .AsNoTracking() .Include(v => v.Emblems) .FirstOrDefaultAsync(v => v.Id == viewerId, ct); var emblems = viewer?.Emblems ?? new(); return new GuildEmblemListResponse { EmblemList = emblems.Select(e => new GuildEmblemEntry { EmblemId = e.Id }).ToList() }; } [HttpPost("others_info")] public async Task> OthersInfo([FromBody] GuildOthersInfoRequest req, CancellationToken ct) { if (!TryGetViewerId(out _)) return Unauthorized(); var guild = await _guild.GetActiveAsync(req.GuildId, ct); if (guild is null) return new GuildOthersInfoResponse(); var memberCount = await _db.GuildMembers.CountAsync(m => m.GuildId == guild.GuildId, ct); var leader = await _db.Viewers.AsNoTracking().FirstOrDefaultAsync(v => v.Id == guild.LeaderViewerId, ct); return new GuildOthersInfoResponse { Guild = new GuildDetailSubTree { Detail = ToDetailDto(guild, memberCount, leader?.DisplayName ?? "") } }; } [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 helpers ===== private static GuildDetailDto ToDetailDto(SVSim.Database.Entities.Guild.Guild guild, int memberCount, string leaderName = "") => new() { GuildId = guild.GuildId, GuildName = guild.Name, Description = guild.Description, GuildEmblemId = guild.EmblemId, JoinCondition = (int)guild.JoinCondition, Activity = (int)guild.Activity, MemberNum = memberCount, LeaderViewerId = guild.LeaderViewerId, LeaderName = leaderName, }; private async Task> ToMemberDtoListAsync( IReadOnlyList members, long callerViewerId, CancellationToken ct) { if (members.Count == 0) return new(); var viewerIds = members.Select(m => m.ViewerId).ToList(); // Batch-load viewer rows with Info + SelectedEmblem + SelectedDegree. var viewers = await _db.Viewers .AsNoTracking() .Include(v => v.Info.SelectedEmblem) .Include(v => v.Info.SelectedDegree) .Where(v => viewerIds.Contains(v.Id)) .ToDictionaryAsync(v => v.Id, ct); var result = new List(members.Count); foreach (var m in members) { viewers.TryGetValue(m.ViewerId, out var v); result.Add(ToMemberDto(m, v)); } return result; } private static GuildMemberInfoDto ToMemberDto(GuildMember member, Viewer? viewer) => new() { ViewerId = member.ViewerId, Name = viewer?.DisplayName ?? "", EmblemId = viewer?.Info?.SelectedEmblem?.Id is > 0 ? viewer.Info.SelectedEmblem.Id : 100_000_000L, CountryCode = viewer?.Info?.CountryCode ?? "", Rank = 1, // TODO: populate from actual rank data when rank tracking lands DegreeId = viewer?.Info?.SelectedDegree?.Id ?? 0, IsOfficialMarkDisplayed = viewer?.Info?.IsOfficialMarkDisplayed == true ? 1 : 0, Role = (int)member.Role, }; /// /// Maps a failed GuildOpResult to the wire error envelope convention used throughout the /// codebase (see CampaignController.Fail(), AchievementController, MissionController). /// Returns HTTP 200 with a JSON body carrying result_code: 2. /// private ActionResult MapErrorToWire(GuildOpResult res) => Ok(new { result_code = GuildErrorResultCode }); /// /// Returns a wire-error for endpoints that return a non-EmptyResponse type. /// Identical result_code=2 body, but typed as ObjectResult so it fits any TResult. /// private ObjectResult WireError() => Ok(new { result_code = GuildErrorResultCode }); private static Task> Stub() => Task.FromResult>(new EmptyResponse()); }