// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // 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.Interfaces; using MUnique.OpenMU.Pathfinding; using MUnique.OpenMU.PlugIns; /// /// Drives the Castle Siege phase state machine: ticks it every second and carries its configuration. /// State is per- and kept in memory (P1: no persistence). /// When the siege phase starts (P3), warps registered guild members to the Valley of Loren battle map. /// [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, 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 short GuardianStatueNumber = 283; // "Guardian Statue" — inner castle defense private const short StatueTemplateNumber = 132; // BloodCastle "Statue of Saint" destructible (has HP) — HP template for 283 private const int SwitchHoldRange = 3; // Castle defenses spawned at siege start — destructibles the attackers must break: (number, x, y). // Gates are at the 6 REAL castle gate positions (client g_byGateLocation) so the client renders them // closed and blocks the terrain until broken. Guardian statues stand inside near the throne. private static readonly (short Number, byte X, byte Y)[] DefenseSpawns = { // 6 real castle gates (client g_byGateLocation) — close + block terrain until broken. (CastleGateNumber, 67, 114), (CastleGateNumber, 93, 114), (CastleGateNumber, 119, 114), (CastleGateNumber, 81, 161), (CastleGateNumber, 107, 161), (CastleGateNumber, 93, 204), // 4 guardian statues inside the castle (real map positions) — must all be destroyed for the throne. (GuardianStatueNumber, 82, 129), (GuardianStatueNumber, 107, 129), (GuardianStatueNumber, 94, 131), (GuardianStatueNumber, 94, 126), }; // 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), }; private static readonly ConcurrentDictionary Contexts = new(); /// public CastleSiegeConfiguration? Configuration { get; set; } /// /// Gets the Castle Siege context for a game context, if the periodic tick has initialized it. /// /// The game context. /// The context, or null if not yet initialized. public static CastleSiegeContext? TryGetContext(IGameContext gameContext) => Contexts.TryGetValue(gameContext, out var context) ? context : null; /// public object CreateDefaultConfig() => new CastleSiegeConfiguration(); /// public async ValueTask ExecuteTaskAsync(GameContext gameContext) { var context = Contexts.GetOrAdd(gameContext, gc => { var created = new CastleSiegeContext(this.Configuration ?? new CastleSiegeConfiguration()); // Announce phase changes to the whole server and, when the siege begins, warp registered members. created.PhaseChanged += phase => _ = OnPhaseChangedAsync(gc, created, phase); return created; }); 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); } } /// 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.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() .LogError(ex, "Castle Siege: error handling phase change to {phase}.", phase); } } private static ValueTask AnnounceAsync(IGameContext gameContext, string message) => gameContext.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync(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) / statue (283) definitions have no HP and aren't destructible; // make them breakable by borrowing the HP/attributes of a working destructible (131/132). var template = gameContext.Configuration.Monsters.FirstOrDefault( m => m.Number == (spawn.Number == CastleGateNumber ? GateTemplateNumber : StatueTemplateNumber)); 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); } catch (Exception ex) { gameContext.LoggerFactory.CreateLogger() .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(); 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); } } catch (Exception ex) { gameContext.LoggerFactory.CreateLogger() .LogError(ex, "Castle Siege: error during siege tick processing."); } } 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() .LogError(ex, "Castle Siege: error while warping registered members to the battle map."); } } private static async ValueTask 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(); } }