feat(hs): raise statue HP, transform players in-battle, per-player team roster packet, drop wing-unequip
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
30
docs/Packets/C1-FC-HeykelSavasiTeamRoster_by-server.md
Normal file
30
docs/Packets/C1-FC-HeykelSavasiTeamRoster_by-server.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# C1 FC - HeykelSavasiTeamRoster (by server)
|
||||||
|
|
||||||
|
## Is sent when
|
||||||
|
|
||||||
|
Periodically (about once every one to two seconds) 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 tints each listed nearby player red or blue according to its team.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
| Index | Length | Data Type | Value | Description |
|
||||||
|
|-------|--------|-----------|-------|-------------|
|
||||||
|
| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
|
||||||
|
| 1 | 1 | Byte | | Packet header - length of the packet |
|
||||||
|
| 2 | 1 | Byte | 0xFC | Packet header - packet type identifier |
|
||||||
|
| 3 | 1 | Byte | | Count; The number of player-team entries which follow. |
|
||||||
|
| 4 | PlayerTeam.Length * Count | Array of PlayerTeam | | Players |
|
||||||
|
|
||||||
|
### PlayerTeam Structure
|
||||||
|
|
||||||
|
Maps a player's network id to its Heykel Savasi team.
|
||||||
|
|
||||||
|
Length: 3 Bytes
|
||||||
|
|
||||||
|
| Index | Length | Data Type | Value | Description |
|
||||||
|
|-------|--------|-----------|-------|-------------|
|
||||||
|
| 0 | 2 | ShortBigEndian | | PlayerId; The network id of the player (same id used in the viewport/appearance packets). |
|
||||||
|
| 2 | 1 | Byte | | Team; 1 = red, 2 = blue. |
|
||||||
@@ -243,3 +243,4 @@
|
|||||||
* [C3 F9 01 - OpenNpcDialog (by server)](C3-F9-01-OpenNpcDialog_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 FA - HeykelSavasiOpenTeamPanel (by server)](C1-FA-HeykelSavasiOpenTeamPanel_by-server.md)
|
||||||
* [C1 FB - HeykelSavasiHudState (by server)](C1-FB-HeykelSavasiHudState_by-server.md)
|
* [C1 FB - HeykelSavasiHudState (by server)](C1-FB-HeykelSavasiHudState_by-server.md)
|
||||||
|
* [C1 FC - HeykelSavasiTeamRoster (by server)](C1-FC-HeykelSavasiTeamRoster_by-server.md)
|
||||||
|
|||||||
@@ -218,31 +218,6 @@ public class HeykelSavasiContext : MiniGameContext
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public override bool AllowPlayerKilling => this.State == MiniGameState.Playing;
|
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>
|
/// <summary>
|
||||||
/// Gets the current number of players assigned to the given team.
|
/// Gets the current number of players assigned to the given team.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -405,6 +380,9 @@ public class HeykelSavasiContext : MiniGameContext
|
|||||||
{
|
{
|
||||||
if (args.Object is Player player)
|
if (args.Object is Player player)
|
||||||
{
|
{
|
||||||
|
// Revert the uniform battle form before dropping the player from the roster, so a player who
|
||||||
|
// leaves the event map mid-battle returns to their normal appearance.
|
||||||
|
await this.RemoveTransformAsync(player).ConfigureAwait(false);
|
||||||
this.RemoveTeam(player);
|
this.RemoveTeam(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,13 +409,18 @@ public class HeykelSavasiContext : MiniGameContext
|
|||||||
await player.WarpToAsync(this.GetTeamSpawnGate(this.GetTeam(player))).ConfigureAwait(false);
|
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
|
// Transform every participant into a single uniform battle form so their wings/mount/pet are
|
||||||
// and the teams are final (see IsItemAllowedToEquip/IsWingOrCape/UnequipWingsAndCapesAsync).
|
// visually hidden and everyone shares the same base skin; the client adds the red/blue tint on top
|
||||||
|
// via the per-player team roster (IHeykelSavasiTeamRosterPlugIn). Do this after the warp so the
|
||||||
|
// appearance broadcast reaches the players' new (base map) scope.
|
||||||
foreach (var player in players)
|
foreach (var player in players)
|
||||||
{
|
{
|
||||||
await this.UnequipWingsAndCapesAsync(player).ConfigureAwait(false);
|
await this.TransformAsync(player).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-show the HUD panel promptly after the battle-start map change.
|
||||||
|
await this.BroadcastHudStateAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
// Spawn the first (outermost) statue of each team's line; the opposing team attacks it.
|
// 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.Red, 0).ConfigureAwait(false);
|
||||||
await this.SpawnStatueAsync(HeykelSavasiTeam.Blue, 0).ConfigureAwait(false);
|
await this.SpawnStatueAsync(HeykelSavasiTeam.Blue, 0).ConfigureAwait(false);
|
||||||
@@ -474,6 +457,12 @@ public class HeykelSavasiContext : MiniGameContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Revert the uniform battle form applied in OnGameStartAsync so players return to their normal appearance.
|
||||||
|
foreach (var player in finishers)
|
||||||
|
{
|
||||||
|
await this.RemoveTransformAsync(player).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Final HUD broadcast: by now MiniGameContext.State is already MiniGameState.Ended (StopAsync sets it
|
// 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
|
// 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
|
// is needed because the periodic loop (HudBroadcastLoopAsync) stops exactly at this same state
|
||||||
@@ -587,69 +576,6 @@ public class HeykelSavasiContext : MiniGameContext
|
|||||||
_ => HeykelSavasiTeam.None,
|
_ => 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>
|
/// <summary>
|
||||||
/// Pure state transition shared by the runtime death handler (<see cref="OnDestructibleDied"/>) and the
|
/// 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
|
/// test-only <see cref="RegisterStatueDestroyedForTest"/>: records the attacker's progress and, once
|
||||||
@@ -857,9 +783,69 @@ public class HeykelSavasiContext : MiniGameContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reapplies the team's earned buffs to a player who just (re)spawned.</summary>
|
/// <summary>
|
||||||
|
/// Reapplies the team's earned buffs to a player who just (re)spawned, and - while the battle is running -
|
||||||
|
/// re-transforms them into the uniform battle form (death clears the transformation skin) and re-shows the
|
||||||
|
/// HUD panel promptly after the respawn map change.
|
||||||
|
/// </summary>
|
||||||
/// <param name="player">The player who (re)spawned.</param>
|
/// <param name="player">The player who (re)spawned.</param>
|
||||||
public ValueTask OnPlayerRespawnedAsync(Player player) => this.ReapplyBuffsAsync(player);
|
public async ValueTask OnPlayerRespawnedAsync(Player player)
|
||||||
|
{
|
||||||
|
await this.ReapplyBuffsAsync(player).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (this.State == MiniGameState.Playing)
|
||||||
|
{
|
||||||
|
await this.TransformAsync(player).ConfigureAwait(false);
|
||||||
|
await this.BroadcastHudStateAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="MonsterDefinition.Number"/> of the transformation skin every participant is turned into
|
||||||
|
/// for the duration of the battle: 14 = Skeleton Warrior, a caped, walking humanoid soldier that reads as a
|
||||||
|
/// reasonable uniform battle form and hides the player's wings/mount/pet. The red/blue distinction is added
|
||||||
|
/// separately by the client via the per-player team roster (<see cref="IHeykelSavasiTeamRosterPlugIn"/>).
|
||||||
|
/// </summary>
|
||||||
|
private const short EventFormSkin = 14;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transforms <paramref name="player"/> into the uniform <see cref="EventFormSkin"/> battle form, mirroring
|
||||||
|
/// the exact mechanism of <c>SkinChatCommandPlugIn</c> (the <c>/skin</c> command): it composes an
|
||||||
|
/// <see cref="AggregateType.AddRaw"/> element onto the player's <see cref="Stats.TransformationSkin"/>
|
||||||
|
/// attribute and sets the attribute value, which fires <c>Player.OnTransformationSkinChanged</c> and
|
||||||
|
/// re-broadcasts the player's appearance to observers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="player">The player to transform.</param>
|
||||||
|
private ValueTask TransformAsync(Player player) => this.SetTransformationSkinAsync(player, EventFormSkin);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reverts <paramref name="player"/>'s transformation by resetting <see cref="Stats.TransformationSkin"/> to
|
||||||
|
/// 0 (no transformation), which restores the player's normal appearance (wings/mount/pet included). Mirrors
|
||||||
|
/// how <c>/skin 0</c> removes the added skin element.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="player">The player to revert.</param>
|
||||||
|
private ValueTask RemoveTransformAsync(Player player) => this.SetTransformationSkinAsync(player, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets <paramref name="player"/>'s <see cref="Stats.TransformationSkin"/> to <paramref name="skin"/> exactly
|
||||||
|
/// as <c>SkinChatCommandPlugIn</c> does: clear any previously composed elements, add a single
|
||||||
|
/// <see cref="AggregateType.AddRaw"/> element with the target value, then write the attribute value (which is
|
||||||
|
/// what actually triggers the appearance re-broadcast). A value of 0 fully reverts to the natural appearance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="player">The player whose transformation skin to set.</param>
|
||||||
|
/// <param name="skin">The transformation skin number (0 = none/revert).</param>
|
||||||
|
private ValueTask SetTransformationSkinAsync(Player player, short skin)
|
||||||
|
{
|
||||||
|
if (player.Attributes is { } attributes
|
||||||
|
&& attributes.GetComposableAttribute(Stats.TransformationSkin) is { } attribute)
|
||||||
|
{
|
||||||
|
attribute.Elements.ToList().ForEach(attribute.RemoveElement);
|
||||||
|
attribute.AddElement(attributes.CreateElement(new MUnique.OpenMU.Persistence.BasicModel.PowerUpDefinitionValue { AggregateType = MUnique.OpenMU.AttributeSystem.AggregateType.AddRaw, Value = skin }, Stats.TransformationSkin));
|
||||||
|
attributes[Stats.TransformationSkin] = skin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Periodically (about once per second) broadcasts the Heykel Savasi HUD state
|
/// Periodically (about once per second) broadcasts the Heykel Savasi HUD state
|
||||||
@@ -884,6 +870,7 @@ public class HeykelSavasiContext : MiniGameContext
|
|||||||
do
|
do
|
||||||
{
|
{
|
||||||
await this.BroadcastHudStateAsync().ConfigureAwait(false);
|
await this.BroadcastHudStateAsync().ConfigureAwait(false);
|
||||||
|
await this.BroadcastTeamRosterAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false));
|
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false));
|
||||||
}
|
}
|
||||||
@@ -917,6 +904,23 @@ public class HeykelSavasiContext : MiniGameContext
|
|||||||
.AsTask()).ConfigureAwait(false);
|
.AsTask()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the current team roster (every participating player's network id and team) to every entered
|
||||||
|
/// player via <see cref="IHeykelSavasiTeamRosterPlugIn"/>, so the client can tint each nearby event player
|
||||||
|
/// red or blue. Broadcast on the same ~1s cadence as the HUD state (see <see cref="HudBroadcastLoopAsync"/>).
|
||||||
|
/// </summary>
|
||||||
|
private async ValueTask BroadcastTeamRosterAsync()
|
||||||
|
{
|
||||||
|
var entries = this._teams
|
||||||
|
.Where(kv => kv.Value != HeykelSavasiTeam.None)
|
||||||
|
.Select(kv => (kv.Key.Id, (byte)kv.Value))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync<IHeykelSavasiTeamRosterPlugIn>(p =>
|
||||||
|
p.UpdateTeamRosterAsync(entries))
|
||||||
|
.AsTask()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines the current HUD phase and the estimated number of seconds remaining in it, purely from
|
/// 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
|
/// <see cref="MiniGameContext.State"/> and wall-clock timestamps recorded (lazily, on first observation) at
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// <copyright file="IHeykelSavasiTeamRosterPlugIn.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 sends the Heykel Savasi (Statue War) team roster, mapping every
|
||||||
|
/// participating player's network id to its team so the client can tint each nearby player red or blue.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHeykelSavasiTeamRosterPlugIn : IViewPlugIn
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends the current team roster.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entries">
|
||||||
|
/// The (player network id, team) pairs of all participating players. Team is 1 = red, 2 = blue.
|
||||||
|
/// </param>
|
||||||
|
ValueTask UpdateTeamRosterAsync(IReadOnlyList<(ushort PlayerId, byte Team)> entries);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// <copyright file="HeykelSavasiTeamRosterPlugIn.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;
|
||||||
|
using MUnique.OpenMU.Network.Packets.ServerToClient;
|
||||||
|
using MUnique.OpenMU.PlugIns;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default implementation of the <see cref="IHeykelSavasiTeamRosterPlugIn"/> which is forwarding the
|
||||||
|
/// team roster to the game client with a <see cref="HeykelSavasiTeamRosterRef"/> data packet (code 0xFC).
|
||||||
|
/// </summary>
|
||||||
|
[PlugIn]
|
||||||
|
[Guid("b1f2a6d4-1c7e-4d3a-9b0e-6a2f5c8e4d11")]
|
||||||
|
public class HeykelSavasiTeamRosterPlugIn : IHeykelSavasiTeamRosterPlugIn
|
||||||
|
{
|
||||||
|
private readonly RemotePlayer _player;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HeykelSavasiTeamRosterPlugIn"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="player">The player.</param>
|
||||||
|
public HeykelSavasiTeamRosterPlugIn(RemotePlayer player) => this._player = player;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async ValueTask UpdateTeamRosterAsync(IReadOnlyList<(ushort PlayerId, byte Team)> entries)
|
||||||
|
{
|
||||||
|
var connection = this._player.Connection;
|
||||||
|
if (connection is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A byte Count field caps the roster at 255 entries; the event never has that many players.
|
||||||
|
var count = entries.Count > byte.MaxValue ? byte.MaxValue : entries.Count;
|
||||||
|
|
||||||
|
int Write()
|
||||||
|
{
|
||||||
|
var size = HeykelSavasiTeamRosterRef.GetRequiredSize(count);
|
||||||
|
var span = connection.Output.GetSpan(size)[..size];
|
||||||
|
var packet = new HeykelSavasiTeamRosterRef(span)
|
||||||
|
{
|
||||||
|
Count = (byte)count,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var block = packet[i];
|
||||||
|
block.PlayerId = entries[i].PlayerId;
|
||||||
|
block.Team = entries[i].Team;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.SendAsync(Write).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30892,6 +30892,133 @@ public readonly struct HeykelSavasiHudState
|
|||||||
/// <returns>The packet as byte span.</returns>
|
/// <returns>The packet as byte span.</returns>
|
||||||
public static implicit operator Memory<byte>(HeykelSavasiHudState packet) => packet._data;
|
public static implicit operator Memory<byte>(HeykelSavasiHudState packet) => packet._data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is sent by the server when: Periodically (about once every one to two seconds) 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 tints each listed nearby player red or blue according to its team.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct HeykelSavasiTeamRoster
|
||||||
|
{
|
||||||
|
private readonly Memory<byte> _data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HeykelSavasiTeamRoster"/> struct.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The underlying data.</param>
|
||||||
|
public HeykelSavasiTeamRoster(Memory<byte> data)
|
||||||
|
: this(data, true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HeykelSavasiTeamRoster"/> 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 HeykelSavasiTeamRoster(Memory<byte> data, bool initialize)
|
||||||
|
{
|
||||||
|
this._data = data;
|
||||||
|
if (initialize)
|
||||||
|
{
|
||||||
|
var header = this.Header;
|
||||||
|
header.Type = HeaderType;
|
||||||
|
header.Code = Code;
|
||||||
|
header.Length = (byte)data.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 => 0xFC;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the header of this packet.
|
||||||
|
/// </summary>
|
||||||
|
public C1Header Header => new (this._data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the number of player-team entries which follow.
|
||||||
|
/// </summary>
|
||||||
|
public byte Count
|
||||||
|
{
|
||||||
|
get => this._data.Span[3];
|
||||||
|
set => this._data.Span[3] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="PlayerTeam"/> of the specified index.
|
||||||
|
/// </summary>
|
||||||
|
public PlayerTeam this[int index] => new (this._data.Slice(4 + index * PlayerTeam.Length));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs an implicit conversion from a Memory of bytes to a <see cref="HeykelSavasiTeamRoster"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="packet">The packet as span.</param>
|
||||||
|
/// <returns>The packet as struct.</returns>
|
||||||
|
public static implicit operator HeykelSavasiTeamRoster(Memory<byte> packet) => new (packet, false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs an implicit conversion from <see cref="HeykelSavasiTeamRoster"/> 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>(HeykelSavasiTeamRoster packet) => packet._data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the size of the packet for the specified count of <see cref="PlayerTeam"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="playersCount">The count of <see cref="PlayerTeam"/> from which the size will be calculated.</param>
|
||||||
|
|
||||||
|
public static int GetRequiredSize(int playersCount) => playersCount * PlayerTeam.Length + 4;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a player's network id to its Heykel Savasi team..
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct PlayerTeam
|
||||||
|
{
|
||||||
|
private readonly Memory<byte> _data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="PlayerTeam"/> struct.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The underlying data.</param>
|
||||||
|
public PlayerTeam(Memory<byte> data)
|
||||||
|
{
|
||||||
|
this._data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 => 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the network id of the player (same id used in the viewport/appearance packets).
|
||||||
|
/// </summary>
|
||||||
|
public ushort PlayerId
|
||||||
|
{
|
||||||
|
get => ReadUInt16BigEndian(this._data.Span);
|
||||||
|
set => WriteUInt16BigEndian(this._data.Span, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets 1 = red, 2 = blue.
|
||||||
|
/// </summary>
|
||||||
|
public byte Team
|
||||||
|
{
|
||||||
|
get => this._data.Span[2];
|
||||||
|
set => this._data.Span[2] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the role of a guild member.
|
/// Defines the role of a guild member.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -11139,6 +11139,50 @@
|
|||||||
</Field>
|
</Field>
|
||||||
</Fields>
|
</Fields>
|
||||||
</Packet>
|
</Packet>
|
||||||
|
<Packet>
|
||||||
|
<HeaderType>C1Header</HeaderType>
|
||||||
|
<Code>FC</Code>
|
||||||
|
<Name>HeykelSavasiTeamRoster</Name>
|
||||||
|
<Direction>ServerToClient</Direction>
|
||||||
|
<SentWhen>Periodically (about once every one to two seconds) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played.</SentWhen>
|
||||||
|
<CausedReaction>The client tints each listed nearby player red or blue according to its team.</CausedReaction>
|
||||||
|
<Fields>
|
||||||
|
<Field>
|
||||||
|
<Index>3</Index>
|
||||||
|
<Type>Byte</Type>
|
||||||
|
<Name>Count</Name>
|
||||||
|
<Description>The number of player-team entries which follow.</Description>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<Index>4</Index>
|
||||||
|
<Type>Structure[]</Type>
|
||||||
|
<TypeName>PlayerTeam</TypeName>
|
||||||
|
<Name>Players</Name>
|
||||||
|
<ItemCountField>Count</ItemCountField>
|
||||||
|
</Field>
|
||||||
|
</Fields>
|
||||||
|
<Structures>
|
||||||
|
<Structure>
|
||||||
|
<Name>PlayerTeam</Name>
|
||||||
|
<Description>Maps a player's network id to its Heykel Savasi team.</Description>
|
||||||
|
<Length>3</Length>
|
||||||
|
<Fields>
|
||||||
|
<Field>
|
||||||
|
<Index>0</Index>
|
||||||
|
<Type>ShortBigEndian</Type>
|
||||||
|
<Name>PlayerId</Name>
|
||||||
|
<Description>The network id of the player (same id used in the viewport/appearance packets).</Description>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<Index>2</Index>
|
||||||
|
<Type>Byte</Type>
|
||||||
|
<Name>Team</Name>
|
||||||
|
<Description>1 = red, 2 = blue.</Description>
|
||||||
|
</Field>
|
||||||
|
</Fields>
|
||||||
|
</Structure>
|
||||||
|
</Structures>
|
||||||
|
</Packet>
|
||||||
</Packets>
|
</Packets>
|
||||||
<Enums>
|
<Enums>
|
||||||
<Enum>
|
<Enum>
|
||||||
|
|||||||
@@ -29255,3 +29255,130 @@ public readonly ref struct HeykelSavasiHudStateRef
|
|||||||
/// <returns>The packet as byte span.</returns>
|
/// <returns>The packet as byte span.</returns>
|
||||||
public static implicit operator Span<byte>(HeykelSavasiHudStateRef packet) => packet._data;
|
public static implicit operator Span<byte>(HeykelSavasiHudStateRef packet) => packet._data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is sent by the server when: Periodically (about once every one to two seconds) 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 tints each listed nearby player red or blue according to its team.
|
||||||
|
/// </summary>
|
||||||
|
public readonly ref struct HeykelSavasiTeamRosterRef
|
||||||
|
{
|
||||||
|
private readonly Span<byte> _data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HeykelSavasiTeamRosterRef"/> struct.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The underlying data.</param>
|
||||||
|
public HeykelSavasiTeamRosterRef(Span<byte> data)
|
||||||
|
: this(data, true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HeykelSavasiTeamRosterRef"/> 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 HeykelSavasiTeamRosterRef(Span<byte> data, bool initialize)
|
||||||
|
{
|
||||||
|
this._data = data;
|
||||||
|
if (initialize)
|
||||||
|
{
|
||||||
|
var header = this.Header;
|
||||||
|
header.Type = HeaderType;
|
||||||
|
header.Code = Code;
|
||||||
|
header.Length = (byte)data.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 => 0xFC;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the header of this packet.
|
||||||
|
/// </summary>
|
||||||
|
public C1HeaderRef Header => new (this._data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the number of player-team entries which follow.
|
||||||
|
/// </summary>
|
||||||
|
public byte Count
|
||||||
|
{
|
||||||
|
get => this._data[3];
|
||||||
|
set => this._data[3] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="PlayerTeamRef"/> of the specified index.
|
||||||
|
/// </summary>
|
||||||
|
public PlayerTeamRef this[int index] => new (this._data[(4 + index * PlayerTeamRef.Length)..]);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs an implicit conversion from a Span of bytes to a <see cref="HeykelSavasiTeamRoster"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="packet">The packet as span.</param>
|
||||||
|
/// <returns>The packet as struct.</returns>
|
||||||
|
public static implicit operator HeykelSavasiTeamRosterRef(Span<byte> packet) => new (packet, false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs an implicit conversion from <see cref="HeykelSavasiTeamRoster"/> 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>(HeykelSavasiTeamRosterRef packet) => packet._data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the size of the packet for the specified count of <see cref="PlayerTeamRef"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="playersCount">The count of <see cref="PlayerTeamRef"/> from which the size will be calculated.</param>
|
||||||
|
|
||||||
|
public static int GetRequiredSize(int playersCount) => playersCount * PlayerTeamRef.Length + 4;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a player's network id to its Heykel Savasi team..
|
||||||
|
/// </summary>
|
||||||
|
public readonly ref struct PlayerTeamRef
|
||||||
|
{
|
||||||
|
private readonly Span<byte> _data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="PlayerTeamRef"/> struct.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The underlying data.</param>
|
||||||
|
public PlayerTeamRef(Span<byte> data)
|
||||||
|
{
|
||||||
|
this._data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 => 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the network id of the player (same id used in the viewport/appearance packets).
|
||||||
|
/// </summary>
|
||||||
|
public ushort PlayerId
|
||||||
|
{
|
||||||
|
get => ReadUInt16BigEndian(this._data);
|
||||||
|
set => WriteUInt16BigEndian(this._data, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets 1 = red, 2 = blue.
|
||||||
|
/// </summary>
|
||||||
|
public byte Team
|
||||||
|
{
|
||||||
|
get => this._data[2];
|
||||||
|
set => this._data[2] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ internal class HeykelSavasiMap : BaseMapInitializer
|
|||||||
var statueAttributes = new Dictionary<AttributeDefinition, float>
|
var statueAttributes = new Dictionary<AttributeDefinition, float>
|
||||||
{
|
{
|
||||||
{ Stats.Level, 100 },
|
{ Stats.Level, 100 },
|
||||||
{ Stats.MaximumHealth, 200000 }, // tuning
|
{ Stats.MaximumHealth, 3_000_000 }, // tuning: big HP piñata that takes sustained effort to break
|
||||||
{ Stats.DefenseBase, 200 },
|
{ Stats.DefenseBase, 500 },
|
||||||
{ Stats.DefenseRatePvm, 100 },
|
{ Stats.DefenseRatePvm, 100 },
|
||||||
};
|
};
|
||||||
statue.AddAttributes(statueAttributes, this.Context, this.GameConfiguration);
|
statue.AddAttributes(statueAttributes, this.Context, this.GameConfiguration);
|
||||||
|
|||||||
Reference in New Issue
Block a user