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,4 +1,7 @@
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Entities.Guild;
using SVSim.Database.Repositories.Guild;
using SVSim.Database.Repositories.Viewer;
using SVSim.Database.Services;
namespace SVSim.Database.Services.Guild;
@@ -11,6 +14,10 @@ public sealed class GuildService : IGuildService
private readonly IGuildJoinRequestRepository _joinRequests;
private readonly IGuildChatMessageRepository _chatMessages;
private readonly IGameConfigService _config;
private readonly IGuildIdGenerator _idGen;
private readonly IGuildChatService _chat;
private readonly IViewerRepository _viewers;
private readonly SVSimDbContext _db;
public GuildService(
IGuildRepository guilds,
@@ -18,7 +25,11 @@ public sealed class GuildService : IGuildService
IGuildInviteRepository invites,
IGuildJoinRequestRepository joinRequests,
IGuildChatMessageRepository chatMessages,
IGameConfigService config)
IGameConfigService config,
IGuildIdGenerator idGen,
IGuildChatService chat,
IViewerRepository viewers,
SVSimDbContext db)
{
_guilds = guilds;
_members = members;
@@ -26,20 +37,91 @@ public sealed class GuildService : IGuildService
_joinRequests = joinRequests;
_chatMessages = chatMessages;
_config = config;
_idGen = idGen;
_chat = chat;
_viewers = viewers;
_db = db;
}
// Returns null so /guild/info can short-circuit to a non-joined response in Phase 1.
public Task<GuildFullView?> GetMyGuildAsync(long viewerId, CancellationToken ct = default)
=> Task.FromResult<GuildFullView?>(null);
public async Task<GuildFullView?> GetMyGuildAsync(long viewerId, CancellationToken ct = default)
{
var membership = await _members.GetMembershipAsync(viewerId, ct);
if (membership is null) return null;
var guild = await _guilds.GetWithMembersAsync(membership.GuildId, ct);
if (guild is null || guild.BreakupAt is not null) return null;
int joinReqCount = await _joinRequests.CountPendingForGuildAsync(guild.GuildId, ct);
int inviteCount = await _invites.CountPendingForInviteeAsync(viewerId, ct);
return new GuildFullView(guild, guild.Members, joinReqCount, inviteCount);
}
public Task<Entities.Guild.Guild?> GetActiveAsync(int guildId, CancellationToken ct = default)
=> throw new NotImplementedException();
=> _guilds.GetActiveByIdAsync(guildId, ct);
public Task<IReadOnlyList<GuildSearchEntry>> SearchAsync(string name, int activity, int joinCondition, int memberBucket, CancellationToken ct = default)
=> throw new NotImplementedException();
public Task<GuildOpResult> CreateAsync(long viewerId, CreateGuildRequest req, CancellationToken ct = default)
=> throw new NotImplementedException();
public async Task<GuildOpResult> CreateAsync(long viewerId, CreateGuildRequest req, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(req.Name) || req.Name.Length > 64)
return new(GuildOpResultCode.NameInvalid);
if (req.Activity is < 1 or > 16)
return new(GuildOpResultCode.NameInvalid);
if (req.JoinCondition is < 1 or > 3)
return new(GuildOpResultCode.NameInvalid);
if (await _members.GetMembershipAsync(viewerId, ct) is not null)
return new(GuildOpResultCode.AlreadyInGuild);
if (await _guilds.NameExistsAsync(req.Name, ct))
return new(GuildOpResultCode.NameTaken);
var viewer = await _db.Viewers
.Include(v => v.Info.SelectedEmblem)
.FirstOrDefaultAsync(v => v.Id == viewerId, ct);
if (viewer is null) return new(GuildOpResultCode.PermissionDenied);
var id = await _idGen.NextAsync(
(cand, c) => _guilds.GetByIdAsync(cand, c).ContinueWith(t => t.Result is not null, c),
ct);
var now = DateTime.UtcNow;
var g = new Entities.Guild.Guild
{
GuildId = id,
Name = req.Name,
Description = "",
LeaderViewerId = viewerId,
EmblemId = DefaultEmblemId(viewer),
Activity = (GuildActivity)req.Activity,
JoinCondition = (GuildJoinCondition)req.JoinCondition,
CreatedAt = now,
};
await _guilds.AddAsync(g, ct);
await _members.AddAsync(
new GuildMember { GuildId = id, ViewerId = viewerId, Role = GuildRole.Leader, JoinedAt = now },
ct);
// Side-effects: clear any pending invites + cancel pending join requests for this viewer.
await _invites.ConsumePendingForViewerAsync(viewerId, now, ct);
await _joinRequests.CancelPendingForViewerAsync(viewerId, now, ct);
// Update Viewer.GuildId pointer for quick lookups.
viewer.GuildId = id;
await _db.SaveChangesAsync(ct);
await _chat.EmitSystemEventAsync(id, viewerId, GuildChatMessageType.CreateGuild, body: null, ct);
return new(GuildOpResultCode.Ok, GuildId: id);
}
/// <summary>
/// Returns the viewer's currently-equipped emblem id, or 100000000 (default) if none.
/// </summary>
private static long DefaultEmblemId(Models.Viewer viewer)
{
var emblemId = viewer.Info?.SelectedEmblem?.Id;
return emblemId is > 0 ? emblemId.Value : 100_000_000L;
}
public Task<GuildOpResult> UpdateAsync(long viewerId, UpdateGuildRequest req, CancellationToken ct = default)
=> throw new NotImplementedException();