feat(CS): castle flag owner logo (C1 B9 02) + catapults for war atmosphere
Some checks failed
.NET Core / build (push) Has been cancelled
Some checks failed
.NET Core / build (push) Has been cancelled
#1 Castle flags now show the owner guild's logo: resolve owner name -> guild logo (cached), broadcast C1 B9 02 (32-byte mark) to players on the castle map every 15s. #3 War atmosphere: spawn catapult NPCs 221/222 at siege start (client renders them as siege weapons); the 0xB2/0x17 start flag already flips the map to warzone mode. All raw S6 packets, no client changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,19 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
|||||||
(CastleGateNumber, 93, 204),
|
(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).
|
// 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 =
|
private static readonly (short SwitchNumber, byte X, byte Y)[] SwitchPositions =
|
||||||
{
|
{
|
||||||
@@ -52,6 +65,9 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
|||||||
|
|
||||||
private static readonly ConcurrentDictionary<IGameContext, CastleSiegeContext> Contexts = new();
|
private static readonly ConcurrentDictionary<IGameContext, CastleSiegeContext> Contexts = new();
|
||||||
|
|
||||||
|
private string? _cachedFlagOwner;
|
||||||
|
private byte[]? _cachedFlagLogo;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CastleSiegeConfiguration? Configuration { get; set; }
|
public CastleSiegeConfiguration? Configuration { get; set; }
|
||||||
|
|
||||||
@@ -97,6 +113,12 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
|||||||
{
|
{
|
||||||
await this.PersistStateAsync(gameContext, context).ConfigureAwait(false);
|
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 />
|
/// <inheritdoc />
|
||||||
@@ -262,6 +284,31 @@ public sealed class CastleSiegeEventPlugIn : IPeriodicTaskPlugIn, ISupportCustom
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.SetDefenseCount(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)
|
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<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)
|
private static async Task WarpRegisteredMembersToSiegeAsync(IGameContext gameContext, CastleSiegeContext context)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -22,4 +22,10 @@ public interface ICastleSiegeStatusViewPlugIn : IViewPlugIn
|
|||||||
/// <param name="hour">The remaining hours.</param>
|
/// <param name="hour">The remaining hours.</param>
|
||||||
/// <param name="minute">The remaining minutes.</param>
|
/// <param name="minute">The remaining minutes.</param>
|
||||||
ValueTask SetTimerAsync(byte hour, byte minute);
|
ValueTask SetTimerAsync(byte hour, byte minute);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the castle owner guild's 32-byte logo (C1 B9 02) so the client paints it onto the castle flags.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guildLogo">The owner guild's 32-byte logo/emblem.</param>
|
||||||
|
ValueTask SetCastleFlagAsync(ReadOnlyMemory<byte> guildLogo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,4 +75,30 @@ public class CastleSiegeStatusViewPlugIn : ICastleSiegeStatusViewPlugIn
|
|||||||
|
|
||||||
await connection.SendAsync(WritePacket).ConfigureAwait(false);
|
await connection.SendAsync(WritePacket).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask SetCastleFlagAsync(ReadOnlyMemory<byte> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user