Some checks failed
.NET Core / build (push) Has been cancelled
- The occupier can no longer re-register its own throne (canCapture = eligible != occupier), which caused a repeating 60s panel loop; only a DIFFERENT guild can contest. - The 60s registration panel (0xB2/0x15) is now sent ONLY to the master on the crown, not broadcast to switch-holders/other players. Shield(0x16) + capture(0x18) still broadcast. +1 test (15 total).
547 lines
23 KiB
C#
547 lines
23 KiB
C#
// <copyright file="CastleSiegeEventPlugIn.cs" company="MUnique">
|
|
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
|
// </copyright>
|
|
|
|
namespace MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks;
|
|
|
|
using System.Collections.Concurrent;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using MUnique.OpenMU.GameLogic.CastleSiege;
|
|
using MUnique.OpenMU.GameLogic.NPC;
|
|
using MUnique.OpenMU.GameLogic.Views;
|
|
using MUnique.OpenMU.GameLogic.Views.CastleSiege;
|
|
using MUnique.OpenMU.Interfaces;
|
|
using MUnique.OpenMU.Pathfinding;
|
|
using MUnique.OpenMU.Persistence;
|
|
using MUnique.OpenMU.PlugIns;
|
|
|
|
/// <summary>
|
|
/// Drives the Castle Siege phase state machine: ticks it every second and carries its configuration.
|
|
/// State is per-<see cref="IGameContext"/> and kept in memory (P1: no persistence).
|
|
/// When the siege phase starts (P3), warps registered guild members to the Valley of Loren battle map.
|
|
/// </summary>
|
|
[PlugIn]
|
|
[Display(Name = nameof(CastleSiegeEventPlugIn), Description = "Castle Siege event (phase state machine, scheduling, siege warp).")]
|
|
[Guid("6E2C8B41-9A4D-4C2E-9E7B-1F2A3B4C5D60")]
|
|
public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustomConfiguration<CastleSiegeConfiguration>, ISupportDefaultCustomConfiguration
|
|
{
|
|
private const ushort ValleyOfLorenMapNumber = 30;
|
|
private const short CastleGateNumber = 277; // client MONSTER_CASTLE_GATE1 -> renders a real gate + blocks terrain until broken
|
|
private const short GateTemplateNumber = 131; // BloodCastle "Castle Gate" destructible (has HP) — HP template for 277
|
|
private const int SwitchHoldRange = 3;
|
|
|
|
// Castle defenses spawned at siege start — the 6 REAL castle gates (client g_byGateLocation).
|
|
// The client renders them closed and blocks the terrain until each is broken. All must fall before the throne.
|
|
private static readonly (short Number, byte X, byte Y)[] DefenseSpawns =
|
|
{
|
|
(CastleGateNumber, 67, 114),
|
|
(CastleGateNumber, 93, 114),
|
|
(CastleGateNumber, 119, 114),
|
|
(CastleGateNumber, 81, 161),
|
|
(CastleGateNumber, 107, 161),
|
|
(CastleGateNumber, 93, 204),
|
|
};
|
|
|
|
// Siege weapons spawned at siege start purely for war atmosphere. The client renders monster 221/222 as
|
|
// catapults (attacker/defender). They are NOT counted as defenses (destroying them doesn't open the throne).
|
|
private const short CatapultAttackNumber = 221; // client MONSTER_SLINGSHOT_ATTACK
|
|
private const short CatapultDefenseNumber = 222; // client MONSTER_SLINGSHOT_DEFENSE
|
|
private static readonly (short Number, byte X, byte Y)[] CatapultSpawns =
|
|
{
|
|
(CatapultDefenseNumber, 80, 140),
|
|
(CatapultDefenseNumber, 110, 140),
|
|
(CatapultDefenseNumber, 93, 178),
|
|
(CatapultAttackNumber, 74, 100),
|
|
(CatapultAttackNumber, 112, 100),
|
|
};
|
|
|
|
// Crown Switch positions on Valley of Loren (from the map init) — held by standing on them: (number, x, y).
|
|
private static readonly (short SwitchNumber, byte X, byte Y)[] SwitchPositions =
|
|
{
|
|
(217, 167, 194),
|
|
(218, 184, 195),
|
|
};
|
|
|
|
// The Crown (NPC 216) position on Valley of Loren — the guild master holds it here to capture the throne.
|
|
private static readonly Point CrownPosition = new(176, 212);
|
|
private const int CrownHoldRange = 4;
|
|
|
|
private static readonly ConcurrentDictionary<IGameContext, CastleSiegeContext> Contexts = new();
|
|
|
|
private string? _cachedFlagOwner;
|
|
private byte[]? _cachedFlagLogo;
|
|
|
|
/// <inheritdoc />
|
|
public CastleSiegeConfiguration? Configuration { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets the Castle Siege context for a game context, if the periodic tick has initialized it.
|
|
/// </summary>
|
|
/// <param name="gameContext">The game context.</param>
|
|
/// <returns>The context, or <c>null</c> if not yet initialized.</returns>
|
|
public static CastleSiegeContext? TryGetContext(IGameContext gameContext)
|
|
=> Contexts.TryGetValue(gameContext, out var context) ? context : null;
|
|
|
|
/// <inheritdoc />
|
|
public object CreateDefaultConfig() => new CastleSiegeConfiguration();
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask ExecuteTaskAsync(GameContext gameContext)
|
|
{
|
|
var context = Contexts.GetOrAdd(gameContext, gc =>
|
|
{
|
|
var config = this.Configuration ?? new CastleSiegeConfiguration();
|
|
var created = new CastleSiegeContext(config);
|
|
|
|
// Restore state persisted before the last restart (owner/phase/registrations) BEFORE subscribing,
|
|
// so restoring doesn't announce phases or re-spawn defenses.
|
|
created.RestoreState(config.PersistedOwnerGuildName, config.PersistedPhase, config.PersistedPhaseStartedUtc, config.PersistedRegisteredGuilds);
|
|
|
|
// Announce phase changes to the whole server and, when the siege begins, warp registered members.
|
|
created.PhaseChanged += phase => _ = OnPhaseChangedAsync(gc, created, phase);
|
|
|
|
return created;
|
|
});
|
|
|
|
// Point the context at the current (possibly AdminPanel-edited) config so schedule/durations are live.
|
|
if (this.Configuration is { } liveConfig)
|
|
{
|
|
context.UpdateConfiguration(liveConfig);
|
|
}
|
|
|
|
await context.TickAsync(DateTime.UtcNow).ConfigureAwait(false);
|
|
|
|
// During the siege, evaluate the Crown Switches (held by standing on them) and the throne capture every tick.
|
|
if (context.Phase == CastleSiegePhase.Siege)
|
|
{
|
|
await ProcessSiegeTickAsync(gameContext, context).ConfigureAwait(false);
|
|
}
|
|
|
|
// Persist owner/phase/registrations to the database whenever they changed, so they survive a restart.
|
|
if (context.ConsumeDirty())
|
|
{
|
|
await this.PersistStateAsync(gameContext, context).ConfigureAwait(false);
|
|
}
|
|
|
|
// Keep the owner guild's logo painted on the castle flags for anyone on the castle map (any phase).
|
|
if (DateTime.UtcNow.Second % 15 == 0)
|
|
{
|
|
await this.BroadcastCastleFlagAsync(gameContext, context).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ForceStart()
|
|
{
|
|
// Force-start applies to every active game context's siege.
|
|
foreach (var context in Contexts.Values)
|
|
{
|
|
_ = context.ForceStartRegistrationAsync(DateTime.UtcNow);
|
|
}
|
|
}
|
|
|
|
private static async Task OnPhaseChangedAsync(IGameContext gameContext, CastleSiegeContext context, CastleSiegePhase phase)
|
|
{
|
|
try
|
|
{
|
|
switch (phase)
|
|
{
|
|
case CastleSiegePhase.Registration:
|
|
await AnnounceAsync(gameContext, "Castle Siege registration is now open! Guild masters, register at the Guardsman in the Valley of Loren.").ConfigureAwait(false);
|
|
break;
|
|
case CastleSiegePhase.Siege:
|
|
await AnnounceAsync(gameContext, "The Castle Siege has begun! Break the castle gates and guardian statues, then hold BOTH Crown Switches to take the throne!").ConfigureAwait(false);
|
|
await SpawnCastleDefensesAsync(gameContext, context).ConfigureAwait(false);
|
|
await WarpRegisteredMembersToSiegeAsync(gameContext, context).ConfigureAwait(false);
|
|
break;
|
|
case CastleSiegePhase.Settlement:
|
|
// Stop the on-map countdown for everyone still on the battle map.
|
|
await BroadcastSiegeStateAsync(gameContext, false, 0, 0).ConfigureAwait(false);
|
|
break;
|
|
case CastleSiegePhase.Ownership when context.OwnerGuildName is { } owner:
|
|
await AnnounceAsync(gameContext, $"The Castle Siege has ended. The castle now belongs to the guild '{owner}'!").ConfigureAwait(false);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
|
.LogError(ex, "Castle Siege: error handling phase change to {phase}.", phase);
|
|
}
|
|
}
|
|
|
|
private async ValueTask PersistStateAsync(GameContext gameContext, CastleSiegeContext context)
|
|
{
|
|
try
|
|
{
|
|
if (this.Configuration is not { } config)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Snapshot the live context into the persisted config fields.
|
|
config.PersistedOwnerGuildName = context.OwnerGuildName;
|
|
config.PersistedPhase = context.Phase;
|
|
config.PersistedPhaseStartedUtc = context.PhaseStartedUtc;
|
|
config.PersistedRegisteredGuilds = context.RegisteredGuilds.ToList();
|
|
|
|
// Find our plugin-configuration row via the in-memory config graph to get its id.
|
|
var pluginTypeId = typeof(CastleSiegeEventPlugIn).GUID;
|
|
var inMemory = gameContext.Configuration.PlugInConfigurations.FirstOrDefault(c => c.TypeId == pluginTypeId);
|
|
if (inMemory is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Load a fresh, change-tracked copy of that row in its own (non-caching) context, rewrite its
|
|
// custom-configuration JSON, and save — this is what actually persists to PostgreSQL.
|
|
using var ctx = gameContext.PersistenceContextProvider.CreateNewTypedContext(typeof(PlugInConfiguration), false, gameContext.Configuration);
|
|
var row = await ctx.GetByIdAsync<PlugInConfiguration>(inMemory.GetId()).ConfigureAwait(false);
|
|
if (row is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
row.SetConfiguration(config, gameContext.PlugInManager.CustomConfigReferenceHandler);
|
|
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
|
|
|
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
|
.LogInformation("Castle Siege: persisted state (owner={owner}, phase={phase}).", config.PersistedOwnerGuildName ?? "(none)", config.PersistedPhase);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
|
.LogError(ex, "Castle Siege: error while persisting state to the database.");
|
|
}
|
|
}
|
|
|
|
private static ValueTask AnnounceAsync(IGameContext gameContext, string message)
|
|
=> gameContext.ForEachPlayerAsync(player =>
|
|
player.InvokeViewPlugInAsync<IShowMessagePlugIn>(p => p.ShowMessageAsync(message, MessageType.GoldenCenter)).AsTask());
|
|
|
|
private static void EnsureDestructible(MonsterDefinition target, MonsterDefinition? template)
|
|
{
|
|
if (target.ObjectKind == NpcObjectKind.Destructible)
|
|
{
|
|
return;
|
|
}
|
|
|
|
target.ObjectKind = NpcObjectKind.Destructible;
|
|
if (template is not null && target.Attributes.Count == 0)
|
|
{
|
|
foreach (var attribute in template.Attributes)
|
|
{
|
|
target.Attributes.Add(attribute);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task SpawnCastleDefensesAsync(IGameContext gameContext, CastleSiegeContext context)
|
|
{
|
|
try
|
|
{
|
|
if (gameContext is not GameContext concrete)
|
|
{
|
|
context.SetDefenseCount(0);
|
|
return;
|
|
}
|
|
|
|
var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
|
|
if (map is null)
|
|
{
|
|
context.SetDefenseCount(0);
|
|
return;
|
|
}
|
|
|
|
var spawned = 0;
|
|
for (var i = 0; i < DefenseSpawns.Length; i++)
|
|
{
|
|
var spawn = DefenseSpawns[i];
|
|
var definition = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == spawn.Number);
|
|
if (definition is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// The stock CS gate (277) definition has no HP and isn't destructible; make it breakable
|
|
// by borrowing the HP/attributes of a working destructible (BloodCastle gate 131).
|
|
var template = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == GateTemplateNumber);
|
|
EnsureDestructible(definition, template);
|
|
|
|
var spawnArea = new MonsterSpawnArea
|
|
{
|
|
MonsterDefinition = definition,
|
|
Quantity = 1,
|
|
X1 = spawn.X,
|
|
X2 = spawn.X,
|
|
Y1 = spawn.Y,
|
|
Y2 = spawn.Y,
|
|
Direction = Direction.South,
|
|
SpawnTrigger = SpawnTrigger.OnceAtEventStart,
|
|
};
|
|
|
|
var npc = await concrete.MapInitializer.InitializeSpawnAsync(1000 + i, map, spawnArea).ConfigureAwait(false);
|
|
if (npc is AttackableNpcBase attackable)
|
|
{
|
|
attackable.Died += (_, _) => context.NotifyDefenseDestroyed();
|
|
spawned++;
|
|
}
|
|
}
|
|
|
|
context.SetDefenseCount(spawned);
|
|
|
|
// Spawn the catapults (siege weapons) for war atmosphere — not counted as defenses.
|
|
for (var i = 0; i < CatapultSpawns.Length; i++)
|
|
{
|
|
var spawn = CatapultSpawns[i];
|
|
var definition = gameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == spawn.Number);
|
|
if (definition is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var spawnArea = new MonsterSpawnArea
|
|
{
|
|
MonsterDefinition = definition,
|
|
Quantity = 1,
|
|
X1 = spawn.X,
|
|
X2 = spawn.X,
|
|
Y1 = spawn.Y,
|
|
Y2 = spawn.Y,
|
|
Direction = Direction.South,
|
|
SpawnTrigger = SpawnTrigger.OnceAtEventStart,
|
|
};
|
|
|
|
await concrete.MapInitializer.InitializeSpawnAsync(2000 + i, map, spawnArea).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
|
.LogError(ex, "Castle Siege: error while spawning castle defenses.");
|
|
context.SetDefenseCount(0);
|
|
}
|
|
}
|
|
|
|
private static async Task ProcessSiegeTickAsync(IGameContext gameContext, CastleSiegeContext context)
|
|
{
|
|
try
|
|
{
|
|
var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
|
|
if (map is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Each Crown Switch is held by whichever registered guild currently has a member standing on it.
|
|
foreach (var (switchNumber, x, y) in SwitchPositions)
|
|
{
|
|
string? holder = null;
|
|
var nearby = map.GetAttackablesInRange(new Point(x, y), SwitchHoldRange).OfType<Player>();
|
|
foreach (var player in nearby)
|
|
{
|
|
var guildName = await GetGuildNameAsync(player).ConfigureAwait(false);
|
|
if (guildName is not null && context.RegisteredGuilds.Contains(guildName))
|
|
{
|
|
holder = guildName;
|
|
break;
|
|
}
|
|
}
|
|
|
|
context.SetSwitchHolder(switchNumber, holder);
|
|
}
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
// Crown-hold capture: when a guild holds both switches with every gate down, the crown shield drops;
|
|
// that guild's master then holds the crown for CrownHoldDuration to take the throne (contestable).
|
|
var eligible = context.GetShieldEligibleGuild();
|
|
Player? masterPlayer = null;
|
|
if (eligible is not null)
|
|
{
|
|
foreach (var player in map.GetAttackablesInRange(CrownPosition, CrownHoldRange).OfType<Player>())
|
|
{
|
|
if (player.GuildStatus?.Position == GuildPosition.GuildMaster
|
|
&& await GetGuildNameAsync(player).ConfigureAwait(false) == eligible)
|
|
{
|
|
masterPlayer = player;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var crown = context.TickCrownHold(eligible, masterPlayer is not null, now, context.Configuration.CrownHoldDuration);
|
|
|
|
// Shield drop/raise -> everyone on the battle map, but only when it flips (the packet pops a modal).
|
|
if (crown.ShieldChanged)
|
|
{
|
|
await ForEachOnBattleMapAsync(gameContext, p => p.SetCrownShieldAsync(crown.ShieldDown)).ConfigureAwait(false);
|
|
}
|
|
|
|
switch (crown.Event)
|
|
{
|
|
case CrownEvent.HoldStarted when masterPlayer is not null:
|
|
// The 60-second registration panel is shown ONLY to the master taking the crown.
|
|
await masterPlayer.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetCrownRegistAsync(0, 0)).ConfigureAwait(false);
|
|
break;
|
|
case CrownEvent.HoldReset when masterPlayer is not null:
|
|
await masterPlayer.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetCrownRegistAsync(2, 0)).ConfigureAwait(false);
|
|
break;
|
|
case CrownEvent.Captured when crown.Guild is { } captured:
|
|
await ForEachOnBattleMapAsync(gameContext, p => p.AnnounceSealCapturedAsync(captured)).ConfigureAwait(false);
|
|
await AnnounceAsync(gameContext, $"Guild '{captured}' has taken the Crown and now holds the throne!").ConfigureAwait(false);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Keep the client's on-map countdown armed and in sync. Resend every 10s so players who just
|
|
// loaded the battle map pick it up, without visibly resetting the second-counter too often.
|
|
if ((int)(now - context.PhaseStartedUtc).TotalSeconds % 10 == 0)
|
|
{
|
|
var remaining = context.GetRemainingSiegeTime(now);
|
|
var totalMinutes = (int)Math.Ceiling(remaining.TotalMinutes);
|
|
await BroadcastSiegeStateAsync(gameContext, true, (byte)(totalMinutes / 60), (byte)(totalMinutes % 60)).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
|
.LogError(ex, "Castle Siege: error during siege tick processing.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends the Castle Siege countdown state to every player currently on the battle map (Valley of Loren).
|
|
/// When <paramref name="started"/> is true it also (re)seeds the remaining hour/minute.
|
|
/// </summary>
|
|
private static ValueTask BroadcastSiegeStateAsync(IGameContext gameContext, bool started, byte hour, byte minute)
|
|
=> gameContext.ForEachPlayerAsync(async player =>
|
|
{
|
|
if (player.CurrentMap?.Definition.Number != ValleyOfLorenMapNumber)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await player.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetBattleStateAsync(started)).ConfigureAwait(false);
|
|
if (started)
|
|
{
|
|
await player.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetTimerAsync(hour, minute)).ConfigureAwait(false);
|
|
}
|
|
});
|
|
|
|
/// <summary>Invokes the Castle Siege status view for every player currently on the battle map.</summary>
|
|
private static ValueTask ForEachOnBattleMapAsync(IGameContext gameContext, Func<ICastleSiegeStatusViewPlugIn, ValueTask> action)
|
|
=> gameContext.ForEachPlayerAsync(player =>
|
|
player.CurrentMap?.Definition.Number == ValleyOfLorenMapNumber
|
|
? player.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(action).AsTask()
|
|
: Task.CompletedTask);
|
|
|
|
private async ValueTask BroadcastCastleFlagAsync(IGameContext gameContext, CastleSiegeContext context)
|
|
{
|
|
try
|
|
{
|
|
if (context.OwnerGuildName is not { Length: > 0 } owner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var logo = await this.GetOwnerLogoAsync(gameContext, owner).ConfigureAwait(false);
|
|
if (logo is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await gameContext.ForEachPlayerAsync(async player =>
|
|
{
|
|
if (player.CurrentMap?.Definition.Number == ValleyOfLorenMapNumber)
|
|
{
|
|
await player.InvokeViewPlugInAsync<ICastleSiegeStatusViewPlugIn>(p => p.SetCastleFlagAsync(logo)).ConfigureAwait(false);
|
|
}
|
|
}).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
|
.LogError(ex, "Castle Siege: error while broadcasting the castle flag.");
|
|
}
|
|
}
|
|
|
|
private async ValueTask<byte[]?> GetOwnerLogoAsync(IGameContext gameContext, string ownerName)
|
|
{
|
|
if (this._cachedFlagOwner == ownerName && this._cachedFlagLogo is not null)
|
|
{
|
|
return this._cachedFlagLogo;
|
|
}
|
|
|
|
if (gameContext is not IGameServerContext serverContext)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var guildId = await serverContext.GuildServer.GetGuildIdByNameAsync(ownerName).ConfigureAwait(false);
|
|
if (guildId == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var guild = await serverContext.GuildServer.GetGuildAsync(guildId).ConfigureAwait(false);
|
|
if (guild?.Logo is not { Length: > 0 } logo)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
this._cachedFlagOwner = ownerName;
|
|
this._cachedFlagLogo = logo;
|
|
return logo;
|
|
}
|
|
|
|
private static async Task WarpRegisteredMembersToSiegeAsync(IGameContext gameContext, CastleSiegeContext context)
|
|
{
|
|
try
|
|
{
|
|
var map = await gameContext.GetMapAsync(ValleyOfLorenMapNumber).ConfigureAwait(false);
|
|
if (map?.SafeZoneSpawnGate is not { } gate)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await gameContext.ForEachPlayerAsync(async player =>
|
|
{
|
|
var guildName = await GetGuildNameAsync(player).ConfigureAwait(false);
|
|
if (guildName is not null && context.RegisteredGuilds.Contains(guildName))
|
|
{
|
|
await player.WarpToAsync(gate).ConfigureAwait(false);
|
|
}
|
|
}).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
gameContext.LoggerFactory.CreateLogger<CastleSiegeEventPlugIn>()
|
|
.LogError(ex, "Castle Siege: error while warping registered members to the battle map.");
|
|
}
|
|
}
|
|
|
|
private static async ValueTask<string?> GetGuildNameAsync(Player player)
|
|
{
|
|
if (player.GuildStatus is not { } guildStatus)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (player.GameContext is IGameServerContext serverContext)
|
|
{
|
|
var guild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false);
|
|
if (guild?.Name is { Length: > 0 } name)
|
|
{
|
|
return name;
|
|
}
|
|
}
|
|
|
|
return guildStatus.GuildId.ToString();
|
|
}
|
|
}
|