fix: complete retail parity stability pass
This commit is contained in:
parent
d3df4cb20a
commit
f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions
|
|
@ -60,6 +60,41 @@ public static class ClientCommandRequests
|
|||
public const uint AddPlayerPermissionOpcode = 0x0219u;
|
||||
public const uint RemovePlayerPermissionOpcode = 0x021Au;
|
||||
public const uint AbandonHouseOpcode = 0x021Fu;
|
||||
public const uint QueryAllegianceNameOpcode = 0x0030u;
|
||||
public const uint ClearAllegianceNameOpcode = 0x0031u;
|
||||
public const uint SetAllegianceNameOpcode = 0x0033u;
|
||||
public const uint SetAllegianceOfficerOpcode = 0x003Bu;
|
||||
public const uint SetAllegianceOfficerTitleOpcode = 0x003Cu;
|
||||
public const uint ListAllegianceOfficerTitlesOpcode = 0x003Du;
|
||||
public const uint ClearAllegianceOfficerTitlesOpcode = 0x003Eu;
|
||||
public const uint DoAllegianceLockActionOpcode = 0x003Fu;
|
||||
public const uint SetAllegianceApprovedVassalOpcode = 0x0040u;
|
||||
public const uint AllegianceChatGagOpcode = 0x0041u;
|
||||
public const uint DoAllegianceHouseActionOpcode = 0x0042u;
|
||||
public const uint AddPermanentGuestOpcode = 0x0245u;
|
||||
public const uint RemovePermanentGuestOpcode = 0x0246u;
|
||||
public const uint SetOpenHouseStatusOpcode = 0x0247u;
|
||||
public const uint ChangeStoragePermissionOpcode = 0x0249u;
|
||||
public const uint BootSpecificHouseGuestOpcode = 0x024Au;
|
||||
public const uint RemoveAllStoragePermissionOpcode = 0x024Cu;
|
||||
public const uint RequestFullGuestListOpcode = 0x024Du;
|
||||
public const uint SetMotdOpcode = 0x0254u;
|
||||
public const uint QueryMotdOpcode = 0x0255u;
|
||||
public const uint ClearMotdOpcode = 0x0256u;
|
||||
public const uint AddAllStoragePermissionOpcode = 0x025Cu;
|
||||
public const uint RemoveAllPermanentGuestsOpcode = 0x025Eu;
|
||||
public const uint BootEveryoneOpcode = 0x025Fu;
|
||||
public const uint SetHooksVisibilityOpcode = 0x0266u;
|
||||
public const uint ModifyAllegianceGuestPermissionOpcode = 0x0267u;
|
||||
public const uint ModifyAllegianceStoragePermissionOpcode = 0x0268u;
|
||||
public const uint BreakAllegianceBootOpcode = 0x0277u;
|
||||
public const uint AllegianceChatBootOpcode = 0x02A0u;
|
||||
public const uint AddAllegianceBanOpcode = 0x02A1u;
|
||||
public const uint RemoveAllegianceBanOpcode = 0x02A2u;
|
||||
public const uint ListAllegianceBansOpcode = 0x02A3u;
|
||||
public const uint RemoveAllegianceOfficerOpcode = 0x02A5u;
|
||||
public const uint ListAllegianceOfficersOpcode = 0x02A6u;
|
||||
public const uint ClearAllegianceOfficersOpcode = 0x02A7u;
|
||||
// Batch C (Map/House toolbar panel, 2026-08-17): the query the House
|
||||
// tab needs to populate. ACE GameActionHouseQuery.cs: [GameAction(
|
||||
// GameActionType.HouseQuery)] (0x021E), Handle reads no payload and
|
||||
|
|
@ -283,8 +318,9 @@ public static class ClientCommandRequests
|
|||
public static byte[] BuildRecallAllegianceHometown(uint sequence) =>
|
||||
BuildParameterless(sequence, RecallAllegianceHometownOpcode);
|
||||
|
||||
// "@allegiance info [name]" — GameActionAllegianceInfoRequest.Handle:
|
||||
// ReadString16L() player name (empty string = self).
|
||||
// "@allegiance info <name>" — GameActionAllegianceInfoRequest.Handle:
|
||||
// ReadString16L() player name. Retail refuses an empty argument before
|
||||
// constructing this request.
|
||||
public static byte[] BuildAllegianceInfoRequest(uint sequence, string playerName) =>
|
||||
BuildString(sequence, AllegianceInfoRequestOpcode, playerName);
|
||||
|
||||
|
|
@ -308,6 +344,133 @@ public static class ClientCommandRequests
|
|||
public static byte[] BuildAbandonHouse(uint sequence) =>
|
||||
BuildParameterless(sequence, AbandonHouseOpcode);
|
||||
|
||||
// Complete named-retail @allegiance/@house administration family.
|
||||
// Opcodes and field order are cross-checked against ACE's individual
|
||||
// GameAction readers; the client-side grammar is ported separately by
|
||||
// ClientCommandController from DoAllegiance/DoHouse and their helpers.
|
||||
public static byte[] BuildBreakAllegianceBoot(
|
||||
uint sequence, string playerName, bool accountBoot) =>
|
||||
BuildStringUInt32(
|
||||
sequence, BreakAllegianceBootOpcode, playerName, accountBoot ? 1u : 0u);
|
||||
|
||||
public static byte[] BuildAllegianceChatBoot(
|
||||
uint sequence, string playerName, string reason) =>
|
||||
BuildTwoStrings(sequence, AllegianceChatBootOpcode, playerName, reason);
|
||||
|
||||
public static byte[] BuildAllegianceChatGag(
|
||||
uint sequence, string playerName, bool enabled) =>
|
||||
BuildStringUInt32(
|
||||
sequence, AllegianceChatGagOpcode, playerName, enabled ? 1u : 0u);
|
||||
|
||||
public static byte[] BuildAddAllegianceBan(uint sequence, string playerName) =>
|
||||
BuildString(sequence, AddAllegianceBanOpcode, playerName);
|
||||
|
||||
public static byte[] BuildRemoveAllegianceBan(uint sequence, string playerName) =>
|
||||
BuildString(sequence, RemoveAllegianceBanOpcode, playerName);
|
||||
|
||||
public static byte[] BuildListAllegianceBans(uint sequence) =>
|
||||
BuildParameterless(sequence, ListAllegianceBansOpcode);
|
||||
|
||||
public static byte[] BuildSetAllegianceOfficer(
|
||||
uint sequence, string playerName, uint officerLevel) =>
|
||||
BuildStringUInt32(
|
||||
sequence, SetAllegianceOfficerOpcode, playerName, officerLevel);
|
||||
|
||||
public static byte[] BuildRemoveAllegianceOfficer(
|
||||
uint sequence, string playerName) =>
|
||||
BuildString(sequence, RemoveAllegianceOfficerOpcode, playerName);
|
||||
|
||||
public static byte[] BuildListAllegianceOfficers(uint sequence) =>
|
||||
BuildParameterless(sequence, ListAllegianceOfficersOpcode);
|
||||
|
||||
public static byte[] BuildClearAllegianceOfficers(uint sequence) =>
|
||||
BuildParameterless(sequence, ClearAllegianceOfficersOpcode);
|
||||
|
||||
public static byte[] BuildSetAllegianceOfficerTitle(
|
||||
uint sequence, uint officerLevel, string title) =>
|
||||
BuildUInt32String(
|
||||
sequence, SetAllegianceOfficerTitleOpcode, officerLevel, title);
|
||||
|
||||
public static byte[] BuildListAllegianceOfficerTitles(uint sequence) =>
|
||||
BuildParameterless(sequence, ListAllegianceOfficerTitlesOpcode);
|
||||
|
||||
public static byte[] BuildClearAllegianceOfficerTitles(uint sequence) =>
|
||||
BuildParameterless(sequence, ClearAllegianceOfficerTitlesOpcode);
|
||||
|
||||
public static byte[] BuildQueryAllegianceName(uint sequence) =>
|
||||
BuildParameterless(sequence, QueryAllegianceNameOpcode);
|
||||
|
||||
public static byte[] BuildSetAllegianceName(uint sequence, string name) =>
|
||||
BuildString(sequence, SetAllegianceNameOpcode, name);
|
||||
|
||||
public static byte[] BuildClearAllegianceName(uint sequence) =>
|
||||
BuildParameterless(sequence, ClearAllegianceNameOpcode);
|
||||
|
||||
public static byte[] BuildAllegianceLockAction(uint sequence, uint action) =>
|
||||
BuildUInt32(sequence, DoAllegianceLockActionOpcode, action);
|
||||
|
||||
public static byte[] BuildSetAllegianceApprovedVassal(
|
||||
uint sequence, string playerName) =>
|
||||
BuildString(sequence, SetAllegianceApprovedVassalOpcode, playerName);
|
||||
|
||||
public static byte[] BuildAllegianceHouseAction(uint sequence, uint action) =>
|
||||
BuildUInt32(sequence, DoAllegianceHouseActionOpcode, action);
|
||||
|
||||
public static byte[] BuildQueryMotd(uint sequence) =>
|
||||
BuildParameterless(sequence, QueryMotdOpcode);
|
||||
|
||||
public static byte[] BuildSetMotd(uint sequence, string motd) =>
|
||||
BuildString(sequence, SetMotdOpcode, motd);
|
||||
|
||||
public static byte[] BuildClearMotd(uint sequence) =>
|
||||
BuildParameterless(sequence, ClearMotdOpcode);
|
||||
|
||||
public static byte[] BuildSetOpenHouseStatus(uint sequence, bool isOpen) =>
|
||||
BuildUInt32(sequence, SetOpenHouseStatusOpcode, isOpen ? 1u : 0u);
|
||||
|
||||
public static byte[] BuildAddPermanentGuest(uint sequence, string playerName) =>
|
||||
BuildString(sequence, AddPermanentGuestOpcode, playerName);
|
||||
|
||||
public static byte[] BuildRemovePermanentGuest(uint sequence, string playerName) =>
|
||||
BuildString(sequence, RemovePermanentGuestOpcode, playerName);
|
||||
|
||||
public static byte[] BuildRemoveAllPermanentGuests(uint sequence) =>
|
||||
BuildParameterless(sequence, RemoveAllPermanentGuestsOpcode);
|
||||
|
||||
public static byte[] BuildChangeStoragePermission(
|
||||
uint sequence, string playerName, bool enabled) =>
|
||||
BuildStringUInt32(
|
||||
sequence, ChangeStoragePermissionOpcode, playerName, enabled ? 1u : 0u);
|
||||
|
||||
public static byte[] BuildAddAllStoragePermission(uint sequence) =>
|
||||
BuildParameterless(sequence, AddAllStoragePermissionOpcode);
|
||||
|
||||
public static byte[] BuildRemoveAllStoragePermission(uint sequence) =>
|
||||
BuildParameterless(sequence, RemoveAllStoragePermissionOpcode);
|
||||
|
||||
public static byte[] BuildRequestFullGuestList(uint sequence) =>
|
||||
BuildParameterless(sequence, RequestFullGuestListOpcode);
|
||||
|
||||
public static byte[] BuildBootSpecificHouseGuest(
|
||||
uint sequence, string playerName) =>
|
||||
BuildString(sequence, BootSpecificHouseGuestOpcode, playerName);
|
||||
|
||||
public static byte[] BuildBootEveryone(uint sequence) =>
|
||||
BuildParameterless(sequence, BootEveryoneOpcode);
|
||||
|
||||
public static byte[] BuildSetHooksVisibility(uint sequence, bool visible) =>
|
||||
BuildUInt32(sequence, SetHooksVisibilityOpcode, visible ? 1u : 0u);
|
||||
|
||||
public static byte[] BuildModifyAllegianceGuestPermission(
|
||||
uint sequence, bool enabled) =>
|
||||
BuildUInt32(
|
||||
sequence, ModifyAllegianceGuestPermissionOpcode, enabled ? 1u : 0u);
|
||||
|
||||
public static byte[] BuildModifyAllegianceStoragePermission(
|
||||
uint sequence, bool enabled) =>
|
||||
BuildUInt32(
|
||||
sequence, ModifyAllegianceStoragePermissionOpcode, enabled ? 1u : 0u);
|
||||
|
||||
// Queries the local player's house info (owned house data, or a
|
||||
// no-house status) — GameActionHouseQuery.Handle: no payload read.
|
||||
public static byte[] BuildHouseQuery(uint sequence) =>
|
||||
|
|
@ -337,6 +500,39 @@ public static class ClientCommandRequests
|
|||
return body;
|
||||
}
|
||||
|
||||
private static byte[] BuildStringUInt32(
|
||||
uint sequence, uint opcode, string value, uint number)
|
||||
{
|
||||
byte[] packed = PackString16L(value);
|
||||
byte[] body = CreateBody(sequence, opcode, packed.Length + 4);
|
||||
packed.CopyTo(body, 12);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
body.AsSpan(12 + packed.Length), number);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static byte[] BuildUInt32String(
|
||||
uint sequence, uint opcode, uint number, string value)
|
||||
{
|
||||
byte[] packed = PackString16L(value);
|
||||
byte[] body = CreateBody(sequence, opcode, 4 + packed.Length);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), number);
|
||||
packed.CopyTo(body, 16);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static byte[] BuildTwoStrings(
|
||||
uint sequence, uint opcode, string first, string second)
|
||||
{
|
||||
byte[] packedFirst = PackString16L(first);
|
||||
byte[] packedSecond = PackString16L(second);
|
||||
byte[] body = CreateBody(
|
||||
sequence, opcode, packedFirst.Length + packedSecond.Length);
|
||||
packedFirst.CopyTo(body, 12);
|
||||
packedSecond.CopyTo(body, 12 + packedFirst.Length);
|
||||
return body;
|
||||
}
|
||||
|
||||
private static byte[] CreateBody(uint sequence, uint opcode, int payloadLength)
|
||||
{
|
||||
byte[] body = new byte[12 + payloadLength];
|
||||
|
|
|
|||
|
|
@ -275,5 +275,6 @@ internal sealed class AckNakScheduler
|
|||
outboundIsaac: null);
|
||||
_send(datagram.Slice(0, datagramLength));
|
||||
_stats.NaksSent++;
|
||||
_stats.NakIdsSent += count;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -263,6 +263,23 @@ internal sealed class OutboundFlowQueue : IDisposable
|
|||
{
|
||||
for (int i = 0; i < _pendingResends.Count; i++)
|
||||
{
|
||||
// A later cumulative ACK can overtake an earlier NAK in the
|
||||
// receive-owner queue before this frame reaches its sweep.
|
||||
// Such a request is stale: the server has already advanced
|
||||
// its ordered watermark past the requested packet. Replaying
|
||||
// the old encrypted key is actively harmful against ACE,
|
||||
// whose CRC/key search runs before duplicate-sequence
|
||||
// rejection and can burn the remaining 256-word window.
|
||||
// The equal case is NOT stale: ids[0] of an ordinary NAK is
|
||||
// also its implicit ACK and still needs retransmission.
|
||||
if (AckWatermark != 0
|
||||
&& SequenceMath.IsNewer(
|
||||
AckWatermark,
|
||||
_pendingResends[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// A pending id can leave the cache between NAK arrival and
|
||||
// this sweep only if a newer ack already covered it — the
|
||||
// server has it; serving nothing is correct.
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ internal sealed class ReliableTransport : IDisposable
|
|||
|
||||
public TransportStats Stats { get; }
|
||||
|
||||
/// <summary>Retail's 40-heartbeat packet-loss percentage.</summary>
|
||||
public double PacketLossPercentage => _packetLoss.Percentage;
|
||||
|
||||
/// <summary>
|
||||
/// N6: retail's ephemeral-info flush cadence
|
||||
/// (<c>Indicator::FlushTimedOutEphInfo @ 0x0054A3D0</c>, the x87 compare
|
||||
|
|
@ -51,6 +54,7 @@ internal sealed class ReliableTransport : IDisposable
|
|||
public const double AssemblerSweepSeconds = 5.0;
|
||||
|
||||
private readonly FragmentAssembler? _assembler;
|
||||
private readonly RetailPacketLossAverager _packetLoss;
|
||||
private readonly long _assemblerSweepTicks;
|
||||
private long _assemblerSweepTimestamp;
|
||||
|
||||
|
|
@ -71,13 +75,19 @@ internal sealed class ReliableTransport : IDisposable
|
|||
_assemblerSweepTicks =
|
||||
(long)Math.Round(AssemblerSweepSeconds * Clock.Frequency);
|
||||
_assemblerSweepTimestamp = Clock.GetTimestamp();
|
||||
void CountedSend(ReadOnlySpan<byte> datagram)
|
||||
{
|
||||
send(datagram);
|
||||
Stats.PacketsSent++;
|
||||
}
|
||||
|
||||
Outbound = new OutboundFlowQueue(
|
||||
outboundIsaac,
|
||||
sessionClientId,
|
||||
sessionIteration,
|
||||
Clock,
|
||||
Stats,
|
||||
send,
|
||||
CountedSend,
|
||||
pool);
|
||||
Inbound = new InboundSequenceTracker(inboundIsaac, Stats);
|
||||
Scheduler = new AckNakScheduler(
|
||||
|
|
@ -87,8 +97,9 @@ internal sealed class ReliableTransport : IDisposable
|
|||
sessionClientId,
|
||||
sessionIteration,
|
||||
Stats,
|
||||
send);
|
||||
CountedSend);
|
||||
Stats.CacheDepthSource = () => Outbound.CacheDepth;
|
||||
_packetLoss = new RetailPacketLossAverager(Clock, Stats);
|
||||
}
|
||||
|
||||
/// <summary>Last reliable sequence on the wire — the value unsequenced
|
||||
|
|
@ -112,6 +123,12 @@ internal sealed class ReliableTransport : IDisposable
|
|||
{
|
||||
Clock.Update();
|
||||
long now = Clock.GetTimestamp();
|
||||
// Retail snapshots the preceding two-second interval inside
|
||||
// ClientNet::ProcessConnection before this heartbeat emits its
|
||||
// ACK/NAK control traffic. Keep boundary control packets in the new
|
||||
// sample rather than attributing them to the interval that just
|
||||
// ended.
|
||||
_packetLoss.Sweep(now, Stats);
|
||||
Scheduler.Sweep(now);
|
||||
Outbound.TransmitPendingResends();
|
||||
|
||||
|
|
|
|||
124
src/AcDream.Core.Net/Transport/RetailPacketLossAverager.cs
Normal file
124
src/AcDream.Core.Net/Transport/RetailPacketLossAverager.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
namespace AcDream.Core.Net.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Retail's packet-loss telemetry window from
|
||||
/// <c>CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610</c> and
|
||||
/// <c>CLinkStatusAverages::AddSnapshot @ 0x00546650</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <c>ClientNet::ProcessConnection @ 0x00545450</c> snapshots the four
|
||||
/// packet counters on its 2.0-second heartbeat. Each counter is a
|
||||
/// <c>CAverager<unsigned short,40></c>. The loss function divides the
|
||||
/// windowed NAK + retransmit totals by received + sent totals and reports a
|
||||
/// percentage. A zero denominator reports zero.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The input values here are cumulative acdream counters; each sample stores
|
||||
/// their delta since the preceding heartbeat. The unchecked ushort conversion
|
||||
/// preserves retail's snapshot-field width.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class RetailPacketLossAverager
|
||||
{
|
||||
public const int WindowSize = 40;
|
||||
public const double SnapshotSeconds = 2.0;
|
||||
|
||||
private readonly Sample[] _samples = new Sample[WindowSize];
|
||||
private readonly long _snapshotTicks;
|
||||
private long _snapshotTimestamp;
|
||||
private long _lastPacketsSent;
|
||||
private long _lastRetransmitsSent;
|
||||
private long _lastPacketsReceived;
|
||||
private long _lastNakIdsSent;
|
||||
private long _sentTotal;
|
||||
private long _retransmitTotal;
|
||||
private long _receivedTotal;
|
||||
private long _nakTotal;
|
||||
private int _next;
|
||||
private int _count;
|
||||
|
||||
public RetailPacketLossAverager(TransportClock clock, TransportStats stats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(clock);
|
||||
ArgumentNullException.ThrowIfNull(stats);
|
||||
|
||||
_snapshotTicks = (long)Math.Round(SnapshotSeconds * clock.Frequency);
|
||||
_snapshotTimestamp = clock.GetTimestamp();
|
||||
CaptureBaselines(stats);
|
||||
}
|
||||
|
||||
public double Percentage
|
||||
{
|
||||
get
|
||||
{
|
||||
long denominator = _receivedTotal + _sentTotal;
|
||||
return denominator <= 0
|
||||
? 0d
|
||||
: 100d * (_nakTotal + _retransmitTotal) / denominator;
|
||||
}
|
||||
}
|
||||
|
||||
public void Sweep(long now, TransportStats stats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stats);
|
||||
if (now - _snapshotTimestamp < _snapshotTicks)
|
||||
return;
|
||||
|
||||
// ProcessConnection records one snapshot when the heartbeat branch
|
||||
// runs; it does not synthesize empty samples for skipped frames.
|
||||
_snapshotTimestamp = now;
|
||||
|
||||
var sample = new Sample(
|
||||
DeltaAsRetailUShort(stats.PacketsSent, ref _lastPacketsSent),
|
||||
DeltaAsRetailUShort(stats.ResendsSent, ref _lastRetransmitsSent),
|
||||
DeltaAsRetailUShort(stats.PacketsReceived, ref _lastPacketsReceived),
|
||||
DeltaAsRetailUShort(stats.NakIdsSent, ref _lastNakIdsSent));
|
||||
|
||||
if (_count == WindowSize)
|
||||
Remove(_samples[_next]);
|
||||
else
|
||||
_count++;
|
||||
|
||||
_samples[_next] = sample;
|
||||
_next = (_next + 1) % WindowSize;
|
||||
Add(sample);
|
||||
}
|
||||
|
||||
private void CaptureBaselines(TransportStats stats)
|
||||
{
|
||||
_lastPacketsSent = stats.PacketsSent;
|
||||
_lastRetransmitsSent = stats.ResendsSent;
|
||||
_lastPacketsReceived = stats.PacketsReceived;
|
||||
_lastNakIdsSent = stats.NakIdsSent;
|
||||
}
|
||||
|
||||
private static ushort DeltaAsRetailUShort(long current, ref long previous)
|
||||
{
|
||||
long delta = current - previous;
|
||||
previous = current;
|
||||
return unchecked((ushort)Math.Max(0L, delta));
|
||||
}
|
||||
|
||||
private void Add(Sample sample)
|
||||
{
|
||||
_sentTotal += sample.Sent;
|
||||
_retransmitTotal += sample.Retransmitted;
|
||||
_receivedTotal += sample.Received;
|
||||
_nakTotal += sample.Naked;
|
||||
}
|
||||
|
||||
private void Remove(Sample sample)
|
||||
{
|
||||
_sentTotal -= sample.Sent;
|
||||
_retransmitTotal -= sample.Retransmitted;
|
||||
_receivedTotal -= sample.Received;
|
||||
_nakTotal -= sample.Naked;
|
||||
}
|
||||
|
||||
private readonly record struct Sample(
|
||||
ushort Sent,
|
||||
ushort Retransmitted,
|
||||
ushort Received,
|
||||
ushort Naked);
|
||||
}
|
||||
|
|
@ -9,6 +9,15 @@ namespace AcDream.Core.Net.Transport;
|
|||
/// </summary>
|
||||
internal sealed class TransportStats
|
||||
{
|
||||
/// <summary>Checksum-valid post-negotiation datagrams received. Used by
|
||||
/// retail's 2-second/40-sample link-status loss window.</summary>
|
||||
public long PacketsReceived;
|
||||
|
||||
/// <summary>Datagrams successfully emitted after transport negotiation.
|
||||
/// Includes reliable traffic, retransmits, cumulative ACKs, and NAKs,
|
||||
/// matching retail's packet-level denominator.</summary>
|
||||
public long PacketsSent;
|
||||
|
||||
/// <summary>Datagrams re-emitted in response to a server NAK.</summary>
|
||||
public long ResendsSent;
|
||||
|
||||
|
|
@ -57,6 +66,11 @@ internal sealed class TransportStats
|
|||
/// <c>ReceiverData::GetNaks @ 0x005490C0</c>, ≤114 ids each).</summary>
|
||||
public long NaksSent;
|
||||
|
||||
/// <summary>Total missing sequence ids carried by emitted NAKs. Retail's
|
||||
/// <c>CLinkStatusSnapshot::nPktsNAKed</c> counts missing packets, not the
|
||||
/// number of control datagrams used to request them.</summary>
|
||||
public long NakIdsSent;
|
||||
|
||||
/// <summary>N4: mis-parked keystream words reclaimed from validated
|
||||
/// cleartext <c>RejectRetransmit</c> sequences (the AD-51 ACE
|
||||
/// adaptation; always zero against a retail server).</summary>
|
||||
|
|
|
|||
|
|
@ -652,7 +652,8 @@ public sealed class WorldSession : IDisposable
|
|||
Volatile.Read(ref _lastInboundPacketTicks),
|
||||
Stopwatch.GetTimestamp(),
|
||||
Stopwatch.Frequency,
|
||||
PingRoundTripSeconds);
|
||||
PingRoundTripSeconds,
|
||||
_transport?.PacketLossPercentage ?? 0d);
|
||||
|
||||
internal double? PingRoundTripSeconds
|
||||
{
|
||||
|
|
@ -1861,6 +1862,8 @@ public sealed class WorldSession : IDisposable
|
|||
// last-heard clock only for valid server traffic. Record at checksum
|
||||
// acceptance, before any heavy render-thread message handling.
|
||||
Volatile.Write(ref _lastInboundPacketTicks, Stopwatch.GetTimestamp());
|
||||
if (_transport is { } acceptedTransport)
|
||||
acceptedTransport.Stats.PacketsReceived++;
|
||||
|
||||
// N6: the first checksum-valid post-negotiation packet confirms the
|
||||
// server accepted our ConnectResponse and stops the handshake
|
||||
|
|
@ -2814,13 +2817,234 @@ public sealed class WorldSession : IDisposable
|
|||
SendGameAction(ClientCommandRequests.BuildRecallAllegianceHometown(seq));
|
||||
}
|
||||
|
||||
/// <summary>Send retail "@allegiance info [name]" (0x027B).</summary>
|
||||
/// <summary>Send retail "@allegiance info <name>" (0x027B).</summary>
|
||||
public void SendAllegianceInfoRequest(string playerName)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildAllegianceInfoRequest(seq, playerName));
|
||||
}
|
||||
|
||||
public void SendBreakAllegianceBoot(string playerName, bool accountBoot)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildBreakAllegianceBoot(
|
||||
seq, playerName, accountBoot));
|
||||
}
|
||||
|
||||
public void SendAllegianceChatBoot(string playerName, string reason)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildAllegianceChatBoot(
|
||||
seq, playerName, reason));
|
||||
}
|
||||
|
||||
public void SendAllegianceChatGag(string playerName, bool enabled)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildAllegianceChatGag(
|
||||
seq, playerName, enabled));
|
||||
}
|
||||
|
||||
public void SendAddAllegianceBan(string playerName)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildAddAllegianceBan(seq, playerName));
|
||||
}
|
||||
|
||||
public void SendRemoveAllegianceBan(string playerName)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildRemoveAllegianceBan(seq, playerName));
|
||||
}
|
||||
|
||||
public void SendListAllegianceBans()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildListAllegianceBans(seq));
|
||||
}
|
||||
|
||||
public void SendSetAllegianceOfficer(string playerName, uint level)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildSetAllegianceOfficer(
|
||||
seq, playerName, level));
|
||||
}
|
||||
|
||||
public void SendRemoveAllegianceOfficer(string playerName)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildRemoveAllegianceOfficer(
|
||||
seq, playerName));
|
||||
}
|
||||
|
||||
public void SendListAllegianceOfficers()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildListAllegianceOfficers(seq));
|
||||
}
|
||||
|
||||
public void SendClearAllegianceOfficers()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildClearAllegianceOfficers(seq));
|
||||
}
|
||||
|
||||
public void SendSetAllegianceOfficerTitle(uint level, string title)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildSetAllegianceOfficerTitle(
|
||||
seq, level, title));
|
||||
}
|
||||
|
||||
public void SendListAllegianceOfficerTitles()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildListAllegianceOfficerTitles(seq));
|
||||
}
|
||||
|
||||
public void SendClearAllegianceOfficerTitles()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildClearAllegianceOfficerTitles(seq));
|
||||
}
|
||||
|
||||
public void SendQueryAllegianceName()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildQueryAllegianceName(seq));
|
||||
}
|
||||
|
||||
public void SendSetAllegianceName(string name)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildSetAllegianceName(seq, name));
|
||||
}
|
||||
|
||||
public void SendClearAllegianceName()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildClearAllegianceName(seq));
|
||||
}
|
||||
|
||||
public void SendAllegianceLockAction(uint action)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildAllegianceLockAction(seq, action));
|
||||
}
|
||||
|
||||
public void SendSetAllegianceApprovedVassal(string playerName)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildSetAllegianceApprovedVassal(
|
||||
seq, playerName));
|
||||
}
|
||||
|
||||
public void SendAllegianceHouseAction(uint action)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildAllegianceHouseAction(seq, action));
|
||||
}
|
||||
|
||||
public void SendQueryMotd()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildQueryMotd(seq));
|
||||
}
|
||||
|
||||
public void SendSetMotd(string motd)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildSetMotd(seq, motd));
|
||||
}
|
||||
|
||||
public void SendClearMotd()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildClearMotd(seq));
|
||||
}
|
||||
|
||||
public void SendSetOpenHouseStatus(bool isOpen)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildSetOpenHouseStatus(seq, isOpen));
|
||||
}
|
||||
|
||||
public void SendAddPermanentGuest(string playerName)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildAddPermanentGuest(seq, playerName));
|
||||
}
|
||||
|
||||
public void SendRemovePermanentGuest(string playerName)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildRemovePermanentGuest(seq, playerName));
|
||||
}
|
||||
|
||||
public void SendRemoveAllPermanentGuests()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildRemoveAllPermanentGuests(seq));
|
||||
}
|
||||
|
||||
public void SendChangeStoragePermission(string playerName, bool enabled)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildChangeStoragePermission(
|
||||
seq, playerName, enabled));
|
||||
}
|
||||
|
||||
public void SendAddAllStoragePermission()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildAddAllStoragePermission(seq));
|
||||
}
|
||||
|
||||
public void SendRemoveAllStoragePermission()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildRemoveAllStoragePermission(seq));
|
||||
}
|
||||
|
||||
public void SendRequestFullGuestList()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildRequestFullGuestList(seq));
|
||||
}
|
||||
|
||||
public void SendBootSpecificHouseGuest(string playerName)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildBootSpecificHouseGuest(
|
||||
seq, playerName));
|
||||
}
|
||||
|
||||
public void SendBootEveryone()
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildBootEveryone(seq));
|
||||
}
|
||||
|
||||
public void SendSetHooksVisibility(bool visible)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildSetHooksVisibility(seq, visible));
|
||||
}
|
||||
|
||||
public void SendModifyAllegianceGuestPermission(bool enabled)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildModifyAllegianceGuestPermission(
|
||||
seq, enabled));
|
||||
}
|
||||
|
||||
public void SendModifyAllegianceStoragePermission(bool enabled)
|
||||
{
|
||||
uint seq = NextGameActionSequence();
|
||||
SendGameAction(ClientCommandRequests.BuildModifyAllegianceStoragePermission(
|
||||
seq, enabled));
|
||||
}
|
||||
|
||||
// ── Campaign FA slice FA2 (2026-08-12): fellowship + allegiance
|
||||
// outbound wrappers. SocialActions/AllegianceRequests ship the byte
|
||||
// builders (repaired/added in FA1); this is the missing
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue