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.
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
@@ -138,11 +141,38 @@ public class HeykelSavasiContext : MiniGameContext
|
||||
new(42, 94),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The duration of the preparation countdown between the entrance closing (<see cref="MiniGameState.Closed"/>)
|
||||
/// and the battle starting (<see cref="MiniGameState.Playing"/>). Mirrors the hard-coded
|
||||
/// <c>countdownMessageDuration</c> in <see cref="MiniGameContext"/>'s own game loop (MiniGameContext.cs:709),
|
||||
/// which isn't exposed to subclasses, so the HUD countdown re-declares the same constant here.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan PreparationDuration = TimeSpan.FromSeconds(30);
|
||||
|
||||
private readonly IGameContext _gameContext;
|
||||
private readonly IMapInitializer _mapInitializer;
|
||||
|
||||
private readonly ConcurrentDictionary<Player, HeykelSavasiTeam> _teams = new();
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp of when this context was constructed, used as the start reference for estimating the
|
||||
/// remaining time of the registration phase (<see cref="MiniGameState.Open"/>) in the periodic HUD broadcast
|
||||
/// (see <see cref="ComputeHudPhaseAndRemaining"/>).
|
||||
/// </summary>
|
||||
private readonly DateTime _createdAtUtc = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp of the first observed transition into <see cref="MiniGameState.Closed"/> (entrance closed,
|
||||
/// preparation countdown running), lazily set by <see cref="ComputeHudPhaseAndRemaining"/>.
|
||||
/// </summary>
|
||||
private DateTime? _closedAtUtc;
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp of the first observed transition into <see cref="MiniGameState.Playing"/> (battle started),
|
||||
/// lazily set by <see cref="ComputeHudPhaseAndRemaining"/>.
|
||||
/// </summary>
|
||||
private DateTime? _playingAtUtc;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="SpawnStatueAsync"/>, 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -186,6 +218,31 @@ public class HeykelSavasiContext : MiniGameContext
|
||||
/// </remarks>
|
||||
public override bool AllowPlayerKilling => this.State == MiniGameState.Playing;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Disallows wings and capes (see <see cref="IsWingOrCape"/>) once the battle has started
|
||||
/// (<see cref="MiniGameState.Playing"/>), so a player cannot re-equip one after
|
||||
/// <see cref="UnequipWingsAndCapesAsync"/> auto-unequipped it at battle start (see <see cref="OnGameStartAsync"/>).
|
||||
/// This check is deliberately scoped to <see cref="MiniGameState.Playing"/> only (not <see cref="MiniGameState.Open"/>
|
||||
/// or <see cref="MiniGameState.Closed"/>): <see cref="MiniGameContext.TryEnterAsync"/> calls this method (via its
|
||||
/// private <c>AreEquippedItemsAllowedAsync</c>) while <see cref="MiniGameContext.State"/> is still
|
||||
/// <see cref="MiniGameState.Open"/> 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 <see cref="MiniGameState.Playing"/> keeps entry
|
||||
/// working while still blocking any (re-)equip attempt during the battle itself, via the other call site of
|
||||
/// this method in <c>MoveItemAction.CanMoveAsync</c> (which checks <c>player.CurrentMiniGame.IsItemAllowedToEquip</c>
|
||||
/// on every equip attempt, regardless of state).
|
||||
/// </remarks>
|
||||
public override bool IsItemAllowedToEquip(Item item)
|
||||
{
|
||||
if (this.State == MiniGameState.Playing && IsWingOrCape(item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.IsItemAllowedToEquip(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current number of players assigned to the given team.
|
||||
/// </summary>
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether <paramref name="item"/> is a wing or a cape, which are disallowed during the battle
|
||||
/// (see <see cref="IsItemAllowedToEquip"/>) and auto-unequipped at battle start (see
|
||||
/// <see cref="UnequipWingsAndCapesAsync"/>).
|
||||
/// </summary>
|
||||
/// <param name="item">The item to check.</param>
|
||||
/// <returns><c>true</c> if <paramref name="item"/> is a wing or cape; otherwise, <c>false</c>.</returns>
|
||||
/// <remarks>
|
||||
/// Almost all wings AND capes share <c>ItemDefinition.Group</c> 12 (see
|
||||
/// <c>Persistence.Initialization.VersionSeasonSix.Items.Wings.CreateWing</c>, which sets <c>wing.Group = 12;</c>
|
||||
/// 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 (<c>capeOfLord.Group = 13;</c> in
|
||||
/// <c>Wings.Initialize</c>) so it is special-cased here by its fixed (group, number) pair.
|
||||
/// </remarks>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-unequips any wing or cape (see <see cref="IsWingOrCape"/>) currently equipped by <paramref name="player"/>,
|
||||
/// moving it to a free general inventory slot so its stats/appearance are removed (see
|
||||
/// <see cref="InventoryStorage.EquippedItemsChanged"/>), without deleting it. Called from
|
||||
/// <see cref="OnGameStartAsync"/> once the battle starts. If no free inventory slot is available, the item is
|
||||
/// deliberately left equipped (logged) rather than risking data loss.
|
||||
/// </summary>
|
||||
/// <param name="player">The player whose equipped wings/capes should be unequipped.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure state transition shared by the runtime death handler (<see cref="OnDestructibleDied"/>) and the
|
||||
/// test-only <see cref="RegisterStatueDestroyedForTest"/>: 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
|
||||
/// <param name="player">The player who (re)spawned.</param>
|
||||
public ValueTask OnPlayerRespawnedAsync(Player player) => this.ReapplyBuffsAsync(player);
|
||||
|
||||
/// <summary>
|
||||
/// Periodically (about once per second) broadcasts the Heykel Savasi HUD state
|
||||
/// (<see cref="IHeykelSavasiHudStatePlugIn"/>) to every entered player, covering the registration phase
|
||||
/// (<see cref="MiniGameState.Open"/>), the preparation countdown (<see cref="MiniGameState.Closed"/>), and
|
||||
/// the battle itself (<see cref="MiniGameState.Playing"/>). Started once from the constructor via
|
||||
/// <c>Task.Run</c> (mirroring <see cref="MiniGameContext"/>'s own pattern for its private game loop).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Stops as soon as <see cref="MiniGameContext.GameEndedToken"/> is cancelled, which happens exactly when
|
||||
/// the game transitions to <see cref="MiniGameState.Ended"/> (see <c>MiniGameContext.StopAsync</c>, which
|
||||
/// cancels the token and only then sets <see cref="MiniGameContext.State"/> to <see cref="MiniGameState.Ended"/>
|
||||
/// before calling <see cref="GameEndedAsync"/>); the final "ended" broadcast is sent separately, from
|
||||
/// <see cref="GameEndedAsync"/> itself, once <see cref="MiniGameContext.State"/> is actually <see cref="MiniGameState.Ended"/>.
|
||||
/// </remarks>
|
||||
/// <param name="cancellationToken">The token which is cancelled once the game ends.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the current Heykel Savasi HUD state (see <see cref="ComputeHudPhaseAndRemaining"/>) to every
|
||||
/// entered player via <see cref="IHeykelSavasiHudStatePlugIn"/>. Called both periodically (see
|
||||
/// <see cref="HudBroadcastLoopAsync"/>) and as a one-shot update from <see cref="TryJoinTeamAsync"/> (on a
|
||||
/// successful join), <see cref="OnStatueDestroyedSideEffectsAsync"/> (on a statue break), and
|
||||
/// <see cref="GameEndedAsync"/> (final state).
|
||||
/// </summary>
|
||||
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<IHeykelSavasiHudStatePlugIn>(p =>
|
||||
p.UpdateHudStateAsync(phase, (byte)this.GetTeam(player), redCount, blueCount, redProgress, blueProgress, remainingSeconds))
|
||||
.AsTask()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the current HUD phase and the estimated number of seconds remaining in it, purely from
|
||||
/// <see cref="MiniGameContext.State"/> and wall-clock timestamps recorded (lazily, on first observation) at
|
||||
/// each relevant state transition. This is an approximation, not authoritative timing: <see cref="_createdAtUtc"/>
|
||||
/// is set at construction time, which happens moments before <c>MiniGameContext.RunGameAsync</c>'s own
|
||||
/// <c>Task.Run</c> starts counting down <see cref="MiniGameDefinition.EnterDuration"/>, and
|
||||
/// <see cref="_closedAtUtc"/>/<see cref="_playingAtUtc"/> 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.
|
||||
/// </summary>
|
||||
/// <returns>The current phase (0-3, see <see cref="IHeykelSavasiHudStatePlugIn.UpdateHudStateAsync"/>) and the
|
||||
/// estimated remaining seconds in that phase.</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts <paramref name="remaining"/> to a <see cref="ushort"/> seconds count for the HUD packet,
|
||||
/// clamping negative durations to 0.
|
||||
/// </summary>
|
||||
/// <param name="remaining">The remaining duration.</param>
|
||||
private static ushort ClampToUInt16Seconds(TimeSpan remaining)
|
||||
{
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var seconds = remaining.TotalSeconds;
|
||||
return seconds >= ushort.MaxValue ? ushort.MaxValue : (ushort)seconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure, dependency-free tracker for statue-break progress and win detection. Extracted out of
|
||||
/// <see cref="HeykelSavasiContext"/> so it can be unit-tested directly, without constructing a full
|
||||
|
||||
25
src/GameLogic/Views/MiniGames/IHeykelSavasiHudStatePlugIn.cs
Normal file
25
src/GameLogic/Views/MiniGames/IHeykelSavasiHudStatePlugIn.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// <copyright file="IHeykelSavasiHudStatePlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.Views.MiniGames;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IHeykelSavasiHudStatePlugIn : IViewPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates the Heykel Savasi HUD state.
|
||||
/// </summary>
|
||||
/// <param name="phase">The current phase of the event (0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended).</param>
|
||||
/// <param name="myTeam">The team of the receiving player (0 = none, 1 = red, 2 = blue).</param>
|
||||
/// <param name="redCount">The current number of players in the red team.</param>
|
||||
/// <param name="blueCount">The current number of players in the blue team.</param>
|
||||
/// <param name="redProgress">The number (0-7) of statues the red team has destroyed of the blue team's line.</param>
|
||||
/// <param name="blueProgress">The number (0-7) of statues the blue team has destroyed of the red team's line.</param>
|
||||
/// <param name="remainingSeconds">The number of seconds left in the current phase.</param>
|
||||
ValueTask UpdateHudStateAsync(byte phase, byte myTeam, byte redCount, byte blueCount, byte redProgress, byte blueProgress, ushort remainingSeconds);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// <copyright file="HeykelSavasiHudStatePlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// The default implementation of the <see cref="IHeykelSavasiHudStatePlugIn"/> which is forwarding everything to the game client with specific data packets.
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Guid("ef0cc7e9-07c6-4e60-afae-f8dd19332ad7")]
|
||||
public class HeykelSavasiHudStatePlugIn : IHeykelSavasiHudStatePlugIn
|
||||
{
|
||||
private readonly RemotePlayer _player;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeykelSavasiHudStatePlugIn"/> class.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
public HeykelSavasiHudStatePlugIn(RemotePlayer player) => this._player = player;
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -6317,5 +6317,45 @@ public static class ConnectionExtensions
|
||||
return packet.Header.Length;
|
||||
}
|
||||
|
||||
await connection.SendAsync(WritePacket).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a <see cref="HeykelSavasiHudState" /> to this connection.
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection.</param>
|
||||
/// <param name="phase">0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended.</param>
|
||||
/// <param name="myTeam">0 = none, 1 = red, 2 = blue.</param>
|
||||
/// <param name="redCount">The red count.</param>
|
||||
/// <param name="blueCount">The blue count.</param>
|
||||
/// <param name="redProgress">The number (0-7) of statues the red team has destroyed of the blue team's line.</param>
|
||||
/// <param name="blueProgress">The number (0-7) of statues the blue team has destroyed of the red team's line.</param>
|
||||
/// <param name="remainingSeconds">The number of seconds left in the current phase.</param>
|
||||
/// <remarks>
|
||||
/// 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).
|
||||
/// </remarks>
|
||||
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);
|
||||
}}
|
||||
@@ -30759,6 +30759,139 @@ public readonly struct HeykelSavasiOpenTeamPanel
|
||||
/// <returns>The packet as byte span.</returns>
|
||||
public static implicit operator Memory<byte>(HeykelSavasiOpenTeamPanel packet) => packet._data;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public readonly struct HeykelSavasiHudState
|
||||
{
|
||||
private readonly Memory<byte> _data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeykelSavasiHudState"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="data">The underlying data.</param>
|
||||
public HeykelSavasiHudState(Memory<byte> data)
|
||||
: this(data, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeykelSavasiHudState"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="data">The underlying data.</param>
|
||||
/// <param name="initialize">If set to <c>true</c>, the header data is automatically initialized and written to the underlying span.</param>
|
||||
private HeykelSavasiHudState(Memory<byte> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the header type of this data packet.
|
||||
/// </summary>
|
||||
public static byte HeaderType => 0xC1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operation code of this data packet.
|
||||
/// </summary>
|
||||
public static byte Code => 0xFB;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
|
||||
/// </summary>
|
||||
public static int Length => 11;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the header of this packet.
|
||||
/// </summary>
|
||||
public C1Header Header => new (this._data);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets 0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended.
|
||||
/// </summary>
|
||||
public byte Phase
|
||||
{
|
||||
get => this._data.Span[3];
|
||||
set => this._data.Span[3] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets 0 = none, 1 = red, 2 = blue.
|
||||
/// </summary>
|
||||
public byte MyTeam
|
||||
{
|
||||
get => this._data.Span[4];
|
||||
set => this._data.Span[4] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the red count.
|
||||
/// </summary>
|
||||
public byte RedCount
|
||||
{
|
||||
get => this._data.Span[5];
|
||||
set => this._data.Span[5] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the blue count.
|
||||
/// </summary>
|
||||
public byte BlueCount
|
||||
{
|
||||
get => this._data.Span[6];
|
||||
set => this._data.Span[6] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number (0-7) of statues the red team has destroyed of the blue team's line.
|
||||
/// </summary>
|
||||
public byte RedProgress
|
||||
{
|
||||
get => this._data.Span[7];
|
||||
set => this._data.Span[7] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number (0-7) of statues the blue team has destroyed of the red team's line.
|
||||
/// </summary>
|
||||
public byte BlueProgress
|
||||
{
|
||||
get => this._data.Span[8];
|
||||
set => this._data.Span[8] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of seconds left in the current phase.
|
||||
/// </summary>
|
||||
public ushort RemainingSeconds
|
||||
{
|
||||
get => ReadUInt16BigEndian(this._data.Span[9..]);
|
||||
set => WriteUInt16BigEndian(this._data.Span[9..], value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from a Memory of bytes to a <see cref="HeykelSavasiHudState"/>.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet as span.</param>
|
||||
/// <returns>The packet as struct.</returns>
|
||||
public static implicit operator HeykelSavasiHudState(Memory<byte> packet) => new (packet, false);
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from <see cref="HeykelSavasiHudState"/> to a Memory of bytes.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet as struct.</param>
|
||||
/// <returns>The packet as byte span.</returns>
|
||||
public static implicit operator Memory<byte>(HeykelSavasiHudState packet) => packet._data;
|
||||
}
|
||||
/// <summary>
|
||||
/// Defines the role of a guild member.
|
||||
/// </summary>
|
||||
|
||||
@@ -11088,6 +11088,57 @@
|
||||
</Field>
|
||||
</Fields>
|
||||
</Packet>
|
||||
<Packet>
|
||||
<HeaderType>C1Header</HeaderType>
|
||||
<Code>FB</Code>
|
||||
<Name>HeykelSavasiHudState</Name>
|
||||
<Length>11</Length>
|
||||
<Direction>ServerToClient</Direction>
|
||||
<SentWhen>Periodically (about once per second) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played.</SentWhen>
|
||||
<CausedReaction>The client updates its Heykel Savasi HUD panel (team counts, statue progress, and remaining time).</CausedReaction>
|
||||
<Fields>
|
||||
<Field>
|
||||
<Index>3</Index>
|
||||
<Type>Byte</Type>
|
||||
<Name>Phase</Name>
|
||||
<Description>0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended.</Description>
|
||||
</Field>
|
||||
<Field>
|
||||
<Index>4</Index>
|
||||
<Type>Byte</Type>
|
||||
<Name>MyTeam</Name>
|
||||
<Description>0 = none, 1 = red, 2 = blue.</Description>
|
||||
</Field>
|
||||
<Field>
|
||||
<Index>5</Index>
|
||||
<Type>Byte</Type>
|
||||
<Name>RedCount</Name>
|
||||
</Field>
|
||||
<Field>
|
||||
<Index>6</Index>
|
||||
<Type>Byte</Type>
|
||||
<Name>BlueCount</Name>
|
||||
</Field>
|
||||
<Field>
|
||||
<Index>7</Index>
|
||||
<Type>Byte</Type>
|
||||
<Name>RedProgress</Name>
|
||||
<Description>The number (0-7) of statues the red team has destroyed of the blue team's line.</Description>
|
||||
</Field>
|
||||
<Field>
|
||||
<Index>8</Index>
|
||||
<Type>Byte</Type>
|
||||
<Name>BlueProgress</Name>
|
||||
<Description>The number (0-7) of statues the blue team has destroyed of the red team's line.</Description>
|
||||
</Field>
|
||||
<Field>
|
||||
<Index>9</Index>
|
||||
<Type>ShortBigEndian</Type>
|
||||
<Name>RemainingSeconds</Name>
|
||||
<Description>The number of seconds left in the current phase.</Description>
|
||||
</Field>
|
||||
</Fields>
|
||||
</Packet>
|
||||
</Packets>
|
||||
<Enums>
|
||||
<Enum>
|
||||
|
||||
@@ -29122,3 +29122,136 @@ public readonly ref struct HeykelSavasiOpenTeamPanelRef
|
||||
/// <returns>The packet as byte span.</returns>
|
||||
public static implicit operator Span<byte>(HeykelSavasiOpenTeamPanelRef packet) => packet._data;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public readonly ref struct HeykelSavasiHudStateRef
|
||||
{
|
||||
private readonly Span<byte> _data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeykelSavasiHudStateRef"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="data">The underlying data.</param>
|
||||
public HeykelSavasiHudStateRef(Span<byte> data)
|
||||
: this(data, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeykelSavasiHudStateRef"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="data">The underlying data.</param>
|
||||
/// <param name="initialize">If set to <c>true</c>, the header data is automatically initialized and written to the underlying span.</param>
|
||||
private HeykelSavasiHudStateRef(Span<byte> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the header type of this data packet.
|
||||
/// </summary>
|
||||
public static byte HeaderType => 0xC1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operation code of this data packet.
|
||||
/// </summary>
|
||||
public static byte Code => 0xFB;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
|
||||
/// </summary>
|
||||
public static int Length => 11;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the header of this packet.
|
||||
/// </summary>
|
||||
public C1HeaderRef Header => new (this._data);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets 0 = registration open, 1 = preparation countdown, 2 = battle, 3 = ended.
|
||||
/// </summary>
|
||||
public byte Phase
|
||||
{
|
||||
get => this._data[3];
|
||||
set => this._data[3] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets 0 = none, 1 = red, 2 = blue.
|
||||
/// </summary>
|
||||
public byte MyTeam
|
||||
{
|
||||
get => this._data[4];
|
||||
set => this._data[4] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the red count.
|
||||
/// </summary>
|
||||
public byte RedCount
|
||||
{
|
||||
get => this._data[5];
|
||||
set => this._data[5] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the blue count.
|
||||
/// </summary>
|
||||
public byte BlueCount
|
||||
{
|
||||
get => this._data[6];
|
||||
set => this._data[6] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number (0-7) of statues the red team has destroyed of the blue team's line.
|
||||
/// </summary>
|
||||
public byte RedProgress
|
||||
{
|
||||
get => this._data[7];
|
||||
set => this._data[7] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number (0-7) of statues the blue team has destroyed of the red team's line.
|
||||
/// </summary>
|
||||
public byte BlueProgress
|
||||
{
|
||||
get => this._data[8];
|
||||
set => this._data[8] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of seconds left in the current phase.
|
||||
/// </summary>
|
||||
public ushort RemainingSeconds
|
||||
{
|
||||
get => ReadUInt16BigEndian(this._data[9..]);
|
||||
set => WriteUInt16BigEndian(this._data[9..], value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from a Span of bytes to a <see cref="HeykelSavasiHudState"/>.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet as span.</param>
|
||||
/// <returns>The packet as struct.</returns>
|
||||
public static implicit operator HeykelSavasiHudStateRef(Span<byte> packet) => new (packet, false);
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from <see cref="HeykelSavasiHudState"/> to a Span of bytes.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet as struct.</param>
|
||||
/// <returns>The packet as byte span.</returns>
|
||||
public static implicit operator Span<byte>(HeykelSavasiHudStateRef packet) => packet._data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user