diff --git a/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs b/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs index b048fa0..4973850 100644 --- a/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs +++ b/src/GameLogic/PlugIns/PeriodicTasks/CastleSiegeEventPlugIn.cs @@ -43,6 +43,19 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom (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 = { @@ -52,6 +65,9 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom private static readonly ConcurrentDictionary Contexts = new(); + private string? _cachedFlagOwner; + private byte[]? _cachedFlagLogo; + /// public CastleSiegeConfiguration? Configuration { get; set; } @@ -97,6 +113,12 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom { 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); + } } /// @@ -262,6 +284,31 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom } 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) { @@ -335,6 +382,65 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom } }); + 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(p => p.SetCastleFlagAsync(logo)).ConfigureAwait(false); + } + }).ConfigureAwait(false); + } + catch (Exception ex) + { + gameContext.LoggerFactory.CreateLogger() + .LogError(ex, "Castle Siege: error while broadcasting the castle flag."); + } + } + + private async ValueTask 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 diff --git a/src/GameLogic/Views/CastleSiege/ICastleSiegeStatusViewPlugIn.cs b/src/GameLogic/Views/CastleSiege/ICastleSiegeStatusViewPlugIn.cs index e27173e..255b2c0 100644 --- a/src/GameLogic/Views/CastleSiege/ICastleSiegeStatusViewPlugIn.cs +++ b/src/GameLogic/Views/CastleSiege/ICastleSiegeStatusViewPlugIn.cs @@ -22,4 +22,10 @@ public interface ICastleSiegeStatusViewPlugIn : IViewPlugIn /// The remaining hours. /// The remaining minutes. ValueTask SetTimerAsync(byte hour, byte minute); + + /// + /// Sends the castle owner guild's 32-byte logo (C1 B9 02) so the client paints it onto the castle flags. + /// + /// The owner guild's 32-byte logo/emblem. + ValueTask SetCastleFlagAsync(ReadOnlyMemory guildLogo); } diff --git a/src/GameServer/RemoteView/CastleSiege/CastleSiegeStatusViewPlugIn.cs b/src/GameServer/RemoteView/CastleSiege/CastleSiegeStatusViewPlugIn.cs index 4787cc4..de2b2ba 100644 --- a/src/GameServer/RemoteView/CastleSiege/CastleSiegeStatusViewPlugIn.cs +++ b/src/GameServer/RemoteView/CastleSiege/CastleSiegeStatusViewPlugIn.cs @@ -75,4 +75,30 @@ public class CastleSiegeStatusViewPlugIn : ICastleSiegeStatusViewPlugIn await connection.SendAsync(WritePacket).ConfigureAwait(false); } + + /// + public async ValueTask SetCastleFlagAsync(ReadOnlyMemory guildLogo) + { + var connection = this._player.Connection; + if (connection is null) + { + return; + } + + int WritePacket() + { + // C1 24 B9 02 <32-byte guild logo> + var span = connection.Output.GetSpan(36)[..36]; + span.Clear(); + span[0] = 0xC1; + span[1] = 0x24; + span[2] = 0xB9; + span[3] = 0x02; + var logo = guildLogo.Span; + logo[..Math.Min(32, logo.Length)].CopyTo(span.Slice(4, 32)); + return span.Length; + } + + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } }