fix: Fixes tick count wrap-around in movement throttle, and eliminates more allocations in NetState (#2603)
## Summary Removes the per-tick allocation in the movement throttle, fixes tick-count wrap-around bugs in the throttle and RTT probe state, and trims per-connection allocations and dead fields in `NetState`. ## Movement throttle - **No more per-tick `List<NetState>` snapshot.** `ProcessAllQueues()` iterates the `HashSet` directly and removes drained or disconnected states in place. `HashSet<T>.Remove` does not invalidate enumerators on .NET Core 3.0+ (verified on 10.0.11); only inserting a *new* member does, and the only `Add` is in the packet handler, which never nests with `Slice()`. The eager `Remove` calls in `RejectAndReset`, `ClearQueue`, and `ProcessMovementQueue` are gone; membership is reconciled once per tick from `_hasQueuedMovements`. - **Debug logging** is now gated solely by the per-connection `NetState.MovementLogging` flag. The global `movementThrottle.debugLogging` setting is removed. - **New settings**: `movementThrottle.maxRttBonus`, `movementThrottle.maxChainGap`, and `movementThrottle.speedHackNotificationCooldown` were fields with no config binding. ## Tick-count wrap-around All comparisons are now in subtraction form and no tick field uses zero as a sentinel: - `now < _nextMovementTime` in the queue drain loop → `now - _nextMovementTime < 0`. - `_lastMovementRecordTime > 0`, `_lastSpeedHackNotification`, `_rttProbeTime > 0`, and `_nextRttProbe == 0` sentinels replaced with `_hasMovementRecord`, `_speedHackNotified`, `_rttProbePending`, and a seeded `_nextRttProbe`. - `_lastQueueDepthCheck` and `_movementWindowStart` are seeded from `Core.TickCount` at construction and on reset instead of zero. User-visible effects of the old code: on hosts with pass-through counters (GCP) movement history never recorded and speed hack detection was silently off; on every host, staff speed hack notifications were suppressed until `Core.TickCount` exceeded the five-minute cooldown. ## NetState - `Instances` returns `HashSet<NetState>` again so engine-internal `foreach` uses the struct enumerator instead of boxing through `IReadOnlySet<T>`. - Removed `_sustainedQueueDepth` (declared and zeroed since #2266, never read), `_lastRtt` (now derived as `LastRtt` from the newest history slot), and `_rttProbeTimestampHiRes` (only fed one debug log line). 20 bytes per connection. - `HuePickers`, `Menus`, and `Trades` are lazily created instead of allocating three lists per connection, including every login-server connection that dies on shard select. `Trades` is released when it empties. All helpers and the `HuePickerResponse` / `MenuResponse` handlers are null-tolerant; the trade cancel loops keep their `i < Count` guards because `SecureTrade.Cancel()` runs virtual item hooks that can re-enter the same list. ## Testing - `dotnet build -c Release` clean. - All MovementThrottle tests pass (27), plus the Trade / Menu / HuePicker / NetState tests (32).
This commit is contained in:
parent
c9875e7f64
commit
547c2ea0fa
5 changed files with 153 additions and 120 deletions
|
|
@ -1627,7 +1627,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
||||||
|
|
||||||
public virtual bool KeepsItemsOnDeath => m_AccessLevel > AccessLevel.Player;
|
public virtual bool KeepsItemsOnDeath => m_AccessLevel > AccessLevel.Player;
|
||||||
|
|
||||||
public bool HasTrade => m_NetState?.Trades.Count > 0;
|
public bool HasTrade => m_NetState?.Trades?.Count > 0;
|
||||||
|
|
||||||
public bool NoMoveHS { get; set; }
|
public bool NoMoveHS { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,6 @@ public static class MovementThrottle
|
||||||
private const int ClientMaxUnackedMovements = 5;
|
private const int ClientMaxUnackedMovements = 5;
|
||||||
private const int MaxQueueWithUnmodifiedClient = ClientMaxUnackedMovements - 1; // 4
|
private const int MaxQueueWithUnmodifiedClient = ClientMaxUnackedMovements - 1; // 4
|
||||||
|
|
||||||
// Debug logging - enable for testing speed hack detection
|
|
||||||
private static bool _debugLogging = false;
|
|
||||||
|
|
||||||
// Track NetStates with queued movements for efficient processing
|
// Track NetStates with queued movements for efficient processing
|
||||||
private static readonly HashSet<NetState> _netStatesWithQueuedMovements = new(256);
|
private static readonly HashSet<NetState> _netStatesWithQueuedMovements = new(256);
|
||||||
|
|
||||||
|
|
@ -83,15 +80,9 @@ public static class MovementThrottle
|
||||||
|
|
||||||
public static void Configure()
|
public static void Configure()
|
||||||
{
|
{
|
||||||
_maxCredit = ServerConfiguration.GetOrUpdateSetting(
|
_maxCredit = ServerConfiguration.GetOrUpdateSetting("movementThrottle.maxCredit", _maxCredit);
|
||||||
"movementThrottle.maxCredit",
|
_maxRttBonus = ServerConfiguration.GetOrUpdateSetting("movementThrottle.maxRttBonus", _maxRttBonus);
|
||||||
_maxCredit
|
_hardQueueLimit = ServerConfiguration.GetOrUpdateSetting("movementThrottle.hardQueueLimit", _hardQueueLimit);
|
||||||
);
|
|
||||||
|
|
||||||
_hardQueueLimit = ServerConfiguration.GetOrUpdateSetting(
|
|
||||||
"movementThrottle.hardQueueLimit",
|
|
||||||
_hardQueueLimit
|
|
||||||
);
|
|
||||||
|
|
||||||
_movementHistorySize = ServerConfiguration.GetOrUpdateSetting(
|
_movementHistorySize = ServerConfiguration.GetOrUpdateSetting(
|
||||||
"movementThrottle.movementHistorySize",
|
"movementThrottle.movementHistorySize",
|
||||||
|
|
@ -103,6 +94,13 @@ public static class MovementThrottle
|
||||||
_minSamplesForRate
|
_minSamplesForRate
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_maxChainGap = ServerConfiguration.GetOrUpdateSetting("movementThrottle.maxChainGap", _maxChainGap);
|
||||||
|
|
||||||
|
_speedHackNotificationCooldown = ServerConfiguration.GetOrUpdateSetting(
|
||||||
|
"movementThrottle.speedHackNotificationCooldown",
|
||||||
|
_speedHackNotificationCooldown
|
||||||
|
);
|
||||||
|
|
||||||
_suspiciousRateThreshold = (float)ServerConfiguration.GetOrUpdateSetting(
|
_suspiciousRateThreshold = (float)ServerConfiguration.GetOrUpdateSetting(
|
||||||
"movementThrottle.suspiciousRateThreshold",
|
"movementThrottle.suspiciousRateThreshold",
|
||||||
_suspiciousRateThreshold
|
_suspiciousRateThreshold
|
||||||
|
|
@ -112,11 +110,6 @@ public static class MovementThrottle
|
||||||
"movementThrottle.definiteRateThreshold",
|
"movementThrottle.definiteRateThreshold",
|
||||||
_definiteRateThreshold
|
_definiteRateThreshold
|
||||||
);
|
);
|
||||||
|
|
||||||
_debugLogging = ServerConfiguration.GetOrUpdateSetting(
|
|
||||||
"movementThrottle.debugLogging",
|
|
||||||
_debugLogging
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -191,15 +184,16 @@ public static class MovementThrottle
|
||||||
// Credit can go negative up to -dynamicCredit (debt limit)
|
// Credit can go negative up to -dynamicCredit (debt limit)
|
||||||
if (ns._movementCredit - earlyAmount >= -dynamicCredit)
|
if (ns._movementCredit - earlyAmount >= -dynamicCredit)
|
||||||
{
|
{
|
||||||
var prevCredit = ns._movementCredit;
|
|
||||||
// Use credit to cover early arrival
|
// Use credit to cover early arrival
|
||||||
ns._movementCredit -= earlyAmount;
|
ns._movementCredit -= earlyAmount;
|
||||||
|
|
||||||
if (_debugLogging && ns._movementLogging)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
|
var prevCredit = ns._movementCredit + earlyAmount;
|
||||||
|
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[Credit] {Name}: delta={Delta}ms early={Early}ms credit={PrevCredit}->{Credit}/{MaxCredit} action=execute",
|
"[Credit] {Name}: delta={Delta}ms early={Early}ms credit={PrevCredit}->{Credit}/{MaxCredit} action=execute",
|
||||||
mobile.RawName, delta, earlyAmount, prevCredit, ns._movementCredit, dynamicCredit
|
mobile, delta, earlyAmount, prevCredit, ns._movementCredit, dynamicCredit
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,11 +202,11 @@ public static class MovementThrottle
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_debugLogging && ns._movementLogging)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[Credit] {Name}: delta={Delta}ms early={Early}ms credit={Credit}/{MaxCredit} EXHAUSTED -> queue",
|
"[Credit] {Name}: delta={Delta}ms early={Early}ms credit={Credit}/{MaxCredit} EXHAUSTED -> queue",
|
||||||
mobile.RawName, delta, earlyAmount, ns._movementCredit, dynamicCredit
|
mobile, delta, earlyAmount, ns._movementCredit, dynamicCredit
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,11 +221,11 @@ public static class MovementThrottle
|
||||||
var prevCredit = ns._movementCredit;
|
var prevCredit = ns._movementCredit;
|
||||||
ns._movementCredit = Math.Min(ns._movementCredit + delta, dynamicCredit);
|
ns._movementCredit = Math.Min(ns._movementCredit + delta, dynamicCredit);
|
||||||
|
|
||||||
if (_debugLogging && ns._movementLogging && ns._movementCredit != prevCredit)
|
if (ns._movementLogging && ns._movementCredit != prevCredit)
|
||||||
{
|
{
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[Credit] {Name}: delta=+{Delta}ms credit={PrevCredit}->{Credit}/{MaxCredit} action=execute",
|
"[Credit] {Name}: delta=+{Delta}ms credit={PrevCredit}->{Credit}/{MaxCredit} action=execute",
|
||||||
mobile.RawName, delta, prevCredit, ns._movementCredit, dynamicCredit
|
mobile, delta, prevCredit, ns._movementCredit, dynamicCredit
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -247,12 +241,9 @@ public static class MovementThrottle
|
||||||
{
|
{
|
||||||
if (!mobile.Move(dir))
|
if (!mobile.Move(dir))
|
||||||
{
|
{
|
||||||
if (_debugLogging && ns._movementLogging)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
logger.Debug(
|
logger.Debug("[Execute] {Name}: Move FAILED dir={Dir} seq={Seq} -> reject+reset", mobile, dir, seq);
|
||||||
"[Execute] {Name}: Move FAILED dir={Dir} seq={Seq} -> reject+reset",
|
|
||||||
mobile.RawName, dir, seq
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Movement failed (blocked, paralyzed, frozen, etc.)
|
// Movement failed (blocked, paralyzed, frozen, etc.)
|
||||||
|
|
@ -260,11 +251,11 @@ public static class MovementThrottle
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_debugLogging && ns._movementLogging)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[Execute] {Name}: Move OK dir={Dir} seq={Seq} nextMove={NextMove}ms",
|
"[Execute] {Name}: Move OK dir={Dir} seq={Seq} nextMove={NextMove}ms",
|
||||||
mobile.RawName, dir, seq, ns._nextMovementTime - Core.TickCount
|
mobile, dir, seq, ns._nextMovementTime - Core.TickCount
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,11 +295,11 @@ public static class MovementThrottle
|
||||||
ns._hasQueuedMovements = true;
|
ns._hasQueuedMovements = true;
|
||||||
_netStatesWithQueuedMovements.Add(ns);
|
_netStatesWithQueuedMovements.Add(ns);
|
||||||
|
|
||||||
if (_debugLogging && ns._movementLogging)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[Queue] {Name}: enqueued dir={Dir} seq={Seq} (depth={Depth})",
|
"[Queue] {Name}: enqueued dir={Dir} seq={Seq} (depth={Depth})",
|
||||||
ns.Mobile?.RawName, dir, seq, ns._movementQueue.Count
|
ns.Mobile, dir, seq, ns._movementQueue.Count
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -320,7 +311,6 @@ public static class MovementThrottle
|
||||||
{
|
{
|
||||||
ns.SendMovementRej(seq, mobile);
|
ns.SendMovementRej(seq, mobile);
|
||||||
ns.ResetMovementState();
|
ns.ResetMovementState();
|
||||||
_netStatesWithQueuedMovements.Remove(ns);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -333,20 +323,18 @@ public static class MovementThrottle
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each NetState with queued movements
|
foreach (var ns in _netStatesWithQueuedMovements)
|
||||||
// Use a snapshot to avoid modification during iteration
|
|
||||||
var toProcess = new List<NetState>(_netStatesWithQueuedMovements);
|
|
||||||
|
|
||||||
for (var i = 0; i < toProcess.Count; i++)
|
|
||||||
{
|
{
|
||||||
var ns = toProcess[i];
|
if (ns.Running)
|
||||||
if (!ns.Running)
|
|
||||||
{
|
{
|
||||||
_netStatesWithQueuedMovements.Remove(ns);
|
ProcessMovementQueue(ns);
|
||||||
continue;
|
if (ns._hasQueuedMovements)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessMovementQueue(ns);
|
_netStatesWithQueuedMovements.Remove(ns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -356,6 +344,7 @@ public static class MovementThrottle
|
||||||
public static void ProcessMovementQueue(NetState ns)
|
public static void ProcessMovementQueue(NetState ns)
|
||||||
{
|
{
|
||||||
var mobile = ns.Mobile;
|
var mobile = ns.Mobile;
|
||||||
|
|
||||||
if (mobile?.Deleted != false)
|
if (mobile?.Deleted != false)
|
||||||
{
|
{
|
||||||
ClearQueue(ns);
|
ClearQueue(ns);
|
||||||
|
|
@ -374,7 +363,7 @@ public static class MovementThrottle
|
||||||
while (ns._movementQueue?.Count > 0)
|
while (ns._movementQueue?.Count > 0)
|
||||||
{
|
{
|
||||||
// Check if it's time to execute
|
// Check if it's time to execute
|
||||||
if (now < ns._nextMovementTime)
|
if (now - ns._nextMovementTime < 0)
|
||||||
{
|
{
|
||||||
// Not yet - leave remaining items in queue for next Slice
|
// Not yet - leave remaining items in queue for next Slice
|
||||||
break;
|
break;
|
||||||
|
|
@ -394,11 +383,11 @@ public static class MovementThrottle
|
||||||
// Execute the move
|
// Execute the move
|
||||||
if (!mobile.Move(movement.Direction))
|
if (!mobile.Move(movement.Direction))
|
||||||
{
|
{
|
||||||
if (_debugLogging && ns._movementLogging)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[Queue] {Name}: dequeued FAILED dir={Dir} (remaining={Remaining})",
|
"[Queue] {Name}: dequeued FAILED dir={Dir} (remaining={Remaining})",
|
||||||
mobile.RawName, movement.Direction, remaining
|
mobile, movement.Direction, remaining
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,12 +396,12 @@ public static class MovementThrottle
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_debugLogging && ns._movementLogging)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
var waited = now - ns._nextMovementTime;
|
var waited = now - ns._nextMovementTime;
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[Queue] {Name}: dequeued OK dir={Dir} (remaining={Remaining}, waited={Waited}ms)",
|
"[Queue] {Name}: dequeued OK dir={Dir} (remaining={Remaining}, waited={Waited}ms)",
|
||||||
mobile.RawName, movement.Direction, remaining, waited >= 0 ? waited : 0
|
mobile, movement.Direction, remaining, waited >= 0 ? waited : 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,10 +419,6 @@ public static class MovementThrottle
|
||||||
|
|
||||||
// Update tracking
|
// Update tracking
|
||||||
ns._hasQueuedMovements = ns._movementQueue?.Count > 0;
|
ns._hasQueuedMovements = ns._movementQueue?.Count > 0;
|
||||||
if (!ns._hasQueuedMovements)
|
|
||||||
{
|
|
||||||
_netStatesWithQueuedMovements.Remove(ns);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -469,7 +454,6 @@ public static class MovementThrottle
|
||||||
{
|
{
|
||||||
ns._movementQueue?.Clear();
|
ns._movementQueue?.Clear();
|
||||||
ns._hasQueuedMovements = false;
|
ns._hasQueuedMovements = false;
|
||||||
_netStatesWithQueuedMovements.Remove(ns);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximum expected packets per second (mounted running = 100ms = 10/sec, plus tolerance)
|
// Maximum expected packets per second (mounted running = 100ms = 10/sec, plus tolerance)
|
||||||
|
|
@ -484,7 +468,7 @@ public static class MovementThrottle
|
||||||
logger.Information(
|
logger.Information(
|
||||||
"Movement queue overflow: {Character} ({Account}) | " +
|
"Movement queue overflow: {Character} ({Account}) | " +
|
||||||
"Queue reached hard limit: {Limit} | IP: {IP}",
|
"Queue reached hard limit: {Limit} | IP: {IP}",
|
||||||
mobile?.RawName ?? "Unknown",
|
mobile,
|
||||||
ns.Account?.Username ?? "Unknown",
|
ns.Account?.Username ?? "Unknown",
|
||||||
_hardQueueLimit,
|
_hardQueueLimit,
|
||||||
ns.Address
|
ns.Address
|
||||||
|
|
@ -516,7 +500,7 @@ public static class MovementThrottle
|
||||||
private static void RecordMovement(NetState ns, long now, int cost, Direction dir, Mobile mobile)
|
private static void RecordMovement(NetState ns, long now, int cost, Direction dir, Mobile mobile)
|
||||||
{
|
{
|
||||||
// Calculate interval since last movement
|
// Calculate interval since last movement
|
||||||
var interval = ns._lastMovementRecordTime > 0
|
var interval = ns._hasMovementRecord
|
||||||
? (int)(now - ns._lastMovementRecordTime)
|
? (int)(now - ns._lastMovementRecordTime)
|
||||||
: -1; // -1 indicates first movement (no previous time)
|
: -1; // -1 indicates first movement (no previous time)
|
||||||
|
|
||||||
|
|
@ -525,6 +509,7 @@ public static class MovementThrottle
|
||||||
if (interval <= 0 || interval > _maxChainGap)
|
if (interval <= 0 || interval > _maxChainGap)
|
||||||
{
|
{
|
||||||
ns._lastMovementRecordTime = now;
|
ns._lastMovementRecordTime = now;
|
||||||
|
ns._hasMovementRecord = true;
|
||||||
|
|
||||||
// Use RTT to distinguish "stopped moving" vs "lagged"
|
// Use RTT to distinguish "stopped moving" vs "lagged"
|
||||||
// - Stable low-latency connection with gap >> RTT → player stopped, reset history
|
// - Stable low-latency connection with gap >> RTT → player stopped, reset history
|
||||||
|
|
@ -544,19 +529,19 @@ public static class MovementThrottle
|
||||||
// A large gap followed by a burst of packets = likely lag recovery, not speed hack
|
// A large gap followed by a burst of packets = likely lag recovery, not speed hack
|
||||||
ns._lastGapDuration = interval;
|
ns._lastGapDuration = interval;
|
||||||
|
|
||||||
if (_debugLogging && mobile?.RawName != null)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
var action = shouldReset ? "history reset" : "history preserved (possible lag)";
|
var action = shouldReset ? "history reset" : "history preserved (possible lag)";
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[Movement] {Name}: SKIP recording (gap {Gap}ms > {MaxGap}ms, " +
|
"[Movement] {Name}: SKIP recording (gap {Gap}ms > {MaxGap}ms, " +
|
||||||
"RTT={RTT}ms stable={Stable} → {Action})",
|
"RTT={RTT}ms stable={Stable} → {Action})",
|
||||||
mobile.RawName, interval, _maxChainGap, avgRtt, ns.HasStableConnection, action
|
mobile, interval, _maxChainGap, avgRtt, ns.HasStableConnection, action
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_debugLogging && mobile?.RawName != null)
|
else if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
logger.Debug("[Movement] {Name}: SKIP recording (first in chain)", mobile.RawName);
|
logger.Debug("[Movement] {Name}: SKIP recording (first in chain)", mobile);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -572,12 +557,9 @@ public static class MovementThrottle
|
||||||
// the next real move's interval artificially short, inflating rate.
|
// the next real move's interval artificially short, inflating rate.
|
||||||
if (cost == 0)
|
if (cost == 0)
|
||||||
{
|
{
|
||||||
if (_debugLogging && mobile?.RawName != null)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
logger.Debug(
|
logger.Debug("[Movement] {Name}: SKIP direction-only change (preserves interval measurement)", mobile);
|
||||||
"[Movement] {Name}: SKIP direction-only change (preserves interval measurement)",
|
|
||||||
mobile.RawName
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -613,15 +595,16 @@ public static class MovementThrottle
|
||||||
}
|
}
|
||||||
|
|
||||||
ns._lastMovementRecordTime = now;
|
ns._lastMovementRecordTime = now;
|
||||||
|
ns._hasMovementRecord = true;
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
if (_debugLogging && mobile?.RawName != null)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
var historyCount = ns._movementHistoryFull ? _movementHistorySize : ns._movementHistoryIndex;
|
var historyCount = ns._movementHistoryFull ? _movementHistorySize : ns._movementHistoryIndex;
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[Movement] {Name}: interval={Interval}ms target={Target}ms queue={Queue} " +
|
"[Movement] {Name}: interval={Interval}ms target={Target}ms queue={Queue} " +
|
||||||
"flags={Flags} history={History}/{MaxHistory} RTT={RTT}ms",
|
"flags={Flags} history={History}/{MaxHistory} RTT={RTT}ms",
|
||||||
mobile.RawName, interval, cost, record.QueueDepth,
|
mobile, interval, cost, record.QueueDepth,
|
||||||
flags, historyCount, _movementHistorySize, ns.AverageRtt
|
flags, historyCount, _movementHistorySize, ns.AverageRtt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -814,7 +797,7 @@ public static class MovementThrottle
|
||||||
var averageRtt = ns.AverageRtt;
|
var averageRtt = ns.AverageRtt;
|
||||||
|
|
||||||
// Detailed rate breakdown for debugging
|
// Detailed rate breakdown for debugging
|
||||||
if (_debugLogging)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
logger.Debug("[MovementAnalysis] Rate={Rate:F3}, Samples={Samples}, RTT={RTT}ms",
|
logger.Debug("[MovementAnalysis] Rate={Rate:F3}, Samples={Samples}, RTT={RTT}ms",
|
||||||
rate, sampleCount, averageRtt);
|
rate, sampleCount, averageRtt);
|
||||||
|
|
@ -977,19 +960,19 @@ public static class MovementThrottle
|
||||||
var verdict = AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence);
|
var verdict = AnalyzeMovement(ns, out var rate, out var sampleCount, out var confidence);
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
if (_debugLogging && ns.Mobile?.RawName != null)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
var (burstSize, _) = DetectRecentBurst(ns);
|
var (burstSize, _) = DetectRecentBurst(ns);
|
||||||
var probeStatus = ns._rttProbeTime > 0 ? "pending" : "idle";
|
var probeStatus = ns._rttProbePending ? "pending" : "idle";
|
||||||
var queueDepth = ns._movementQueue?.Count ?? 0;
|
var queueDepth = ns._movementQueue?.Count ?? 0;
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[RateCheck] {Name}: rate={Rate:F3} samples={Samples} verdict={Verdict} " +
|
"[RateCheck] {Name}: rate={Rate:F3} samples={Samples} verdict={Verdict} " +
|
||||||
"confidence={Confidence:P0} queue={Queue} burst={Burst} sustained={Sustained}s",
|
"confidence={Confidence:P0} queue={Queue} burst={Burst} sustained={Sustained}s",
|
||||||
ns.Mobile.RawName, rate, sampleCount, verdict, confidence, queueDepth, burstSize, ns._consecutiveHighRateSeconds
|
ns.Mobile, rate, sampleCount, verdict, confidence, queueDepth, burstSize, ns._consecutiveHighRateSeconds
|
||||||
);
|
);
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
" RTT: avg={Avg}ms last={Last}ms var={Var} samples={RttSamples} stable={Stable} probe={Probe}",
|
" RTT: avg={Avg}ms last={Last}ms var={Var} samples={RttSamples} stable={Stable} probe={Probe}",
|
||||||
ns.AverageRtt, ns._lastRtt, ns._rttVariance, ns._rttSampleCount, ns.HasStableConnection, probeStatus
|
ns.AverageRtt, ns.LastRtt, ns._rttVariance, ns._rttSampleCount, ns.HasStableConnection, probeStatus
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1025,11 +1008,11 @@ public static class MovementThrottle
|
||||||
|
|
||||||
if (shouldNotify)
|
if (shouldNotify)
|
||||||
{
|
{
|
||||||
if (_debugLogging)
|
if (ns._movementLogging)
|
||||||
{
|
{
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
"[ALERT] {Urgency} - {Name}: rate={Rate:F3} verdict={Verdict} confidence={Confidence:P0}",
|
"[ALERT] {Urgency} - {Name}: rate={Rate:F3} verdict={Verdict} confidence={Confidence:P0}",
|
||||||
urgency, ns.Mobile?.RawName, rate, verdict, confidence
|
urgency, ns.Mobile, rate, verdict, confidence
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
NotifyStaff(ns, rate, sampleCount, confidence, verdict, urgency);
|
NotifyStaff(ns, rate, sampleCount, confidence, verdict, urgency);
|
||||||
|
|
@ -1054,11 +1037,12 @@ public static class MovementThrottle
|
||||||
var now = Core.TickCount;
|
var now = Core.TickCount;
|
||||||
|
|
||||||
// Rate-limit notifications per player
|
// Rate-limit notifications per player
|
||||||
if (now - ns._lastSpeedHackNotification < _speedHackNotificationCooldown)
|
if (ns._speedHackNotified && now - ns._lastSpeedHackNotification < _speedHackNotificationCooldown)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ns._speedHackNotified = true;
|
||||||
ns._lastSpeedHackNotification = now;
|
ns._lastSpeedHackNotification = now;
|
||||||
|
|
||||||
var mobile = ns.Mobile;
|
var mobile = ns.Mobile;
|
||||||
|
|
@ -1070,7 +1054,7 @@ public static class MovementThrottle
|
||||||
"PacketRate: {PacketRate}/s (peak: {PeakRate}/s) | RTT: {Rtt}ms (stable: {Stable}) | " +
|
"PacketRate: {PacketRate}/s (peak: {PeakRate}/s) | RTT: {Rtt}ms (stable: {Stable}) | " +
|
||||||
"Sustained: {Sustained}s | Queue: {Queue} | Location: {Location} Map: {Map} | IP: {IP}",
|
"Sustained: {Sustained}s | Queue: {Queue} | Location: {Location} Map: {Map} | IP: {IP}",
|
||||||
urgency,
|
urgency,
|
||||||
mobile?.RawName ?? "Unknown",
|
mobile,
|
||||||
ns.Account?.Username ?? "Unknown",
|
ns.Account?.Username ?? "Unknown",
|
||||||
rate,
|
rate,
|
||||||
sampleCount,
|
sampleCount,
|
||||||
|
|
@ -1138,7 +1122,7 @@ public static class MovementThrottle
|
||||||
Verdict = verdict,
|
Verdict = verdict,
|
||||||
Confidence = confidence,
|
Confidence = confidence,
|
||||||
AverageRtt = ns.AverageRtt,
|
AverageRtt = ns.AverageRtt,
|
||||||
LastRtt = ns._lastRtt,
|
LastRtt = ns.LastRtt,
|
||||||
RttVariance = ns._rttVariance,
|
RttVariance = ns._rttVariance,
|
||||||
StableConnection = ns.HasStableConnection,
|
StableConnection = ns.HasStableConnection,
|
||||||
RttSampleCount = ns._rttSampleCount,
|
RttSampleCount = ns._rttSampleCount,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Server.Logging;
|
using Server.Logging;
|
||||||
|
|
||||||
|
|
@ -70,23 +69,24 @@ public partial class NetState
|
||||||
internal Queue<QueuedMovement> _movementQueue; // Lazy initialized
|
internal Queue<QueuedMovement> _movementQueue; // Lazy initialized
|
||||||
internal long _movementCredit; // Credit buffer for timing jitter
|
internal long _movementCredit; // Credit buffer for timing jitter
|
||||||
internal long _nextMovementTime = Core.TickCount; // When next movement is allowed
|
internal long _nextMovementTime = Core.TickCount; // When next movement is allowed
|
||||||
internal int _sustainedQueueDepth; // Tracks sustained high queue depth
|
internal long _lastQueueDepthCheck = Core.TickCount; // Throttle depth check frequency
|
||||||
internal long _lastQueueDepthCheck; // Throttle depth check frequency
|
|
||||||
internal bool _hasQueuedMovements; // Fast check for Slice()
|
internal bool _hasQueuedMovements; // Fast check for Slice()
|
||||||
|
|
||||||
// Movement history for rate-based speed hack detection (lazy initialized)
|
// Movement history for rate-based speed hack detection (lazy initialized)
|
||||||
internal MovementRecord[] _movementHistory; // Circular buffer
|
internal MovementRecord[] _movementHistory; // Circular buffer
|
||||||
internal int _movementHistoryIndex; // Next write position (also serves as count until full)
|
internal int _movementHistoryIndex; // Next write position (also serves as count until full)
|
||||||
internal bool _movementHistoryFull; // True once buffer has wrapped
|
internal bool _movementHistoryFull; // True once buffer has wrapped
|
||||||
internal long _lastMovementRecordTime; // For calculating intervals
|
internal long _lastMovementRecordTime; // For calculating intervals (valid only when _hasMovementRecord)
|
||||||
|
internal bool _hasMovementRecord; // False until the first movement in a chain is seen
|
||||||
|
|
||||||
// Detection state
|
// Detection state
|
||||||
internal int _consecutiveHighRateSeconds; // Sustained detection counter
|
internal int _consecutiveHighRateSeconds; // Sustained detection counter
|
||||||
internal long _lastSpeedHackNotification; // Rate-limit notifications
|
internal long _lastSpeedHackNotification; // Rate-limit notifications (valid only when _speedHackNotified)
|
||||||
|
internal bool _speedHackNotified; // False until the first notification is sent
|
||||||
internal int _lastGapDuration; // Duration of last gap > maxChainGap (for burst forgiveness)
|
internal int _lastGapDuration; // Duration of last gap > maxChainGap (for burst forgiveness)
|
||||||
|
|
||||||
// Movement packet rate tracking (for speed hack detection)
|
// Movement packet rate tracking (for speed hack detection)
|
||||||
internal long _movementWindowStart; // Start of current 1-second window
|
internal long _movementWindowStart = Core.TickCount; // Start of current 1-second window
|
||||||
internal int _movementsInWindow; // Count in current window
|
internal int _movementsInWindow; // Count in current window
|
||||||
internal int _peakMovementRate; // Highest rate seen (packets/sec)
|
internal int _peakMovementRate; // Highest rate seen (packets/sec)
|
||||||
|
|
||||||
|
|
@ -100,10 +100,9 @@ public partial class NetState
|
||||||
_nextMovementTime = Core.TickCount;
|
_nextMovementTime = Core.TickCount;
|
||||||
_movementCredit = 0;
|
_movementCredit = 0;
|
||||||
_hasQueuedMovements = false;
|
_hasQueuedMovements = false;
|
||||||
_sustainedQueueDepth = 0;
|
|
||||||
|
|
||||||
// Reset movement history - next movement starts a new chain
|
// Reset movement history - next movement starts a new chain
|
||||||
_lastMovementRecordTime = 0;
|
_hasMovementRecord = false;
|
||||||
_movementHistoryIndex = 0;
|
_movementHistoryIndex = 0;
|
||||||
_movementHistoryFull = false;
|
_movementHistoryFull = false;
|
||||||
|
|
||||||
|
|
@ -113,7 +112,7 @@ public partial class NetState
|
||||||
_rttProbeInterval = RttProbeIntervalNormal;
|
_rttProbeInterval = RttProbeIntervalNormal;
|
||||||
|
|
||||||
// Reset packet rate window
|
// Reset packet rate window
|
||||||
_movementWindowStart = 0;
|
_movementWindowStart = Core.TickCount;
|
||||||
_movementsInWindow = 0;
|
_movementsInWindow = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,17 +164,19 @@ public partial class NetState
|
||||||
private const long MaxStableLatency = 200; // Max RTT (ms) for "stable" connection
|
private const long MaxStableLatency = 200; // Max RTT (ms) for "stable" connection
|
||||||
|
|
||||||
// RTT state
|
// RTT state
|
||||||
internal long _rttProbeTime; // When we sent the probe (0 = not waiting)
|
internal bool _rttProbePending; // True while waiting for a probe response
|
||||||
internal long _lastRtt; // Most recent RTT measurement
|
internal long _rttProbeTime; // When we sent the probe (valid only when _rttProbePending)
|
||||||
internal long[] _rttHistory; // Rolling history (lazy init)
|
internal long[] _rttHistory; // Rolling history (lazy init)
|
||||||
internal int _rttHistoryIndex; // Current position in history
|
internal int _rttHistoryIndex; // Current position in history
|
||||||
internal int _rttSampleCount; // Number of samples collected (saturates at RttHistorySize)
|
internal int _rttSampleCount; // Number of samples collected (saturates at RttHistorySize)
|
||||||
internal long _rttVariance; // Calculated variance for stability
|
internal long _rttVariance; // Calculated variance for stability
|
||||||
internal long _nextRttProbe; // When to send next probe
|
internal long _nextRttProbe = Core.TickCount; // When to send next probe
|
||||||
internal int _rttProbeInterval = RttProbeIntervalNormal; // Current probe interval
|
internal int _rttProbeInterval = RttProbeIntervalNormal; // Current probe interval
|
||||||
|
|
||||||
// High-resolution timestamp for RTT measurement (Stopwatch ticks, not game loop ticks)
|
/// <summary>
|
||||||
private long _rttProbeTimestampHiRes;
|
/// Gets the most recent RTT measurement, or 0 if none has been recorded.
|
||||||
|
/// </summary>
|
||||||
|
public long LastRtt => _rttSampleCount > 0 ? _rttHistory[(_rttHistoryIndex - 1) & (RttHistorySize - 1)] : 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the RTT probe interval based on suspicion level.
|
/// Sets the RTT probe interval based on suspicion level.
|
||||||
|
|
@ -206,23 +207,22 @@ public partial class NetState
|
||||||
var now = Core.TickCount;
|
var now = Core.TickCount;
|
||||||
|
|
||||||
// Don't send if we're still waiting for a response
|
// Don't send if we're still waiting for a response
|
||||||
if (_rttProbeTime > 0)
|
if (_rttProbePending)
|
||||||
{
|
{
|
||||||
// Timeout after 10 seconds - connection is probably dead or very laggy
|
// Timeout after 10 seconds - connection is probably dead or very laggy
|
||||||
if (now - _rttProbeTime > 10000)
|
if (now - _rttProbeTime > 10000)
|
||||||
{
|
{
|
||||||
_rttProbeTime = 0;
|
_rttProbePending = false;
|
||||||
_rttProbeTimestampHiRes = 0;
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First probe: send immediately when player starts moving
|
// First probe: send immediately when player starts moving
|
||||||
// Subsequent probes: send when interval has passed
|
// Subsequent probes: send when interval has passed
|
||||||
if (_nextRttProbe == 0 || now >= _nextRttProbe)
|
if (now - _nextRttProbe >= 0)
|
||||||
{
|
{
|
||||||
|
_rttProbePending = true;
|
||||||
_rttProbeTime = now;
|
_rttProbeTime = now;
|
||||||
_rttProbeTimestampHiRes = Stopwatch.GetTimestamp();
|
|
||||||
_nextRttProbe = now + _rttProbeInterval + Utility.Random(RttProbeJitter);
|
_nextRttProbe = now + _rttProbeInterval + Utility.Random(RttProbeJitter);
|
||||||
|
|
||||||
if (_movementLogging)
|
if (_movementLogging)
|
||||||
|
|
@ -242,10 +242,9 @@ public partial class NetState
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RecordRttMeasurement()
|
public void RecordRttMeasurement()
|
||||||
{
|
{
|
||||||
var nowHiRes = Stopwatch.GetTimestamp();
|
|
||||||
var now = Core.TickCount;
|
var now = Core.TickCount;
|
||||||
|
|
||||||
if (_rttProbeTime <= 0)
|
if (!_rttProbePending)
|
||||||
{
|
{
|
||||||
// Not expecting a response (client-initiated version send) - ignore silently
|
// Not expecting a response (client-initiated version send) - ignore silently
|
||||||
return;
|
return;
|
||||||
|
|
@ -253,19 +252,15 @@ public partial class NetState
|
||||||
|
|
||||||
var rtt = now - _rttProbeTime;
|
var rtt = now - _rttProbeTime;
|
||||||
|
|
||||||
// High-resolution RTT in microseconds
|
|
||||||
var rttHiResUs = (nowHiRes - _rttProbeTimestampHiRes) * 1_000_000 / Stopwatch.Frequency;
|
|
||||||
|
|
||||||
if (_movementLogging)
|
if (_movementLogging)
|
||||||
{
|
{
|
||||||
movementLogger.Debug(
|
movementLogger.Debug(
|
||||||
"[RTT-Response] {Account}: {Rtt}ms (HiRes: {RttHiRes:F2}ms)",
|
"[RTT-Response] {Account}: {Rtt}ms",
|
||||||
Account?.Username ?? _toString, rtt, rttHiResUs / 1000.0
|
Account?.Username ?? _toString, rtt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_rttProbeTime = 0;
|
_rttProbePending = false;
|
||||||
_rttProbeTimestampHiRes = 0;
|
|
||||||
|
|
||||||
// Sanity check - RTT should be positive and reasonable
|
// Sanity check - RTT should be positive and reasonable
|
||||||
if (rtt is <= 0 or > 10000)
|
if (rtt is <= 0 or > 10000)
|
||||||
|
|
@ -285,7 +280,6 @@ public partial class NetState
|
||||||
|
|
||||||
// Update history
|
// Update history
|
||||||
_rttHistory[_rttHistoryIndex++ & (RttHistorySize - 1)] = rtt;
|
_rttHistory[_rttHistoryIndex++ & (RttHistorySize - 1)] = rtt;
|
||||||
_lastRtt = rtt;
|
|
||||||
|
|
||||||
// Track sample count (saturates at buffer size)
|
// Track sample count (saturates at buffer size)
|
||||||
if (_rttSampleCount < RttHistorySize)
|
if (_rttSampleCount < RttHistorySize)
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
|
|
||||||
private static readonly Queue<NetState> _connectingQueue = new(2048);
|
private static readonly Queue<NetState> _connectingQueue = new(2048);
|
||||||
private static readonly HashSet<NetState> _instances = new(2048);
|
private static readonly HashSet<NetState> _instances = new(2048);
|
||||||
public static IReadOnlySet<NetState> Instances => _instances;
|
public static HashSet<NetState> Instances => _instances;
|
||||||
|
|
||||||
private readonly string _toString;
|
private readonly string _toString;
|
||||||
private ClientVersion _version;
|
private ClientVersion _version;
|
||||||
|
|
@ -109,9 +109,6 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
Address = address;
|
Address = address;
|
||||||
|
|
||||||
Seeded = false;
|
Seeded = false;
|
||||||
HuePickers = [];
|
|
||||||
Menus = [];
|
|
||||||
Trades = [];
|
|
||||||
NextActivityCheck = Core.TickCount + 30000;
|
NextActivityCheck = Core.TickCount + 30000;
|
||||||
ConnectedOn = Core.Now;
|
ConnectedOn = Core.Now;
|
||||||
_toString = address?.ToString() ?? "(error)";
|
_toString = address?.ToString() ?? "(error)";
|
||||||
|
|
@ -166,7 +163,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
|
|
||||||
public bool BlockAllPackets { get; set; }
|
public bool BlockAllPackets { get; set; }
|
||||||
|
|
||||||
public List<SecureTrade> Trades { get; }
|
public List<SecureTrade> Trades { get; private set; }
|
||||||
|
|
||||||
public bool Seeded { get; set; }
|
public bool Seeded { get; set; }
|
||||||
|
|
||||||
|
|
@ -260,8 +257,18 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
|
|
||||||
public void ValidateAllTrades()
|
public void ValidateAllTrades()
|
||||||
{
|
{
|
||||||
|
if (Trades == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = Trades.Count - 1; i >= 0; --i)
|
for (var i = Trades.Count - 1; i >= 0; --i)
|
||||||
{
|
{
|
||||||
|
if (Trades == null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (i >= Trades.Count)
|
if (i >= Trades.Count)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -280,8 +287,18 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
|
|
||||||
public void CancelAllTrades()
|
public void CancelAllTrades()
|
||||||
{
|
{
|
||||||
|
if (Trades == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = Trades.Count - 1; i >= 0; --i)
|
for (var i = Trades.Count - 1; i >= 0; --i)
|
||||||
{
|
{
|
||||||
|
if (Trades != null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (i < Trades.Count)
|
if (i < Trades.Count)
|
||||||
{
|
{
|
||||||
Trades[i].Cancel();
|
Trades[i].Cancel();
|
||||||
|
|
@ -291,11 +308,21 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
|
|
||||||
public void RemoveTrade(SecureTrade trade)
|
public void RemoveTrade(SecureTrade trade)
|
||||||
{
|
{
|
||||||
Trades.Remove(trade);
|
Trades?.Remove(trade);
|
||||||
|
|
||||||
|
if (Trades?.Count == 0)
|
||||||
|
{
|
||||||
|
Trades = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SecureTrade FindTrade(Mobile m)
|
public SecureTrade FindTrade(Mobile m)
|
||||||
{
|
{
|
||||||
|
if (Trades == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < Trades.Count; ++i)
|
for (var i = 0; i < Trades.Count; ++i)
|
||||||
{
|
{
|
||||||
var trade = Trades[i];
|
var trade = Trades[i];
|
||||||
|
|
@ -311,6 +338,11 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
|
|
||||||
public SecureTradeContainer FindTradeContainer(Mobile m)
|
public SecureTradeContainer FindTradeContainer(Mobile m)
|
||||||
{
|
{
|
||||||
|
if (Trades == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < Trades.Count; ++i)
|
for (var i = 0; i < Trades.Count; ++i)
|
||||||
{
|
{
|
||||||
var trade = Trades[i];
|
var trade = Trades[i];
|
||||||
|
|
@ -336,7 +368,11 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
{
|
{
|
||||||
var newTrade = new SecureTrade(Mobile, state.Mobile);
|
var newTrade = new SecureTrade(Mobile, state.Mobile);
|
||||||
|
|
||||||
|
Trades ??= [];
|
||||||
|
|
||||||
Trades.Add(newTrade);
|
Trades.Add(newTrade);
|
||||||
|
|
||||||
|
state.Trades ??= [];
|
||||||
state.Trades.Add(newTrade);
|
state.Trades.Add(newTrade);
|
||||||
|
|
||||||
return newTrade.From.Container;
|
return newTrade.From.Container;
|
||||||
|
|
@ -1176,8 +1212,16 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
|
||||||
|
|
||||||
var a = Account;
|
var a = Account;
|
||||||
|
|
||||||
Menus.Clear();
|
Menus?.Clear();
|
||||||
HuePickers.Clear();
|
Menus = null;
|
||||||
|
|
||||||
|
HuePickers?.Clear();
|
||||||
|
HuePickers = null;
|
||||||
|
|
||||||
|
// Just in case, but should already be nulled when Mobile.NetState is set to null and CancelAllTrades is called.
|
||||||
|
Trades?.Clear();
|
||||||
|
Trades = null;
|
||||||
|
|
||||||
Account = null;
|
Account = null;
|
||||||
ServerInfo = null;
|
ServerInfo = null;
|
||||||
CityInfo = null;
|
CityInfo = null;
|
||||||
|
|
|
||||||
|
|
@ -86,11 +86,17 @@ public static class IncomingPlayerPackets
|
||||||
public static void HuePickerResponse(NetState state, SpanReader reader)
|
public static void HuePickerResponse(NetState state, SpanReader reader)
|
||||||
{
|
{
|
||||||
var serial = reader.ReadUInt32();
|
var serial = reader.ReadUInt32();
|
||||||
_ = reader.ReadInt16(); // Item ID
|
reader.ReadInt16(); // Item ID
|
||||||
var hue = Utility.ClipDyedHue(reader.ReadInt16() & 0x3FFF);
|
var hue = Utility.ClipDyedHue(reader.ReadInt16() & 0x3FFF);
|
||||||
|
|
||||||
foreach (var huePicker in state.HuePickers)
|
if (state.HuePickers == null)
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < state.HuePickers.Count; i++)
|
||||||
|
{
|
||||||
|
var huePicker = state.HuePickers[i];
|
||||||
if (huePicker.Serial == serial)
|
if (huePicker.Serial == serial)
|
||||||
{
|
{
|
||||||
state.RemoveHuePicker(huePicker);
|
state.RemoveHuePicker(huePicker);
|
||||||
|
|
@ -287,13 +293,18 @@ public static class IncomingPlayerPackets
|
||||||
public static void MenuResponse(NetState state, SpanReader reader)
|
public static void MenuResponse(NetState state, SpanReader reader)
|
||||||
{
|
{
|
||||||
var serial = reader.ReadUInt32();
|
var serial = reader.ReadUInt32();
|
||||||
int menuID = reader.ReadInt16();
|
reader.ReadInt16(); // menu id
|
||||||
int index = reader.ReadInt16();
|
int index = reader.ReadInt16();
|
||||||
int itemID = reader.ReadInt16();
|
reader.ReadInt16(); // item id
|
||||||
int hue = reader.ReadInt16();
|
reader.ReadInt16(); // hue
|
||||||
|
|
||||||
index -= 1; // convert from 1-based to 0-based
|
index -= 1; // convert from 1-based to 0-based
|
||||||
|
|
||||||
|
if (state.Menus == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < state.Menus.Count; i++)
|
for (var i = 0; i < state.Menus.Count; i++)
|
||||||
{
|
{
|
||||||
var menu = state.Menus[i];
|
var menu = state.Menus[i];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue