From 7aae9dff0ce4b09207bb415b9c917afd76491fba Mon Sep 17 00:00:00 2001 From: Acentech Dev Date: Tue, 21 Jul 2026 03:16:29 +0300 Subject: [PATCH] feat(hs): disable wings in-event + periodic HUD-state packet/broadcast - HeykelSavasiContext.IsItemAllowedToEquip disallows wings/capes during Playing only (so entry is never rejected for wearing wings); auto-unequips any equipped wing/cape into a free inventory slot in OnGameStartAsync. - New S2C packet HeykelSavasiHudState (C1, code FB, length 11) carrying phase, team, counts, statue progress and remaining seconds, plus its view plugin/interface, broadcast once per second across registration/prep/ battle and one-shot on join/statue-break/game-end. --- .../C1-FB-HeykelSavasiHudState_by-server.md | 24 ++ docs/Packets/ServerToClient.md | 1 + .../MiniGames/HeykelSavasiContext.cs | 251 ++++++++++++++++++ .../MiniGames/IHeykelSavasiHudStatePlugIn.cs | 25 ++ .../MiniGames/HeykelSavasiHudStatePlugIn.cs | 39 +++ .../ServerToClient/ConnectionExtensions.cs | 40 +++ .../ServerToClient/ServerToClientPackets.cs | 133 ++++++++++ .../ServerToClient/ServerToClientPackets.xml | 51 ++++ .../ServerToClientPacketsRef.cs | 133 ++++++++++ 9 files changed, 697 insertions(+) create mode 100644 docs/Packets/C1-FB-HeykelSavasiHudState_by-server.md create mode 100644 src/GameLogic/Views/MiniGames/IHeykelSavasiHudStatePlugIn.cs create mode 100644 src/GameServer/RemoteView/MiniGames/HeykelSavasiHudStatePlugIn.cs diff --git a/docs/Packets/C1-FB-HeykelSavasiHudState_by-server.md b/docs/Packets/C1-FB-HeykelSavasiHudState_by-server.md new file mode 100644 index 0000000..da9fbd0 --- /dev/null +++ b/docs/Packets/C1-FB-HeykelSavasiHudState_by-server.md @@ -0,0 +1,24 @@ +# C1 FB - HeykelSavasiHudState (by server) + +## Is sent when + +Periodically (about once per second) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played. + +## Causes the following actions on the client side + +The client updates its Heykel Savasi HUD panel (team counts, statue progress, and remaining time). + +## Structure + +| Index | Length | Data Type | Value | Description | +|-------|--------|-----------|-------|-------------| +| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) | +| 1 | 1 | Byte | 11 | Packet header - length of the packet | +| 2 | 1 | Byte | 0xFB | Packet header - packet type identifier | +| 3 | 1 | Byte | | Phase; 0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended. | +| 4 | 1 | Byte | | MyTeam; 0 = none, 1 = red, 2 = blue. | +| 5 | 1 | Byte | | RedCount | +| 6 | 1 | Byte | | BlueCount | +| 7 | 1 | Byte | | RedProgress; The number (0-7) of statues the red team has destroyed of the blue team's line. | +| 8 | 1 | Byte | | BlueProgress; The number (0-7) of statues the blue team has destroyed of the red team's line. | +| 9 | 2 | ShortBigEndian | | RemainingSeconds; The number of seconds left in the current phase. | \ No newline at end of file diff --git a/docs/Packets/ServerToClient.md b/docs/Packets/ServerToClient.md index 79aca78..8fa178f 100644 --- a/docs/Packets/ServerToClient.md +++ b/docs/Packets/ServerToClient.md @@ -242,3 +242,4 @@ * [C2 F6 1B - QuestStateExtended (by server)](C2-F6-1B-QuestStateExtended_by-server.md) * [C3 F9 01 - OpenNpcDialog (by server)](C3-F9-01-OpenNpcDialog_by-server.md) * [C1 FA - HeykelSavasiOpenTeamPanel (by server)](C1-FA-HeykelSavasiOpenTeamPanel_by-server.md) + * [C1 FB - HeykelSavasiHudState (by server)](C1-FB-HeykelSavasiHudState_by-server.md) diff --git a/src/GameLogic/MiniGames/HeykelSavasiContext.cs b/src/GameLogic/MiniGames/HeykelSavasiContext.cs index 96b3ef8..e17dca3 100644 --- a/src/GameLogic/MiniGames/HeykelSavasiContext.cs +++ b/src/GameLogic/MiniGames/HeykelSavasiContext.cs @@ -8,11 +8,14 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using MUnique.OpenMU.DataModel; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.GameLogic.PlayerActions.MiniGames; +using MUnique.OpenMU.GameLogic.Views.MiniGames; using MUnique.OpenMU.Pathfinding; /// @@ -138,11 +141,38 @@ public class HeykelSavasiContext : MiniGameContext new(42, 94), }; + /// + /// The duration of the preparation countdown between the entrance closing () + /// and the battle starting (). Mirrors the hard-coded + /// countdownMessageDuration in 's own game loop (MiniGameContext.cs:709), + /// which isn't exposed to subclasses, so the HUD countdown re-declares the same constant here. + /// + private static readonly TimeSpan PreparationDuration = TimeSpan.FromSeconds(30); + private readonly IGameContext _gameContext; private readonly IMapInitializer _mapInitializer; private readonly ConcurrentDictionary _teams = new(); + /// + /// UTC timestamp of when this context was constructed, used as the start reference for estimating the + /// remaining time of the registration phase () in the periodic HUD broadcast + /// (see ). + /// + private readonly DateTime _createdAtUtc = DateTime.UtcNow; + + /// + /// UTC timestamp of the first observed transition into (entrance closed, + /// preparation countdown running), lazily set by . + /// + private DateTime? _closedAtUtc; + + /// + /// UTC timestamp of the first observed transition into (battle started), + /// lazily set by . + /// + private DateTime? _playingAtUtc; + /// /// Maps a currently-alive, spawned statue NPC to the team it defends and its index (0..6) in that /// team's statue line. Populated by , consumed (and removed) by @@ -175,6 +205,8 @@ public class HeykelSavasiContext : MiniGameContext { this._gameContext = gameContext; this._mapInitializer = mapInitializer; + + _ = Task.Run(() => this.HudBroadcastLoopAsync(this.GameEndedToken), this.GameEndedToken); } /// @@ -186,6 +218,31 @@ public class HeykelSavasiContext : MiniGameContext /// public override bool AllowPlayerKilling => this.State == MiniGameState.Playing; + /// + /// + /// Disallows wings and capes (see ) once the battle has started + /// (), so a player cannot re-equip one after + /// auto-unequipped it at battle start (see ). + /// This check is deliberately scoped to only (not + /// or ): calls this method (via its + /// private AreEquippedItemsAllowedAsync) while is still + /// to decide whether to REJECT entry outright for a disallowed equipped item - + /// which we do not want here (a player wearing wings should still be able to join; they get unequipped + /// automatically once the battle starts). Scoping the check to keeps entry + /// working while still blocking any (re-)equip attempt during the battle itself, via the other call site of + /// this method in MoveItemAction.CanMoveAsync (which checks player.CurrentMiniGame.IsItemAllowedToEquip + /// on every equip attempt, regardless of state). + /// + public override bool IsItemAllowedToEquip(Item item) + { + if (this.State == MiniGameState.Playing && IsWingOrCape(item)) + { + return false; + } + + return base.IsItemAllowedToEquip(item); + } + /// /// Gets the current number of players assigned to the given team. /// @@ -322,6 +379,13 @@ public class HeykelSavasiContext : MiniGameContext } } + if (success) + { + // One-shot HUD broadcast so every already-entered player sees the updated team counts immediately, + // rather than waiting for the next tick of the periodic broadcast (see HudBroadcastLoopAsync). + await this.BroadcastHudStateAsync().ConfigureAwait(false); + } + return success; } @@ -367,6 +431,13 @@ public class HeykelSavasiContext : MiniGameContext await player.WarpToAsync(this.GetTeamSpawnGate(this.GetTeam(player))).ConfigureAwait(false); } + // Players must be wingless for the battle; auto-unequip any wing/cape now that the entrance is closed + // and the teams are final (see IsItemAllowedToEquip/IsWingOrCape/UnequipWingsAndCapesAsync). + foreach (var player in players) + { + await this.UnequipWingsAndCapesAsync(player).ConfigureAwait(false); + } + // Spawn the first (outermost) statue of each team's line; the opposing team attacks it. await this.SpawnStatueAsync(HeykelSavasiTeam.Red, 0).ConfigureAwait(false); await this.SpawnStatueAsync(HeykelSavasiTeam.Blue, 0).ConfigureAwait(false); @@ -403,6 +474,12 @@ public class HeykelSavasiContext : MiniGameContext } } + // Final HUD broadcast: by now MiniGameContext.State is already MiniGameState.Ended (StopAsync sets it + // before calling this method), so ComputeHudPhaseAndRemaining naturally reports phase 3/0 seconds. This + // is needed because the periodic loop (HudBroadcastLoopAsync) stops exactly at this same state + // transition (it runs off GameEndedToken, which StopAsync cancels right before setting State to Ended). + await this.BroadcastHudStateAsync().ConfigureAwait(false); + await base.GameEndedAsync(finishers).ConfigureAwait(false); } @@ -510,6 +587,69 @@ public class HeykelSavasiContext : MiniGameContext _ => HeykelSavasiTeam.None, }; + /// + /// Determines whether is a wing or a cape, which are disallowed during the battle + /// (see ) and auto-unequipped at battle start (see + /// ). + /// + /// The item to check. + /// true if is a wing or cape; otherwise, false. + /// + /// Almost all wings AND capes share ItemDefinition.Group 12 (see + /// Persistence.Initialization.VersionSeasonSix.Items.Wings.CreateWing, which sets wing.Group = 12; + /// for every wing/cape it creates); the single exception is "Cape of Lord" (group 12, number 30 at creation), + /// which is deliberately reassigned to group 13 right after creation (capeOfLord.Group = 13; in + /// Wings.Initialize) so it is special-cased here by its fixed (group, number) pair. + /// + private static bool IsWingOrCape(Item item) + { + if (item.Definition is not { } definition) + { + return false; + } + + return (definition.Group, definition.Number) switch + { + (12, _) => true, // Wings, including most capes (Cape of Fighter/Emperor/Overrule, Poison/Warrior Cape, ...). + (13, 30) => true, // Cape of Lord, the one cape reassigned to its own item group. + _ => false, + }; + } + + /// + /// Auto-unequips any wing or cape (see ) currently equipped by , + /// moving it to a free general inventory slot so its stats/appearance are removed (see + /// ), without deleting it. Called from + /// once the battle starts. If no free inventory slot is available, the item is + /// deliberately left equipped (logged) rather than risking data loss. + /// + /// The player whose equipped wings/capes should be unequipped. + private async ValueTask UnequipWingsAndCapesAsync(Player player) + { + if (player.Inventory is not { } inventory) + { + return; + } + + // Snapshot first: EquippedItems is a live view over the equip slots, and RemoveItemAsync below mutates it. + var wingItems = inventory.EquippedItems.Where(IsWingOrCape).ToList(); + foreach (var item in wingItems) + { + var freeSlot = inventory.CheckInvSpace(item); + if (freeSlot is null) + { + this.Logger.LogWarning("{context}: Player {player} has no free inventory slot to unequip {item}; leaving it equipped.", this, player, item); + continue; + } + + await inventory.RemoveItemAsync(item).ConfigureAwait(false); + if (!await inventory.AddItemAsync(freeSlot.Value, item).ConfigureAwait(false)) + { + this.Logger.LogError("{context}: Failed to move unequipped item {item} of player {player} to inventory slot {slot} after removal from the equip slot; the item may now be lost.", this, item, player, freeSlot.Value); + } + } + } + /// /// Pure state transition shared by the runtime death handler () and the /// test-only : records the attacker's progress and, once @@ -544,6 +684,11 @@ public class HeykelSavasiContext : MiniGameContext { try { + // One-shot HUD broadcast: the statue's progress counter was already updated synchronously by + // HandleStatueDestroyed (called from OnDestructibleDied before this method was scheduled), so every + // player sees the new progress immediately rather than waiting for the next periodic tick. + await this.BroadcastHudStateAsync().ConfigureAwait(false); + await this.ApplyStatueBuffToTeamAsync(attacker, index).ConfigureAwait(false); if (this.GetWinner() == attacker) @@ -716,6 +861,112 @@ public class HeykelSavasiContext : MiniGameContext /// The player who (re)spawned. public ValueTask OnPlayerRespawnedAsync(Player player) => this.ReapplyBuffsAsync(player); + /// + /// Periodically (about once per second) broadcasts the Heykel Savasi HUD state + /// () to every entered player, covering the registration phase + /// (), the preparation countdown (), and + /// the battle itself (). Started once from the constructor via + /// Task.Run (mirroring 's own pattern for its private game loop). + /// + /// + /// Stops as soon as is cancelled, which happens exactly when + /// the game transitions to (see MiniGameContext.StopAsync, which + /// cancels the token and only then sets to + /// before calling ); the final "ended" broadcast is sent separately, from + /// itself, once is actually . + /// + /// The token which is cancelled once the game ends. + private async ValueTask HudBroadcastLoopAsync(CancellationToken cancellationToken) + { + try + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); + do + { + await this.BroadcastHudStateAsync().ConfigureAwait(false); + } + while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)); + } + catch (OperationCanceledException) + { + // Expected: the game ended; see GameEndedAsync for the final broadcast. + } + catch (Exception ex) + { + this.Logger.LogError(ex, "{context}: Error in the HUD broadcast loop.", this); + } + } + + /// + /// Sends the current Heykel Savasi HUD state (see ) to every + /// entered player via . Called both periodically (see + /// ) and as a one-shot update from (on a + /// successful join), (on a statue break), and + /// (final state). + /// + private async ValueTask BroadcastHudStateAsync() + { + var (phase, remainingSeconds) = this.ComputeHudPhaseAndRemaining(); + var redCount = (byte)this.TeamCount(HeykelSavasiTeam.Red); + var blueCount = (byte)this.TeamCount(HeykelSavasiTeam.Blue); + var redProgress = (byte)this.GetProgress(HeykelSavasiTeam.Red); + var blueProgress = (byte)this.GetProgress(HeykelSavasiTeam.Blue); + + await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync(p => + p.UpdateHudStateAsync(phase, (byte)this.GetTeam(player), redCount, blueCount, redProgress, blueProgress, remainingSeconds)) + .AsTask()).ConfigureAwait(false); + } + + /// + /// Determines the current HUD phase and the estimated number of seconds remaining in it, purely from + /// and wall-clock timestamps recorded (lazily, on first observation) at + /// each relevant state transition. This is an approximation, not authoritative timing: + /// is set at construction time, which happens moments before MiniGameContext.RunGameAsync's own + /// Task.Run starts counting down , and + /// / are set by whichever caller (the periodic loop or a + /// one-shot broadcast) first observes the corresponding state - always within about a second of the real + /// transition, which is precise enough for a HUD countdown. + /// + /// The current phase (0-3, see ) and the + /// estimated remaining seconds in that phase. + private (byte Phase, ushort RemainingSeconds) ComputeHudPhaseAndRemaining() + { + var now = DateTime.UtcNow; + switch (this.State) + { + case MiniGameState.Open: + return (0, ClampToUInt16Seconds(this.Definition.EnterDuration - (now - this._createdAtUtc))); + + case MiniGameState.Closed: + this._closedAtUtc ??= now; + return (1, ClampToUInt16Seconds(PreparationDuration - (now - this._closedAtUtc.Value))); + + case MiniGameState.Playing: + this._playingAtUtc ??= now; + return (2, ClampToUInt16Seconds(this.Definition.GameDuration - (now - this._playingAtUtc.Value))); + + default: + // Undefined, Ended, or Disposed: the event is over (or hasn't been set up yet). + return (3, 0); + } + } + + /// + /// Converts to a seconds count for the HUD packet, + /// clamping negative durations to 0. + /// + /// The remaining duration. + private static ushort ClampToUInt16Seconds(TimeSpan remaining) + { + if (remaining <= TimeSpan.Zero) + { + return 0; + } + + var seconds = remaining.TotalSeconds; + return seconds >= ushort.MaxValue ? ushort.MaxValue : (ushort)seconds; + } + /// /// Pure, dependency-free tracker for statue-break progress and win detection. Extracted out of /// so it can be unit-tested directly, without constructing a full diff --git a/src/GameLogic/Views/MiniGames/IHeykelSavasiHudStatePlugIn.cs b/src/GameLogic/Views/MiniGames/IHeykelSavasiHudStatePlugIn.cs new file mode 100644 index 0000000..fb65b17 --- /dev/null +++ b/src/GameLogic/Views/MiniGames/IHeykelSavasiHudStatePlugIn.cs @@ -0,0 +1,25 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.Views.MiniGames; + +/// +/// Interface of a view whose implementation updates the Heykel Savasi (Statue War) HUD panel, which is sent +/// periodically (about once per second) while the event is open for registration, in its pre-battle +/// countdown, or being played. +/// +public interface IHeykelSavasiHudStatePlugIn : IViewPlugIn +{ + /// + /// Updates the Heykel Savasi HUD state. + /// + /// The current phase of the event (0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended). + /// The team of the receiving player (0 = none, 1 = red, 2 = blue). + /// The current number of players in the red team. + /// The current number of players in the blue team. + /// The number (0-7) of statues the red team has destroyed of the blue team's line. + /// The number (0-7) of statues the blue team has destroyed of the red team's line. + /// The number of seconds left in the current phase. + ValueTask UpdateHudStateAsync(byte phase, byte myTeam, byte redCount, byte blueCount, byte redProgress, byte blueProgress, ushort remainingSeconds); +} diff --git a/src/GameServer/RemoteView/MiniGames/HeykelSavasiHudStatePlugIn.cs b/src/GameServer/RemoteView/MiniGames/HeykelSavasiHudStatePlugIn.cs new file mode 100644 index 0000000..976388b --- /dev/null +++ b/src/GameServer/RemoteView/MiniGames/HeykelSavasiHudStatePlugIn.cs @@ -0,0 +1,39 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.RemoteView.MiniGames; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Views.MiniGames; +using MUnique.OpenMU.Network.Packets.ServerToClient; +using MUnique.OpenMU.PlugIns; + +/// +/// The default implementation of the which is forwarding everything to the game client with specific data packets. +/// +[PlugIn] +[Guid("ef0cc7e9-07c6-4e60-afae-f8dd19332ad7")] +public class HeykelSavasiHudStatePlugIn : IHeykelSavasiHudStatePlugIn +{ + private readonly RemotePlayer _player; + + /// + /// Initializes a new instance of the class. + /// + /// The player. + public HeykelSavasiHudStatePlugIn(RemotePlayer player) => this._player = player; + + /// + public async ValueTask UpdateHudStateAsync(byte phase, byte myTeam, byte redCount, byte blueCount, byte redProgress, byte blueProgress, ushort remainingSeconds) + { + var connection = this._player.Connection; + if (connection is null) + { + return; + } + + await connection.SendHeykelSavasiHudStateAsync(phase, myTeam, redCount, blueCount, redProgress, blueProgress, remainingSeconds).ConfigureAwait(false); + } +} diff --git a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs index 9e5dc4b..9668f9c 100644 --- a/src/Network/Packets/ServerToClient/ConnectionExtensions.cs +++ b/src/Network/Packets/ServerToClient/ConnectionExtensions.cs @@ -6317,5 +6317,45 @@ public static class ConnectionExtensions return packet.Header.Length; } + await connection.SendAsync(WritePacket).ConfigureAwait(false); + } + + /// + /// Sends a to this connection. + /// + /// The connection. + /// 0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended. + /// 0 = none, 1 = red, 2 = blue. + /// The red count. + /// The blue count. + /// The number (0-7) of statues the red team has destroyed of the blue team's line. + /// The number (0-7) of statues the blue team has destroyed of the red team's line. + /// The number of seconds left in the current phase. + /// + /// Is sent by the server when: Periodically (about once per second) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played. + /// Causes reaction on client side: The client updates its Heykel Savasi HUD panel (team counts, statue progress, and remaining time). + /// + public static async ValueTask SendHeykelSavasiHudStateAsync(this IConnection? connection, byte @phase, byte @myTeam, byte @redCount, byte @blueCount, byte @redProgress, byte @blueProgress, ushort @remainingSeconds) + { + if (connection is null) + { + return; + } + + int WritePacket() + { + var length = HeykelSavasiHudStateRef.Length; + var packet = new HeykelSavasiHudStateRef(connection.Output.GetSpan(length)[..length]); + packet.Phase = @phase; + packet.MyTeam = @myTeam; + packet.RedCount = @redCount; + packet.BlueCount = @blueCount; + packet.RedProgress = @redProgress; + packet.BlueProgress = @blueProgress; + packet.RemainingSeconds = @remainingSeconds; + + return packet.Header.Length; + } + await connection.SendAsync(WritePacket).ConfigureAwait(false); }} \ No newline at end of file diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs index 4544da8..4d5d3b0 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs @@ -30759,6 +30759,139 @@ public readonly struct HeykelSavasiOpenTeamPanel /// The packet as byte span. public static implicit operator Memory(HeykelSavasiOpenTeamPanel packet) => packet._data; } + + +/// +/// Is sent by the server when: Periodically (about once per second) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played. +/// Causes reaction on client side: The client updates its Heykel Savasi HUD panel (team counts, statue progress, and remaining time). +/// +public readonly struct HeykelSavasiHudState +{ + private readonly Memory _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public HeykelSavasiHudState(Memory data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private HeykelSavasiHudState(Memory data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xFB; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 11; + + /// + /// Gets the header of this packet. + /// + public C1Header Header => new (this._data); + + /// + /// Gets or sets 0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended. + /// + public byte Phase + { + get => this._data.Span[3]; + set => this._data.Span[3] = value; + } + + /// + /// Gets or sets 0 = none, 1 = red, 2 = blue. + /// + public byte MyTeam + { + get => this._data.Span[4]; + set => this._data.Span[4] = value; + } + + /// + /// Gets or sets the red count. + /// + public byte RedCount + { + get => this._data.Span[5]; + set => this._data.Span[5] = value; + } + + /// + /// Gets or sets the blue count. + /// + public byte BlueCount + { + get => this._data.Span[6]; + set => this._data.Span[6] = value; + } + + /// + /// Gets or sets the number (0-7) of statues the red team has destroyed of the blue team's line. + /// + public byte RedProgress + { + get => this._data.Span[7]; + set => this._data.Span[7] = value; + } + + /// + /// Gets or sets the number (0-7) of statues the blue team has destroyed of the red team's line. + /// + public byte BlueProgress + { + get => this._data.Span[8]; + set => this._data.Span[8] = value; + } + + /// + /// Gets or sets the number of seconds left in the current phase. + /// + public ushort RemainingSeconds + { + get => ReadUInt16BigEndian(this._data.Span[9..]); + set => WriteUInt16BigEndian(this._data.Span[9..], value); + } + + /// + /// Performs an implicit conversion from a Memory of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator HeykelSavasiHudState(Memory packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Memory of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Memory(HeykelSavasiHudState packet) => packet._data; +} /// /// Defines the role of a guild member. /// diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml index 379b709..02d62f5 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml +++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml @@ -11088,6 +11088,57 @@ + + C1Header + FB + HeykelSavasiHudState + 11 + ServerToClient + Periodically (about once per second) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played. + The client updates its Heykel Savasi HUD panel (team counts, statue progress, and remaining time). + + + 3 + Byte + Phase + 0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended. + + + 4 + Byte + MyTeam + 0 = none, 1 = red, 2 = blue. + + + 5 + Byte + RedCount + + + 6 + Byte + BlueCount + + + 7 + Byte + RedProgress + The number (0-7) of statues the red team has destroyed of the blue team's line. + + + 8 + Byte + BlueProgress + The number (0-7) of statues the blue team has destroyed of the red team's line. + + + 9 + ShortBigEndian + RemainingSeconds + The number of seconds left in the current phase. + + + diff --git a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs index 2921413..81eb413 100644 --- a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs +++ b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs @@ -29122,3 +29122,136 @@ public readonly ref struct HeykelSavasiOpenTeamPanelRef /// The packet as byte span. public static implicit operator Span(HeykelSavasiOpenTeamPanelRef packet) => packet._data; } + + +/// +/// Is sent by the server when: Periodically (about once per second) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played. +/// Causes reaction on client side: The client updates its Heykel Savasi HUD panel (team counts, statue progress, and remaining time). +/// +public readonly ref struct HeykelSavasiHudStateRef +{ + private readonly Span _data; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + public HeykelSavasiHudStateRef(Span data) + : this(data, true) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying data. + /// If set to true, the header data is automatically initialized and written to the underlying span. + private HeykelSavasiHudStateRef(Span data, bool initialize) + { + this._data = data; + if (initialize) + { + var header = this.Header; + header.Type = HeaderType; + header.Code = Code; + header.Length = (byte)Math.Min(data.Length, Length); + } + } + + /// + /// Gets the header type of this data packet. + /// + public static byte HeaderType => 0xC1; + + /// + /// Gets the operation code of this data packet. + /// + public static byte Code => 0xFB; + + /// + /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed. + /// + public static int Length => 11; + + /// + /// Gets the header of this packet. + /// + public C1HeaderRef Header => new (this._data); + + /// + /// Gets or sets 0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended. + /// + public byte Phase + { + get => this._data[3]; + set => this._data[3] = value; + } + + /// + /// Gets or sets 0 = none, 1 = red, 2 = blue. + /// + public byte MyTeam + { + get => this._data[4]; + set => this._data[4] = value; + } + + /// + /// Gets or sets the red count. + /// + public byte RedCount + { + get => this._data[5]; + set => this._data[5] = value; + } + + /// + /// Gets or sets the blue count. + /// + public byte BlueCount + { + get => this._data[6]; + set => this._data[6] = value; + } + + /// + /// Gets or sets the number (0-7) of statues the red team has destroyed of the blue team's line. + /// + public byte RedProgress + { + get => this._data[7]; + set => this._data[7] = value; + } + + /// + /// Gets or sets the number (0-7) of statues the blue team has destroyed of the red team's line. + /// + public byte BlueProgress + { + get => this._data[8]; + set => this._data[8] = value; + } + + /// + /// Gets or sets the number of seconds left in the current phase. + /// + public ushort RemainingSeconds + { + get => ReadUInt16BigEndian(this._data[9..]); + set => WriteUInt16BigEndian(this._data[9..], value); + } + + /// + /// Performs an implicit conversion from a Span of bytes to a . + /// + /// The packet as span. + /// The packet as struct. + public static implicit operator HeykelSavasiHudStateRef(Span packet) => new (packet, false); + + /// + /// Performs an implicit conversion from to a Span of bytes. + /// + /// The packet as struct. + /// The packet as byte span. + public static implicit operator Span(HeykelSavasiHudStateRef packet) => packet._data; +}