diff --git a/src/Dapr/GuildServer.Host/GuildServerController.cs b/src/Dapr/GuildServer.Host/GuildServerController.cs index 7254e1e636..c5d1b808c4 100644 --- a/src/Dapr/GuildServer.Host/GuildServerController.cs +++ b/src/Dapr/GuildServer.Host/GuildServerController.cs @@ -170,7 +170,7 @@ public ValueTask ChangeGuildMemberPositionAsync([FromBody] GuildMemberRoleChange [HttpPost(nameof(IGuildServer.ChangeGuildMemberPositionByNameAsync))] public ValueTask ChangeGuildMemberPositionByNameAsync([FromBody] GuildMemberRoleChangeByNameArguments data) { - return this._guildServer.ChangeGuildMemberPositionByNameAsync(data.GuildId, data.CharacterName, data.NewRole); + return this._guildServer.ChangeGuildMemberPositionByNameAsync(data.GuildId, data.CharacterName, data.NewRole, data.MasterTotalLevel); } /// diff --git a/src/Dapr/ServerClients/GuildMemberRoleChangeByNameArguments.cs b/src/Dapr/ServerClients/GuildMemberRoleChangeByNameArguments.cs index 259debd99c..6867bbcabb 100644 --- a/src/Dapr/ServerClients/GuildMemberRoleChangeByNameArguments.cs +++ b/src/Dapr/ServerClients/GuildMemberRoleChangeByNameArguments.cs @@ -9,4 +9,4 @@ namespace MUnique.OpenMU.ServerClients; /// /// Arguments for a guild member role change by character name. /// -public record GuildMemberRoleChangeByNameArguments(uint GuildId, string CharacterName, GuildPosition NewRole); +public record GuildMemberRoleChangeByNameArguments(uint GuildId, string CharacterName, GuildPosition NewRole, int MasterTotalLevel); diff --git a/src/Dapr/ServerClients/GuildServer.cs b/src/Dapr/ServerClients/GuildServer.cs index 60f57957bd..bd09213826 100644 --- a/src/Dapr/ServerClients/GuildServer.cs +++ b/src/Dapr/ServerClients/GuildServer.cs @@ -202,11 +202,11 @@ public async ValueTask ChangeGuildMemberPositionAsync(uint guildId, Guid charact } /// - public async ValueTask ChangeGuildMemberPositionByNameAsync(uint guildId, string characterName, GuildPosition role) + public async ValueTask ChangeGuildMemberPositionByNameAsync(uint guildId, string characterName, GuildPosition role, int masterTotalLevel) { try { - return await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.ChangeGuildMemberPositionByNameAsync), new GuildMemberRoleChangeByNameArguments(guildId, characterName, role)).ConfigureAwait(false); + return await this._daprClient.InvokeMethodAsync(this._targetAppId, nameof(this.ChangeGuildMemberPositionByNameAsync), new GuildMemberRoleChangeByNameArguments(guildId, characterName, role, masterTotalLevel)).ConfigureAwait(false); } catch (Exception ex) { diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 94383d0c9b..28d7c55138 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -20,6 +20,7 @@ + diff --git a/src/Interfaces/GuildPositionComparer.cs b/src/Interfaces/GuildPositionComparer.cs index 88c8b3c548..7af557e013 100644 --- a/src/Interfaces/GuildPositionComparer.cs +++ b/src/Interfaces/GuildPositionComparer.cs @@ -22,23 +22,6 @@ private GuildPositionComparer() /// public static GuildPositionComparer Instance { get; } = new(); - /// - /// Gets the numerical rank for ordering purposes. A lower value means a higher rank. - /// - /// The guild position. - /// The rank of the position. - private static int GetRank(GuildPosition position) - { - return position switch - { - GuildPosition.GuildMaster => 0, - GuildPosition.AssistantMaster => 1, - GuildPosition.BattleMaster => 2, - GuildPosition.NormalMember => 3, - _ => 4, - }; - } - /// public int Compare(GuildPosition x, GuildPosition y) { @@ -71,4 +54,21 @@ public int Compare(GuildListEntry? x, GuildListEntry? y) return StringComparer.OrdinalIgnoreCase.Compare(x.PlayerName, y.PlayerName); } + + /// + /// Gets the numerical rank for ordering purposes. A lower value means a higher rank. + /// + /// The guild position. + /// The rank of the position. + private static int GetRank(GuildPosition position) + { + return position switch + { + GuildPosition.GuildMaster => 0, + GuildPosition.AssistantMaster => 1, + GuildPosition.BattleMaster => 2, + GuildPosition.NormalMember => 3, + _ => 4, + }; + } } diff --git a/src/Interfaces/Properties/ModelResources.resx b/src/Interfaces/Properties/ModelResources.resx index c2d2b3eb16..c0f8545de8 100644 --- a/src/Interfaces/Properties/ModelResources.resx +++ b/src/Interfaces/Properties/ModelResources.resx @@ -1,4 +1,4 @@ - + text/microsoft-resx @@ -328,6 +328,12 @@ + + Assistant Master + + + + Localizable Exception Base diff --git a/src/Persistence/EntityFramework/GuildServerContext.cs b/src/Persistence/EntityFramework/GuildServerContext.cs index eb895bf44f..2e2aae3590 100644 --- a/src/Persistence/EntityFramework/GuildServerContext.cs +++ b/src/Persistence/EntityFramework/GuildServerContext.cs @@ -1,9 +1,10 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Persistence.EntityFramework; +using System.Threading; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using MUnique.OpenMU.Persistence.EntityFramework.Model; @@ -68,4 +69,51 @@ join character in this.Context.Set() on member.Id equals characte .Include(g => g.RawMembers) .ToListAsync().ConfigureAwait(false); } + + /// + public async ValueTask> GetGuildsOrderedByNameAsync(int skip, int count, CancellationToken cancellationToken = default) + { + return await this.Context.Set() + .AsNoTracking() + .Include(g => g.RawMembers) + .Include(g => g.RawAllianceGuild) + .OrderBy(g => g.Name) + .Skip(skip) + .Take(count) + .ToListAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public async ValueTask> SearchGuildsAsync(string searchTerm, int skip, int count, CancellationToken cancellationToken = default) + { + // Invariant: this runs in .NET, so it must not depend on the server's locale (see the + // equivalent remark in PlayerContext.SearchAccountsAsync). The ToLower() calls below are + // translated to the database's own lower(), which is why they cannot take a culture. + var term = searchTerm.ToLowerInvariant(); + return await this.Context.Set() + .AsNoTracking() + .Include(g => g.RawMembers) + .Include(g => g.RawAllianceGuild) + .Where(g => g.Name != null && g.Name.ToLower().Contains(term)) + .OrderBy(g => g.Name) + .Skip(skip) + .Take(count) + .ToListAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public async ValueTask> GetAllianceMasterIdsAsync(IReadOnlyCollection guildIds) + { + if (guildIds.Count == 0) + { + return []; + } + + return await this.Context.Set() + .AsNoTracking() + .Where(g => g.AllianceGuildId != null && guildIds.Contains(g.AllianceGuildId!.Value)) + .Select(g => g.AllianceGuildId!.Value) + .Distinct() + .ToListAsync().ConfigureAwait(false); + } } diff --git a/src/Persistence/IGuildServerContext.cs b/src/Persistence/IGuildServerContext.cs index 7e2fd3a758..f0b9f48c54 100644 --- a/src/Persistence/IGuildServerContext.cs +++ b/src/Persistence/IGuildServerContext.cs @@ -1,9 +1,10 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Persistence; +using System.Threading; using MUnique.OpenMU.Interfaces; /// @@ -46,4 +47,31 @@ public interface IGuildServerContext : IContext /// The guild identifier. /// The ids of the alliances of a guild. ValueTask> GetAlliancesAsync(Guid guildId); + + /// + /// Gets a page of guilds, ordered by name, without loading the whole guild table into memory. + /// + /// The number of guilds to skip. + /// The maximum number of guilds to return. + /// The cancellation token. + /// The requested page of guilds, including alliance and member information. + ValueTask> GetGuildsOrderedByNameAsync(int skip, int count, CancellationToken cancellationToken = default); + + /// + /// Searches guilds by name and returns a page of the matching results, without loading the whole guild table into memory. + /// + /// The case-insensitive search term which is matched against the guild name. + /// The number of matching guilds to skip. + /// The maximum number of guilds to return. + /// The cancellation token. + /// The requested page of matching guilds, including alliance and member information. + ValueTask> SearchGuildsAsync(string searchTerm, int skip, int count, CancellationToken cancellationToken = default); + + /// + /// Of the given guild identifiers, returns the ones which are the master of an alliance + /// (i.e. at least one other guild points to them as their ). + /// + /// The guild identifiers to check. Kept small (e.g. one page) to avoid a full table scan. + /// The subset of which are alliance masters. + ValueTask> GetAllianceMasterIdsAsync(IReadOnlyCollection guildIds); } diff --git a/src/Persistence/InMemory/GuildServerInMemoryContext.cs b/src/Persistence/InMemory/GuildServerInMemoryContext.cs index 581d0c5ff3..d3a17b5fec 100644 --- a/src/Persistence/InMemory/GuildServerInMemoryContext.cs +++ b/src/Persistence/InMemory/GuildServerInMemoryContext.cs @@ -1,9 +1,10 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Persistence.InMemory; +using System.Threading; using MUnique.OpenMU.Persistence.BasicModel; /// @@ -64,4 +65,40 @@ public async ValueTask> GetMemberNamesAsync(Gu .Where(g => g.AllianceGuild?.GetId() == guildId) .ToList(); } + + /// + public async ValueTask> GetGuildsOrderedByNameAsync(int skip, int count, CancellationToken cancellationToken = default) + { + var allGuilds = await this.Provider.GetRepository().GetAllAsync(cancellationToken).ConfigureAwait(false); + return allGuilds.OrderBy(g => g.Name).Skip(skip).Take(count).ToList(); + } + + /// + public async ValueTask> SearchGuildsAsync(string searchTerm, int skip, int count, CancellationToken cancellationToken = default) + { + var allGuilds = await this.Provider.GetRepository().GetAllAsync(cancellationToken).ConfigureAwait(false); + return allGuilds + .Where(g => g.Name?.Contains(searchTerm, StringComparison.InvariantCultureIgnoreCase) == true) + .OrderBy(g => g.Name) + .Skip(skip) + .Take(count) + .ToList(); + } + + /// + public async ValueTask> GetAllianceMasterIdsAsync(IReadOnlyCollection guildIds) + { + if (guildIds.Count == 0) + { + return []; + } + + var guildIdSet = guildIds.ToHashSet(); + var allGuilds = await this.Provider.GetRepository().GetAllAsync().ConfigureAwait(false); + return allGuilds + .Where(g => g.AllianceGuild is { } master && guildIdSet.Contains(master.GetId())) + .Select(g => g.AllianceGuild!.GetId()) + .Distinct() + .ToList(); + } } diff --git a/src/Web/AdminPanel/Components/Layout/NavMenu.razor b/src/Web/AdminPanel/Components/Layout/NavMenu.razor index 6ad2372d5e..6173143d0a 100644 --- a/src/Web/AdminPanel/Components/Layout/NavMenu.razor +++ b/src/Web/AdminPanel/Components/Layout/NavMenu.razor @@ -49,6 +49,18 @@