feat(guild): /guild/create + populated /guild/info

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-27 12:17:22 -04:00
parent 99019963ca
commit f7f68d03a7
8 changed files with 430 additions and 11 deletions

View File

@@ -1,8 +1,13 @@
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;
@@ -14,11 +19,16 @@ public sealed class GuildController : SVSimController
{
private readonly IGuildService _guild;
private readonly IGameConfigService _configs;
private readonly SVSimDbContext _db;
public GuildController(IGuildService guild, IGameConfigService configs)
// 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")]
@@ -39,8 +49,14 @@ public sealed class GuildController : SVSimController
}
else
{
resp.GuildStatus = 2; // JOINING — fully populated path lands in Task 7
// Task 7 populates `Guild`, JoinRequestCount, InviteCount.
resp.GuildStatus = 2; // JOINING
resp.JoinRequestCount = view.JoinRequestCount;
resp.InviteCount = view.InviteCount;
resp.Guild = new GuildBundle
{
Detail = ToDetailDto(view.Guild, view.Members.Count),
Members = await ToMemberDtoListAsync(view.Members, viewerId, ct),
};
}
return resp;
}
@@ -48,7 +64,13 @@ public sealed class GuildController : SVSimController
// ===== 21 remaining stubs — each returns the response DTO with defaults =====
[HttpPost("create")]
public Task<ActionResult<EmptyResponse>> Create([FromBody] GuildCreateRequest req, CancellationToken ct) => Stub();
public async Task<ActionResult<EmptyResponse>> 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 Task<ActionResult<EmptyResponse>> Breakup([FromBody] BaseRequest _, CancellationToken ct) => Stub();
@@ -124,6 +146,68 @@ public sealed class GuildController : SVSimController
public Task<ActionResult<GuildChangeRoleResponse>> ChangeRole([FromBody] GuildChangeRoleRequest req, CancellationToken ct)
=> Task.FromResult<ActionResult<GuildChangeRoleResponse>>(new GuildChangeRoleResponse { Members = new() });
// ===== Private helpers =====
private static GuildDetailDto ToDetailDto(SVSim.Database.Entities.Guild.Guild guild, int memberCount) => 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 will be filled via members list join — leave "" here; caller populates it from member data if needed.
LeaderName = "",
};
private async Task<List<GuildMemberInfoDto>> ToMemberDtoListAsync(
IReadOnlyList<GuildMember> 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<GuildMemberInfoDto>(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,
};
/// <summary>
/// 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 <c>result_code: 2</c>.
/// </summary>
private ActionResult<EmptyResponse> MapErrorToWire(GuildOpResult res)
=> Ok(new { result_code = GuildErrorResultCode });
private static Task<ActionResult<EmptyResponse>> Stub() =>
Task.FromResult<ActionResult<EmptyResponse>>(new EmptyResponse());
}