baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,787 @@
// <copyright file="GuildServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GuildServer;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence;
using Nito.AsyncEx;
using Guild = MUnique.OpenMU.DataModel.Entities.Guild;
using GuildMember = MUnique.OpenMU.DataModel.Entities.GuildMember;
/// <summary>
/// GuildServer which is managing the guilds. All Guilds of online guild members will be cached here.
/// The server needs to be aware when characters leave or enter the world, so a connection to a
/// GameServer is required.
/// </summary>
public class GuildServer : IGuildServer
{
/// <summary>
/// The server id value that is used to indicate that the guild member is offline.
/// </summary>
public static readonly byte OfflineServerId = 0xFF;
private readonly ILogger<GuildServer> _logger;
private readonly AsyncLock _createContainerLock = new AsyncLock();
private readonly IDictionary<uint, GuildContainer> _guildDictionary;
private readonly IDictionary<Guid, uint> _guildIdMapping;
private readonly IdGenerator _idGenerator;
private readonly IGuildChangePublisher _changePublisher;
private readonly IPersistenceContextProvider _persistenceContextProvider;
/// <summary>
/// Initializes a new instance of the <see cref="GuildServer" /> class.
/// </summary>
/// <param name="changePublisher">The change publisher.</param>
/// <param name="persistenceContextProvider">The persistence context provider.</param>
/// <param name="logger">The logger.</param>
public GuildServer(IGuildChangePublisher changePublisher, IPersistenceContextProvider persistenceContextProvider, ILogger<GuildServer> logger)
{
this._changePublisher = changePublisher;
this._persistenceContextProvider = persistenceContextProvider;
this._logger = logger;
this._guildDictionary = new ConcurrentDictionary<uint, GuildContainer>();
this._idGenerator = new IdGenerator(1, int.MaxValue)
{
ReUseSetting = IdGenerator.ReUsePolicy.ReUseWhenExceeded,
};
this._guildIdMapping = new ConcurrentDictionary<Guid, uint>();
}
/// <inheritdoc/>
public async ValueTask<bool> GuildExistsAsync(string guildName)
{
using var context = this._persistenceContextProvider.CreateNewGuildContext();
return await context.GuildWithNameExistsAsync(guildName).ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask<Interfaces.Guild?> GetGuildAsync(uint guildId)
{
if (this._guildDictionary.TryGetValue(guildId, out var guild))
{
return guild.Guild;
}
return null;
}
/// <inheritdoc/>
public async ValueTask<uint> GetGuildIdByNameAsync(string guildName)
{
var guild = this._guildDictionary
.FirstOrDefault(x => x.Value.Guild.Name!.Equals(guildName, StringComparison.OrdinalIgnoreCase));
return guild.Key;
}
/// <inheritdoc />
public async ValueTask IncreaseGuildScoreAsync(uint guildId)
{
if (this._guildDictionary.TryGetValue(guildId, out var guildContainer))
{
guildContainer.Guild.Score++;
await guildContainer.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
}
}
/// <inheritdoc/>
public async ValueTask<bool> CreateGuildAsync(string name, string masterName, Guid masterId, byte[] logo, byte serverId)
{
var context = this._persistenceContextProvider.CreateNewGuildContext();
var guild = context.CreateNew<Guild>();
guild.Name = name;
guild.Logo = logo;
var masterGuildMemberInfo = context.CreateNew<GuildMember>(masterId);
masterGuildMemberInfo.Status = GuildPosition.GuildMaster;
masterGuildMemberInfo.GuildId = guild.Id;
guild.Members.Add(masterGuildMemberInfo);
if (await context.SaveChangesAsync().ConfigureAwait(false))
{
var container = this.CreateGuildContainer(guild, context);
container.SetServerId(masterId, serverId);
container.Members[masterId].PlayerName = masterName;
var status = new GuildMemberStatus(container.Id, GuildPosition.GuildMaster);
await this._changePublisher.AssignGuildToPlayerAsync(serverId, masterName, status).ConfigureAwait(false);
return true;
}
return false;
}
/// <inheritdoc/>
public async ValueTask CreateGuildMemberAsync(uint guildId, Guid characterId, string characterName, GuildPosition role, byte serverId)
{
try
{
if (this._guildDictionary.TryGetValue(guildId, out var guild))
{
if (guild.Members.ContainsKey(characterId))
{
this._logger.LogWarning("Guildmember already exists: {0}", characterName);
return;
}
var guildMember = guild.DatabaseContext.CreateNew<GuildMember>();
guildMember.Id = characterId;
guildMember.Status = role;
guildMember.GuildId = guild.Guild.Id;
guild.Guild.Members.Add(guildMember);
await guild.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
guild.Members.Add(characterId, new GuildListEntry { PlayerName = characterName, PlayerPosition = guildMember.Status, ServerId = serverId });
await this._changePublisher.AssignGuildToPlayerAsync(serverId, characterName, new GuildMemberStatus(guildId, guildMember.Status)).ConfigureAwait(false);
}
}
catch (Exception ex)
{
// Rollback?
this._logger.LogError(ex, "Error when creating a guild member.");
}
}
/// <inheritdoc/>
public async ValueTask ChangeGuildMemberPositionAsync(uint guildId, Guid characterId, GuildPosition role)
{
try
{
if (this._guildDictionary.TryGetValue(guildId, out var guild))
{
var guildMember = guild.Guild.Members.FirstOrDefault(m => m.Id == characterId);
if (guildMember != null)
{
guildMember.Status = role;
await guild.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
var listEntry = guild.Members[characterId];
listEntry.PlayerPosition = role;
}
}
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error when saving a changed guild member.");
}
}
/// <inheritdoc />
public async ValueTask PlayerEnteredGameAsync(Guid characterId, string characterName, byte serverId)
{
using var tempContext = this._persistenceContextProvider.CreateNewGuildContext();
var guildMember = await tempContext.GetByIdAsync<GuildMember>(characterId).ConfigureAwait(false); // we use the same id for Character.Id and GuildMemberInfo.Id
if (guildMember is not null)
{
var guildId = await this.GuildMemberEnterGameAsync(guildMember.GuildId, guildMember.Id, serverId).ConfigureAwait(false);
var status = new GuildMemberStatus(guildId, guildMember.Status);
await this._changePublisher.AssignGuildToPlayerAsync(serverId, characterName, status).ConfigureAwait(false);
}
}
/// <inheritdoc/>
public ValueTask GuildMemberLeftGameAsync(uint guildId, Guid guildMemberId, byte serverId)
{
if (this._guildDictionary.TryGetValue(guildId, out var guild))
{
guild.SetServerId(guildMemberId, OfflineServerId);
// Keep alliances in memory for simplicity.
if (guild.Members.Values.All(member => member.ServerId == OfflineServerId)
&& guild.Guild.AllianceGuild is null)
{
this.RemoveGuildContainer(guild);
}
}
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public async ValueTask<IImmutableList<GuildListEntry>> GetGuildListAsync(uint guildId)
{
if (!this._guildDictionary.TryGetValue(guildId, out var guildContainer))
{
return ImmutableList<GuildListEntry>.Empty;
}
return guildContainer.Members.Values.ToImmutableList();
}
/// <inheritdoc/>
public async ValueTask KickMemberAsync(uint guildId, string playerName)
{
if (!this._guildDictionary.TryGetValue(guildId, out var guildContainer))
{
this._logger.LogWarning($"Guild {guildId} not found, so Player {playerName} can't be kicked.");
return;
}
var kvp = guildContainer.Members.FirstOrDefault(m => m.Value.PlayerName == playerName);
if (default(KeyValuePair<Guid, GuildListEntry>).Equals(kvp))
{
this._logger.LogWarning($"Guild {guildId} and Player {playerName} not found, so it can't be kicked.");
return;
}
var member = kvp.Value;
if (member.PlayerPosition == GuildPosition.GuildMaster)
{
await this.DeleteGuildAsync(guildContainer).ConfigureAwait(false);
return;
}
guildContainer.Members.Remove(kvp.Key);
var guildMember = guildContainer.Guild.Members.FirstOrDefault(m => m.Id == kvp.Key);
if (guildMember != null)
{
await guildContainer.DatabaseContext.DeleteAsync(guildMember).ConfigureAwait(false);
await guildContainer.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
}
await this._changePublisher.GuildPlayerKickedAsync(playerName).ConfigureAwait(false);
}
/// <inheritdoc />
public async ValueTask<GuildPosition> GetGuildPositionAsync(Guid characterId)
{
using var tempContext = this._persistenceContextProvider.CreateNewGuildContext();
var guildMember = await tempContext.GetByIdAsync<GuildMember>(characterId).ConfigureAwait(false); // we use the same id for Character.Id and GuildMemberInfo.Id
return guildMember?.Status ?? GuildPosition.Undefined;
}
/// <inheritdoc />
public async ValueTask<AllianceCreationResult> CreateAllianceAsync(uint masterGuildId, uint targetGuildId)
{
if (!this._guildDictionary.TryGetValue(masterGuildId, out var masterContainer))
{
return AllianceCreationResult.MasterGuildNotFound;
}
if (!this._guildDictionary.TryGetValue(targetGuildId, out var targetContainer))
{
return AllianceCreationResult.TargetGuildNotFound;
}
if (targetContainer.Guild.AllianceGuild is not null)
{
return AllianceCreationResult.TargetGuildAlreadyInAlliance;
}
bool isNewAlliance = false;
try
{
// Set the master guild as alliance master (self-reference)
if (masterContainer.Guild.AllianceGuild is null)
{
masterContainer.Guild.AllianceGuild = masterContainer.Guild;
await masterContainer.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
isNewAlliance = true;
}
// In the target guild's context, load the master guild and set the relationship
var masterGuildInTargetContext = await targetContainer.DatabaseContext
.GetByIdAsync<Guild>(masterContainer.Guild.Id).ConfigureAwait(false);
if (masterGuildInTargetContext is null)
{
return AllianceCreationResult.GuildNotFoundInTargetContext;
}
targetContainer.Guild.AllianceGuild = masterGuildInTargetContext;
await targetContainer.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error when creating an alliance.");
return AllianceCreationResult.Error;
}
if (isNewAlliance)
{
await this._changePublisher.AllianceCreatedAsync(masterGuildId, masterGuildId).ConfigureAwait(false);
}
await this._changePublisher.AllianceCreatedAsync(masterGuildId, targetGuildId).ConfigureAwait(false);
return AllianceCreationResult.Success;
}
/// <inheritdoc />
public async ValueTask<bool> RemoveAllianceAsync(uint targetGuildId)
{
if (!this._guildDictionary.TryGetValue(targetGuildId, out var targetContainer))
{
return false;
}
if (targetContainer.Guild.AllianceGuild?.Name is not { } allianceMaster)
{
return false;
}
if (IsAllianceMaster(targetContainer.Guild))
{
return await this.DisbandAllianceAsync(targetGuildId).ConfigureAwait(false);
}
var masterGuildId = await this.GetGuildIdByNameAsync(allianceMaster).ConfigureAwait(false);
try
{
targetContainer.Guild.AllianceGuild = null;
await targetContainer.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
await this._changePublisher.AllianceDisbandedAsync(masterGuildId, targetGuildId).ConfigureAwait(false);
var remainingAllianceGuilds = await this.GetAllianceGuildsAsync(masterGuildId).ConfigureAwait(false);
if (remainingAllianceGuilds.Count < 2
&& this._guildDictionary.TryGetValue(masterGuildId, out var masterContainer))
{
// No more members in the alliance, clear the master's alliance reference
masterContainer.Guild.AllianceGuild = null;
await masterContainer.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
await this._changePublisher.AllianceDisbandedAsync(masterGuildId, masterGuildId).ConfigureAwait(false);
}
return true;
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error when removing a guild from alliance.");
return false;
}
}
/// <summary>
/// Disbands the alliance of the specified master guild, removing all alliance relationships for all members of the alliance.
/// </summary>
/// <param name="masterGuildId">The ID of the master guild.</param>
/// <returns>True if the alliance was disbanded, false otherwise.</returns>
public async ValueTask<bool> DisbandAllianceAsync(uint masterGuildId)
{
if (!this._guildDictionary.TryGetValue(masterGuildId, out var masterContainer))
{
return false;
}
if (!IsAllianceMaster(masterContainer.Guild))
{
return false;
}
try
{
// Clear AllianceGuild for all members in the alliance (including master)
var allianceMembers = this._guildDictionary.Values
.Where(g => IsInSameAlliance(g.Guild, masterContainer.Guild))
.ToList();
foreach (var member in allianceMembers)
{
member.Guild.AllianceGuild = null;
await member.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
}
await this._changePublisher.AllianceDisbandedAsync(masterGuildId, masterGuildId).ConfigureAwait(false);
return true;
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error when disbanding alliance.");
return false;
}
}
/// <inheritdoc />
public async ValueTask<IImmutableList<AllianceGuildEntry>> GetAllianceGuildsAsync(uint guildId)
{
if (!this._guildDictionary.TryGetValue(guildId, out var guildContainer))
{
return ImmutableList<AllianceGuildEntry>.Empty;
}
if (guildContainer.Guild.AllianceGuild is null)
{
return ImmutableList<AllianceGuildEntry>.Empty;
}
var masterGuid = GetAllianceMasterGuid(guildContainer.Guild);
if (masterGuid == Guid.Empty)
{
return ImmutableList<AllianceGuildEntry>.Empty;
}
return this._guildDictionary.Values
.Where(g => GetAllianceMasterGuid(g.Guild) == masterGuid)
.Select(g => new AllianceGuildEntry(g.Id, g.Guild.Name ?? string.Empty, g.Guild.Members.Count, g.Guild.Logo))
.ToImmutableList();
}
/// <inheritdoc />
public ValueTask<bool> IsAllianceMasterAsync(uint guildId)
{
if (!this._guildDictionary.TryGetValue(guildId, out var guildContainer))
{
return ValueTask.FromResult(false);
}
return ValueTask.FromResult(IsAllianceMaster(guildContainer.Guild));
}
/// <inheritdoc />
public async ValueTask<bool> SetHostilityAsync(uint guildIdA, uint guildIdB, bool create)
{
if (!this._guildDictionary.TryGetValue(guildIdA, out var guildContainerA)
|| !this._guildDictionary.TryGetValue(guildIdB, out var guildContainerB))
{
return false;
}
try
{
if (create)
{
// Load target guild in requester's context to set the hostility FK correctly
var guildBInContainerA = await guildContainerA.DatabaseContext
.GetByIdAsync<Guild>(guildContainerB.Guild.Id).ConfigureAwait(false);
if (guildBInContainerA is null)
{
return false;
}
var guildAInContainerB = await guildContainerB.DatabaseContext
.GetByIdAsync<Guild>(guildContainerA.Guild.Id).ConfigureAwait(false);
if (guildAInContainerB is null)
{
return false;
}
guildContainerA.Guild.Hostility = guildBInContainerA;
guildContainerB.Guild.Hostility = guildAInContainerB;
}
else
{
guildContainerA.Guild.Hostility = null;
guildContainerB.Guild.Hostility = null;
}
await guildContainerA.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
await guildContainerB.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error when setting hostility.");
return false;
}
// Compute all alliance members for both sides to propagate transitive hostility
var allianceIdsA = this.GetAllianceMemberIds(guildIdA);
var allianceIdsB = this.GetAllianceMemberIds(guildIdB);
// When removing a hostility, only notify game servers if no other cross-alliance
// hostility remains between the two alliances (transitive rule: if any pair is still
// hostile all members of both alliances are still rivals).
if (!create && this.AreAlliancesStillHostile(allianceIdsA, allianceIdsB))
{
return true;
}
await this._changePublisher.GuildHostilityChangedAsync(guildIdA, allianceIdsA, guildIdB, allianceIdsB, create).ConfigureAwait(false);
return true;
}
/// <inheritdoc />
public ValueTask<GuildRelationship> GetGuildRelationshipAsync(uint guild1Id, uint guild2Id)
{
if (!this._guildDictionary.TryGetValue(guild1Id, out var guild1Container)
|| !this._guildDictionary.TryGetValue(guild2Id, out var guild2Container))
{
return ValueTask.FromResult(GuildRelationship.None);
}
var guild1 = guild1Container.Guild;
var guild2 = guild2Container.Guild;
// Check if both guilds are in the same alliance
if (guild1.AllianceGuild is not null && guild2.AllianceGuild is not null)
{
var master1Guid = GetAllianceMasterGuid(guild1);
var master2Guid = GetAllianceMasterGuid(guild2);
if (master1Guid != Guid.Empty && master1Guid == master2Guid)
{
return ValueTask.FromResult(GuildRelationship.Union);
}
}
// Check hostility (transitively through alliances)
if (this.HasHostility(guild1, guild2) || this.HasHostility(guild2, guild1))
{
return ValueTask.FromResult(GuildRelationship.Rival);
}
return ValueTask.FromResult(GuildRelationship.None);
}
private static bool IsAllianceMaster(Guild guild)
{
if (guild.AllianceGuild is null)
{
return false;
}
if (ReferenceEquals(guild.AllianceGuild, guild))
{
return true;
}
return guild.AllianceGuild is Guild allianceGuild && allianceGuild.Id == guild.Id;
}
private static Guid GetAllianceMasterGuid(Guild guild)
{
if (guild.AllianceGuild is not Guild masterGuild)
{
return Guid.Empty;
}
return masterGuild.Id;
}
private static bool IsInSameAlliance(Guild guild, Guild masterGuild)
{
if (guild.AllianceGuild is null)
{
return false;
}
var masterGuid = GetAllianceMasterGuid(masterGuild);
if (masterGuid == Guid.Empty)
{
// masterGuild must itself be the master - compare by reference or ID
return (guild.AllianceGuild is Guild allianceGuildOfMember
&& allianceGuildOfMember.Id == masterGuild.Id)
|| ReferenceEquals(guild.AllianceGuild, masterGuild);
}
return GetAllianceMasterGuid(guild) == masterGuid;
}
/// <summary>
/// Removes a guild from the server and the database.
/// First we are trying to get the guild out of our dictionary.
/// We are assuming that all guilds are in the dictionary, because
/// we are holding all guilds of all ingame-characters in it.
/// So this method is only called usefully from the gameserver itself,
/// by player interaction.
/// </summary>
/// <param name="guildContainer">The container of the guild which should be deleted.</param>
private async ValueTask DeleteGuildAsync(GuildContainer guildContainer)
{
// If this guild is alliance master, disband the alliance first
if (IsAllianceMaster(guildContainer.Guild))
{
await this.DisbandAllianceAsync(guildContainer.Id).ConfigureAwait(false);
}
else if (guildContainer.Guild.AllianceGuild is not null)
{
// Remove from alliance
guildContainer.Guild.AllianceGuild = null;
await guildContainer.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
}
await guildContainer.DatabaseContext.DeleteAsync(guildContainer.Guild).ConfigureAwait(false);
await guildContainer.DatabaseContext.SaveChangesAsync().ConfigureAwait(false);
this.RemoveGuildContainer(guildContainer);
await this._changePublisher.GuildDeletedAsync(guildContainer.Id).ConfigureAwait(false);
// TODO: Inform gameServers that guildwar/hostility ended
}
private bool HasHostility(Guild aggressor, Guild target)
{
if (aggressor.Hostility is null)
{
return false;
}
// Direct hostility
if (aggressor.Hostility is Guild hostileGuild && hostileGuild.Id == target.Id)
{
return true;
}
// Transitive: if target is in an alliance, check if aggressor is hostile to any of the target's alliance members
var targetMasterGuid = GetAllianceMasterGuid(target);
if (targetMasterGuid == Guid.Empty)
{
return false;
}
if (aggressor.Hostility is not Guild hostileGuild2)
{
return false;
}
// Check if aggressor's hostility target is in the same alliance as target
return this._guildDictionary.Values
.Any(g => GetAllianceMasterGuid(g.Guild) == targetMasterGuid
&& g.Guild.Id == hostileGuild2.Id);
}
private async ValueTask<uint> GuildMemberEnterGameAsync(Guid guildId, Guid characterId, byte serverId)
{
var guild = await this.GetOrCreateGuildContainerAsync(guildId).ConfigureAwait(false);
if (guild is null)
{
throw new ArgumentException($"Guild not found. Id: {guildId}", nameof(guildId));
}
guild.SetServerId(characterId, serverId);
return guild.Id;
}
private async ValueTask<GuildContainer?> GetOrCreateGuildContainerAsync(Guid guildId)
{
if (!this._guildIdMapping.TryGetValue(guildId, out var shortGuildId)
|| !this._guildDictionary.TryGetValue(shortGuildId, out var guild))
{
guild = await this.CreateGuildContainerAsync(guildId).ConfigureAwait(false);
}
return guild;
}
private async ValueTask<GuildContainer?> CreateGuildContainerAsync(Guid guildId)
{
using var l = await this._createContainerLock.LockAsync();
if (this._guildIdMapping.TryGetValue(guildId, out var existingId)
&& this._guildDictionary.TryGetValue(existingId, out var existingContainer))
{
return existingContainer;
}
var context = this._persistenceContextProvider.CreateNewGuildContext();
var guildinfo = await context.GetByIdAsync<Guild>(guildId).ConfigureAwait(false);
if (guildinfo is null)
{
this._logger.LogWarning("GuildMemberEnter: Guild {0} not found", guildId);
context.Dispose();
return null;
}
var guild = this.CreateGuildContainer(guildinfo, context);
await guild.LoadMemberNamesAsync().ConfigureAwait(false);
if (guildinfo.AllianceGuild is { } allianceMaster)
{
var allianceGuilds = await context.GetAlliancesAsync(allianceMaster.GetId()).ConfigureAwait(false);
foreach (var allianceGuild in allianceGuilds)
{
if (!this._guildIdMapping.ContainsKey(allianceGuild.GetId()))
{
var allyContext = this._persistenceContextProvider.CreateNewGuildContext();
allyContext.Attach(allianceGuild);
var allyGuild = this.CreateGuildContainer(allianceGuild, allyContext);
await allyGuild.LoadMemberNamesAsync().ConfigureAwait(false);
}
}
}
return guild;
}
private GuildContainer CreateGuildContainer(Guild guild, IGuildServerContext databaseContext)
{
var id = (uint)this._idGenerator.GenerateId();
var guildContainer = new GuildContainer(guild, id, databaseContext);
this._guildDictionary.Add(id, guildContainer);
this._guildIdMapping.Add(guild.Id, id);
return guildContainer;
}
private void RemoveGuildContainer(GuildContainer guildContainer)
{
this._guildDictionary.Remove(guildContainer.Id);
this._guildIdMapping.Remove(guildContainer.Guild.Id);
this._idGenerator.GiveBack((int)guildContainer.Id);
guildContainer.DatabaseContext.Dispose();
}
/// <summary>
/// Returns <c>true</c> if any guild in <paramref name="allianceIdsA"/> still has a
/// hostility relationship pointing to any guild in <paramref name="allianceIdsB"/>,
/// or vice versa. Used to avoid sending a spurious "no longer rivals" notification
/// when only one of several cross-alliance hostilities has been removed.
/// </summary>
private bool AreAlliancesStillHostile(IReadOnlyList<uint> allianceIdsA, IReadOnlyList<uint> allianceIdsB)
{
var setB = new HashSet<Guid>(
allianceIdsB
.Select(id => this._guildDictionary.TryGetValue(id, out var c) ? c.Guild.Id : Guid.Empty)
.Where(g => g != Guid.Empty));
// Check A→B direction
foreach (var idA in allianceIdsA)
{
if (this._guildDictionary.TryGetValue(idA, out var cA)
&& cA.Guild.Hostility is Guild hA
&& setB.Contains(hA.Id))
{
return true;
}
}
// Check B→A direction (only build setA if not already returned)
var setA = new HashSet<Guid>(
allianceIdsA
.Select(id => this._guildDictionary.TryGetValue(id, out var c) ? c.Guild.Id : Guid.Empty)
.Where(g => g != Guid.Empty));
foreach (var idB in allianceIdsB)
{
if (this._guildDictionary.TryGetValue(idB, out var cB)
&& cB.Guild.Hostility is Guild hB
&& setA.Contains(hB.Id))
{
return true;
}
}
return false;
}
/// <summary>
/// Gets the runtime uint IDs of all guilds in the alliance of the given guild.
/// If the guild is not in an alliance, returns a list containing only the guild's own ID.
/// </summary>
/// <remarks>
/// Note: this iterates all entries in <see cref="_guildDictionary"/> to find alliance members.
/// For the expected number of online guilds this is acceptable; a separate alliance-to-members index
/// could be introduced in the future if profiling shows this to be a bottleneck.
/// </remarks>
private IReadOnlyList<uint> GetAllianceMemberIds(uint guildId)
{
if (!this._guildDictionary.TryGetValue(guildId, out var guildContainer))
{
return [guildId];
}
var masterGuid = GetAllianceMasterGuid(guildContainer.Guild);
if (masterGuid == Guid.Empty)
{
return [guildId];
}
return this._guildDictionary
.Where(kvp => GetAllianceMasterGuid(kvp.Value.Guild) == masterGuid)
.Select(kvp => kvp.Key)
.ToList();
}
}