feat(tvt): per-character scoreboard (kills, statues, statue damage)
Server: new 0xFD HeykelSavasiScoreboard packet + view plugin; HeykelSavasiContext tracks per-player kills (Player.AfterKilledPlayerAsync hook), statues broken (OnDestructibleDied), and statue damage (AttackableNpcBase.AttackByAsync hook); broadcast top-8 by damage (desc) every ~1s, omitting zero-damage characters.
This commit is contained in:
@@ -248,6 +248,18 @@ public class HeykelSavasiContext : MiniGameContext
|
||||
/// </summary>
|
||||
private int _bossesSpawned;
|
||||
|
||||
/// <summary>
|
||||
/// Per-character contribution stats for the scoreboard (<see cref="IHeykelSavasiScoreboardPlugIn"/>):
|
||||
/// enemy kills, statues broken, and total damage dealt to statues. Keyed by the participating player;
|
||||
/// fed by <see cref="RecordKill"/>, <see cref="RecordStatueBreak"/> and <see cref="RecordStatueDamage"/>.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<Player, PlayerScore> _scores = new();
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of characters shown on the scoreboard (top contributors by statue damage).
|
||||
/// </summary>
|
||||
private const int ScoreboardMaxEntries = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Synchronizes the check-then-act sequence of <see cref="TryJoinTeamAsync"/>, so that concurrent
|
||||
/// join attempts cannot both pass the balance check before either of them reserves a slot.
|
||||
@@ -448,6 +460,64 @@ public class HeykelSavasiContext : MiniGameContext
|
||||
/// <param name="player">The player.</param>
|
||||
private void RemoveTeam(Player player) => this._teams.TryRemove(player, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Records that <paramref name="killer"/> killed an enemy-team player (for the scoreboard kill count).
|
||||
/// Called from <c>Player.AfterKilledPlayerAsync</c> when both players are in this event on opposing teams.
|
||||
/// </summary>
|
||||
/// <param name="killer">The killing player.</param>
|
||||
public void RecordKill(Player killer)
|
||||
{
|
||||
if (killer is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var score = this._scores.GetOrAdd(killer, _ => new PlayerScore());
|
||||
Interlocked.Increment(ref score.Kills);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records that the character named <paramref name="killerName"/> broke a statue (dealt the killing blow),
|
||||
/// for the scoreboard statue count. Called from <see cref="OnDestructibleDied"/>, which only knows the
|
||||
/// killer's name; it is matched to the participating player of that name.
|
||||
/// </summary>
|
||||
/// <param name="killerName">The character name of the player who broke the statue.</param>
|
||||
public void RecordStatueBreak(string killerName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(killerName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var player = this._teams.Keys.FirstOrDefault(p => string.Equals(p.Name, killerName, StringComparison.Ordinal));
|
||||
if (player is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var score = this._scores.GetOrAdd(player, _ => new PlayerScore());
|
||||
Interlocked.Increment(ref score.Statues);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records <paramref name="damage"/> dealt by <paramref name="player"/> to <paramref name="statue"/>, for the
|
||||
/// scoreboard damage ranking. Called from <c>AttackableNpcBase.AttackByAsync</c> for every hit that lands on a
|
||||
/// statue of this event.
|
||||
/// </summary>
|
||||
/// <param name="player">The attacking player.</param>
|
||||
/// <param name="statue">The statue NPC that was hit.</param>
|
||||
/// <param name="damage">The health damage dealt by this hit.</param>
|
||||
public void RecordStatueDamage(Player player, AttackableNpcBase statue, uint damage)
|
||||
{
|
||||
if (player is null || damage == 0 || !this._statues.ContainsKey(statue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var score = this._scores.GetOrAdd(player, _ => new PlayerScore());
|
||||
Interlocked.Add(ref score.Damage, damage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a player from their team when they leave the event map for good (disconnect, or warp
|
||||
/// to another map such as Lorencia). A same-map death-respawn does NOT trigger this (RespawnAtAsync
|
||||
@@ -591,6 +661,9 @@ public class HeykelSavasiContext : MiniGameContext
|
||||
this.HandleStatueDestroyed(info.Defender, info.Index, attacker, spawnNext: false);
|
||||
var count = this.GetProgress(attacker);
|
||||
|
||||
// Credit the statue break to the destroyer's character for the scoreboard.
|
||||
this.RecordStatueBreak(e.KillerName);
|
||||
|
||||
// Fire-and-forget the async side effects (break announcement, buff application, or finishing the
|
||||
// event); the death event handler itself must stay synchronous.
|
||||
_ = this.OnStatueDestroyedSideEffectsAsync(attacker, count, e.KillerName);
|
||||
@@ -1083,6 +1156,7 @@ public class HeykelSavasiContext : MiniGameContext
|
||||
{
|
||||
await this.BroadcastHudStateAsync().ConfigureAwait(false);
|
||||
await this.BroadcastTeamRosterAsync().ConfigureAwait(false);
|
||||
await this.BroadcastScoreboardAsync().ConfigureAwait(false);
|
||||
}
|
||||
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false));
|
||||
}
|
||||
@@ -1133,6 +1207,32 @@ public class HeykelSavasiContext : MiniGameContext
|
||||
.AsTask()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the per-character scoreboard to every entered player via <see cref="IHeykelSavasiScoreboardPlugIn"/>:
|
||||
/// the top <see cref="ScoreboardMaxEntries"/> contributors ordered by total statue damage (descending).
|
||||
/// Characters that dealt no statue damage are omitted. Broadcast on the same ~1s cadence as the HUD state.
|
||||
/// </summary>
|
||||
private async ValueTask BroadcastScoreboardAsync()
|
||||
{
|
||||
var entries = this._scores
|
||||
.Select(kv => (kv.Key.Name, Score: kv.Value))
|
||||
.Where(x => x.Score.Damage > 0)
|
||||
.OrderByDescending(x => x.Score.Damage)
|
||||
.ThenByDescending(x => x.Score.Statues)
|
||||
.ThenByDescending(x => x.Score.Kills)
|
||||
.Take(ScoreboardMaxEntries)
|
||||
.Select(x => (
|
||||
Name: x.Name ?? string.Empty,
|
||||
Kills: (ushort)Math.Min(x.Score.Kills, ushort.MaxValue),
|
||||
Statues: (byte)Math.Min(x.Score.Statues, byte.MaxValue),
|
||||
Damage: (uint)Math.Min(Interlocked.Read(ref x.Score.Damage), uint.MaxValue)))
|
||||
.ToList();
|
||||
|
||||
await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync<IHeykelSavasiScoreboardPlugIn>(p =>
|
||||
p.UpdateScoreboardAsync(entries))
|
||||
.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
|
||||
@@ -1191,6 +1291,22 @@ public class HeykelSavasiContext : MiniGameContext
|
||||
/// <see cref="IsJoinAllowed"/>, extracted for the same reason). Kept <c>internal</c> (rather than
|
||||
/// <c>private</c>) specifically so <c>MUnique.OpenMU.Tests</c> can drive it directly.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Mutable per-character contribution counters for the scoreboard. Fields (not properties) so they can be
|
||||
/// updated atomically with <see cref="Interlocked"/> from concurrent hit/kill handlers.
|
||||
/// </summary>
|
||||
private sealed class PlayerScore
|
||||
{
|
||||
/// <summary>Number of enemy-team players killed.</summary>
|
||||
public int Kills;
|
||||
|
||||
/// <summary>Number of statues broken (killing blow).</summary>
|
||||
public int Statues;
|
||||
|
||||
/// <summary>Total damage dealt to statues.</summary>
|
||||
public long Damage;
|
||||
}
|
||||
|
||||
internal sealed class StatueProgressState
|
||||
{
|
||||
private readonly ConcurrentDictionary<HeykelSavasiTeam, int> _progress = new()
|
||||
|
||||
@@ -142,6 +142,13 @@ public abstract class AttackableNpcBase : NonPlayerCharacter, IAttackable
|
||||
{
|
||||
await player.ApplyMaceMasteryStunEffectAsync(this).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// ADAMU-CUSTOM: TvT Event -> record per-character statue damage for the event scoreboard.
|
||||
if (this.Definition.ObjectKind == NpcObjectKind.Destructible
|
||||
&& player.CurrentMiniGame is MiniGames.HeykelSavasiContext heykelDamageGame)
|
||||
{
|
||||
heykelDamageGame.RecordStatueDamage(player, this, (uint)hitInfo.HealthDamage);
|
||||
}
|
||||
}
|
||||
|
||||
if (attacker as IPlayerSurrogate is { } playerSurrogate)
|
||||
|
||||
@@ -1890,6 +1890,21 @@ public class Player : AsyncDisposable, IBucketMapObserver, IAttackable, IAttacke
|
||||
/// <param name="killedPlayer">The player killed.</param>
|
||||
internal async ValueTask AfterKilledPlayerAsync(Player killedPlayer)
|
||||
{
|
||||
// ADAMU-CUSTOM: TvT Event -> credit the killer with an enemy-team kill for the event scoreboard.
|
||||
// Done up front, before the PK-state early-returns below, so it counts regardless of hero state.
|
||||
if (this.CurrentMiniGame is MiniGames.HeykelSavasiContext heykelKillGame
|
||||
&& ReferenceEquals(killedPlayer.CurrentMiniGame, this.CurrentMiniGame))
|
||||
{
|
||||
var killerTeam = heykelKillGame.GetTeam(this);
|
||||
var victimTeam = heykelKillGame.GetTeam(killedPlayer);
|
||||
if (killerTeam != victimTeam
|
||||
&& killerTeam != MiniGames.HeykelSavasiTeam.None
|
||||
&& victimTeam != MiniGames.HeykelSavasiTeam.None)
|
||||
{
|
||||
heykelKillGame.RecordKill(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.DuelRoom?.State == DuelState.DuelStarted)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// <copyright file="IHeykelSavasiScoreboardPlugIn.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 TvT Event (Statue War) per-character scoreboard: the
|
||||
/// top contributing characters ordered by statue damage, each with its kill count, statues broken, and total
|
||||
/// statue damage.
|
||||
/// </summary>
|
||||
public interface IHeykelSavasiScoreboardPlugIn : IViewPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends the current scoreboard.
|
||||
/// </summary>
|
||||
/// <param name="entries">
|
||||
/// The scoreboard entries, already ordered (highest statue damage first) and capped by the caller.
|
||||
/// Each entry is (character name, kills, statues broken, total statue damage).
|
||||
/// </param>
|
||||
ValueTask UpdateScoreboardAsync(IReadOnlyList<(string Name, ushort Kills, byte Statues, uint Damage)> entries);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// <copyright file="HeykelSavasiScoreboardPlugIn.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="IHeykelSavasiScoreboardPlugIn"/> which forwards the
|
||||
/// per-character scoreboard to the game client with a <see cref="HeykelSavasiScoreboardRef"/> data packet
|
||||
/// (code 0xFD).
|
||||
/// </summary>
|
||||
[PlugIn]
|
||||
[Guid("a3d8f1c2-6b4e-4f7a-8c2d-9e1b3a5f7d20")]
|
||||
public class HeykelSavasiScoreboardPlugIn : IHeykelSavasiScoreboardPlugIn
|
||||
{
|
||||
private readonly RemotePlayer _player;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeykelSavasiScoreboardPlugIn"/> class.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
public HeykelSavasiScoreboardPlugIn(RemotePlayer player) => this._player = player;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask UpdateScoreboardAsync(IReadOnlyList<(string Name, ushort Kills, byte Statues, uint Damage)> entries)
|
||||
{
|
||||
var connection = this._player.Connection;
|
||||
if (connection is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// A byte Count field caps the scoreboard at 255 entries; the caller sends far fewer (top contributors).
|
||||
var count = entries.Count > byte.MaxValue ? byte.MaxValue : entries.Count;
|
||||
|
||||
int Write()
|
||||
{
|
||||
var size = HeykelSavasiScoreboardRef.GetRequiredSize(count);
|
||||
var span = connection.Output.GetSpan(size)[..size];
|
||||
var packet = new HeykelSavasiScoreboardRef(span)
|
||||
{
|
||||
Count = (byte)count,
|
||||
};
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var block = packet[i];
|
||||
block.Name = entries[i].Name;
|
||||
block.Kills = entries[i].Kills;
|
||||
block.Statues = entries[i].Statues;
|
||||
block.Damage = entries[i].Damage;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
await connection.SendAsync(Write).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -31018,6 +31018,151 @@ public readonly struct PlayerTeam
|
||||
set => this._data.Span[2] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Is sent by the server when: Periodically (about once every one to two seconds) while the TvT Event (Statue War) is being played.
|
||||
/// Causes reaction on client side: The client updates its TvT Event scoreboard panel, listing the top contributing characters ordered by statue damage.
|
||||
/// </summary>
|
||||
public readonly struct HeykelSavasiScoreboard
|
||||
{
|
||||
private readonly Memory<byte> _data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeykelSavasiScoreboard"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="data">The underlying data.</param>
|
||||
public HeykelSavasiScoreboard(Memory<byte> data)
|
||||
: this(data, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeykelSavasiScoreboard"/> 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 HeykelSavasiScoreboard(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 => 0xFD;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the header of this packet.
|
||||
/// </summary>
|
||||
public C1Header Header => new (this._data);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of scoreboard entries which follow.
|
||||
/// </summary>
|
||||
public byte Count
|
||||
{
|
||||
get => this._data.Span[3];
|
||||
set => this._data.Span[3] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ScoreEntry"/> of the specified index.
|
||||
/// </summary>
|
||||
public ScoreEntry this[int index] => new (this._data.Slice(4 + index * ScoreEntry.Length));
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from a Memory of bytes to a <see cref="HeykelSavasiScoreboard"/>.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet as span.</param>
|
||||
/// <returns>The packet as struct.</returns>
|
||||
public static implicit operator HeykelSavasiScoreboard(Memory<byte> packet) => new (packet, false);
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from <see cref="HeykelSavasiScoreboard"/> 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>(HeykelSavasiScoreboard packet) => packet._data;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the size of the packet for the specified count of <see cref="ScoreEntry"/>.
|
||||
/// </summary>
|
||||
/// <param name="entriesCount">The count of <see cref="ScoreEntry"/> from which the size will be calculated.</param>
|
||||
|
||||
public static int GetRequiredSize(int entriesCount) => entriesCount * ScoreEntry.Length + 4;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A character's TvT Event contribution: kills, statues broken, and total statue damage..
|
||||
/// </summary>
|
||||
public readonly struct ScoreEntry
|
||||
{
|
||||
private readonly Memory<byte> _data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScoreEntry"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="data">The underlying data.</param>
|
||||
public ScoreEntry(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 => 17;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the character name.
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get => this._data.Span.ExtractString(0, 10, System.Text.Encoding.UTF8);
|
||||
set => this._data.Slice(0, 10).Span.WriteString(value, System.Text.Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets number of enemy-team players this character killed.
|
||||
/// </summary>
|
||||
public ushort Kills
|
||||
{
|
||||
get => ReadUInt16BigEndian(this._data.Span[10..]);
|
||||
set => WriteUInt16BigEndian(this._data.Span[10..], value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets number of statues this character broke (dealt the killing blow to).
|
||||
/// </summary>
|
||||
public byte Statues
|
||||
{
|
||||
get => this._data.Span[12];
|
||||
set => this._data.Span[12] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets total damage this character dealt to statues.
|
||||
/// </summary>
|
||||
public uint Damage
|
||||
{
|
||||
get => ReadUInt32BigEndian(this._data.Span[13..]);
|
||||
set => WriteUInt32BigEndian(this._data.Span[13..], value);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Defines the role of a guild member.
|
||||
|
||||
@@ -11183,6 +11183,63 @@
|
||||
</Structure>
|
||||
</Structures>
|
||||
</Packet>
|
||||
<Packet>
|
||||
<HeaderType>C1Header</HeaderType>
|
||||
<Code>FD</Code>
|
||||
<Name>HeykelSavasiScoreboard</Name>
|
||||
<Direction>ServerToClient</Direction>
|
||||
<SentWhen>Periodically (about once every one to two seconds) while the TvT Event (Statue War) is being played.</SentWhen>
|
||||
<CausedReaction>The client updates its TvT Event scoreboard panel, listing the top contributing characters ordered by statue damage.</CausedReaction>
|
||||
<Fields>
|
||||
<Field>
|
||||
<Index>3</Index>
|
||||
<Type>Byte</Type>
|
||||
<Name>Count</Name>
|
||||
<Description>The number of scoreboard entries which follow.</Description>
|
||||
</Field>
|
||||
<Field>
|
||||
<Index>4</Index>
|
||||
<Type>Structure[]</Type>
|
||||
<TypeName>ScoreEntry</TypeName>
|
||||
<Name>Entries</Name>
|
||||
<ItemCountField>Count</ItemCountField>
|
||||
</Field>
|
||||
</Fields>
|
||||
<Structures>
|
||||
<Structure>
|
||||
<Name>ScoreEntry</Name>
|
||||
<Description>A character's TvT Event contribution: kills, statues broken, and total statue damage.</Description>
|
||||
<Length>17</Length>
|
||||
<Fields>
|
||||
<Field>
|
||||
<Index>0</Index>
|
||||
<Type>String</Type>
|
||||
<Name>Name</Name>
|
||||
<Length>10</Length>
|
||||
<Description>The character name.</Description>
|
||||
</Field>
|
||||
<Field>
|
||||
<Index>10</Index>
|
||||
<Type>ShortBigEndian</Type>
|
||||
<Name>Kills</Name>
|
||||
<Description>Number of enemy-team players this character killed.</Description>
|
||||
</Field>
|
||||
<Field>
|
||||
<Index>12</Index>
|
||||
<Type>Byte</Type>
|
||||
<Name>Statues</Name>
|
||||
<Description>Number of statues this character broke (dealt the killing blow to).</Description>
|
||||
</Field>
|
||||
<Field>
|
||||
<Index>13</Index>
|
||||
<Type>IntegerBigEndian</Type>
|
||||
<Name>Damage</Name>
|
||||
<Description>Total damage this character dealt to statues.</Description>
|
||||
</Field>
|
||||
</Fields>
|
||||
</Structure>
|
||||
</Structures>
|
||||
</Packet>
|
||||
</Packets>
|
||||
<Enums>
|
||||
<Enum>
|
||||
|
||||
@@ -29382,3 +29382,148 @@ public readonly ref struct PlayerTeamRef
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Is sent by the server when: Periodically (about once every one to two seconds) while the TvT Event (Statue War) is being played.
|
||||
/// Causes reaction on client side: The client updates its TvT Event scoreboard panel, listing the top contributing characters ordered by statue damage.
|
||||
/// </summary>
|
||||
public readonly ref struct HeykelSavasiScoreboardRef
|
||||
{
|
||||
private readonly Span<byte> _data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeykelSavasiScoreboardRef"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="data">The underlying data.</param>
|
||||
public HeykelSavasiScoreboardRef(Span<byte> data)
|
||||
: this(data, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HeykelSavasiScoreboardRef"/> 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 HeykelSavasiScoreboardRef(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 => 0xFD;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the header of this packet.
|
||||
/// </summary>
|
||||
public C1HeaderRef Header => new (this._data);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of scoreboard entries which follow.
|
||||
/// </summary>
|
||||
public byte Count
|
||||
{
|
||||
get => this._data[3];
|
||||
set => this._data[3] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ScoreEntryRef"/> of the specified index.
|
||||
/// </summary>
|
||||
public ScoreEntryRef this[int index] => new (this._data[(4 + index * ScoreEntryRef.Length)..]);
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from a Span of bytes to a <see cref="HeykelSavasiScoreboard"/>.
|
||||
/// </summary>
|
||||
/// <param name="packet">The packet as span.</param>
|
||||
/// <returns>The packet as struct.</returns>
|
||||
public static implicit operator HeykelSavasiScoreboardRef(Span<byte> packet) => new (packet, false);
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from <see cref="HeykelSavasiScoreboard"/> 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>(HeykelSavasiScoreboardRef packet) => packet._data;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the size of the packet for the specified count of <see cref="ScoreEntryRef"/>.
|
||||
/// </summary>
|
||||
/// <param name="entriesCount">The count of <see cref="ScoreEntryRef"/> from which the size will be calculated.</param>
|
||||
|
||||
public static int GetRequiredSize(int entriesCount) => entriesCount * ScoreEntryRef.Length + 4;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A character's TvT Event contribution: kills, statues broken, and total statue damage..
|
||||
/// </summary>
|
||||
public readonly ref struct ScoreEntryRef
|
||||
{
|
||||
private readonly Span<byte> _data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScoreEntryRef"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="data">The underlying data.</param>
|
||||
public ScoreEntryRef(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 => 17;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the character name.
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get => this._data.ExtractString(0, 10, System.Text.Encoding.UTF8);
|
||||
set => this._data.Slice(0, 10).WriteString(value, System.Text.Encoding.UTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets number of enemy-team players this character killed.
|
||||
/// </summary>
|
||||
public ushort Kills
|
||||
{
|
||||
get => ReadUInt16BigEndian(this._data[10..]);
|
||||
set => WriteUInt16BigEndian(this._data[10..], value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets number of statues this character broke (dealt the killing blow to).
|
||||
/// </summary>
|
||||
public byte Statues
|
||||
{
|
||||
get => this._data[12];
|
||||
set => this._data[12] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets total damage this character dealt to statues.
|
||||
/// </summary>
|
||||
public uint Damage
|
||||
{
|
||||
get => ReadUInt32BigEndian(this._data[13..]);
|
||||
set => WriteUInt32BigEndian(this._data[13..], value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user