From a96fe1e47fd42961638fce1ef51eedbf4fb9c6cc Mon Sep 17 00:00:00 2001 From: hondacrx Date: Thu, 2 Dec 2021 22:34:04 -0500 Subject: [PATCH] Core/Movement: Add time synchronisation (Needs tested) Port From (https://github.com/TrinityCore/TrinityCore/commit/c19a4db1c12b8864d6c486ee8e2f0e058fb4155a) --- .../Framework/Collections/CircularBuffer.cs | 424 ++++++++++++++++++ Source/Framework/Util/MathFunctions.cs | 17 + Source/Framework/Util/Time.cs | 6 + Source/Game/Entities/Player/Player.Fields.cs | 4 - Source/Game/Entities/Player/Player.cs | 36 +- Source/Game/Handlers/MovementHandler.cs | 88 +++- Source/Game/Handlers/TimeHandler.cs | 19 - Source/Game/Networking/Packet.cs | 4 + Source/Game/Networking/Packets/MiscPackets.cs | 2 + Source/Game/Networking/WorldSocket.cs | 6 +- Source/Game/Server/WorldSession.cs | 55 ++- 11 files changed, 586 insertions(+), 75 deletions(-) create mode 100644 Source/Framework/Collections/CircularBuffer.cs diff --git a/Source/Framework/Collections/CircularBuffer.cs b/Source/Framework/Collections/CircularBuffer.cs new file mode 100644 index 000000000..91d1ead41 --- /dev/null +++ b/Source/Framework/Collections/CircularBuffer.cs @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2012-2020 CypherCore + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Framework.Collections +{ + /// + /// + /// Circular buffer. + /// + /// When writing to a full buffer: + /// PushBack -> removes this[0] / Front() + /// PushFront -> removes this[Size-1] / Back() + /// + /// this implementation is inspired by + /// http://www.boost.org/doc/libs/1_53_0/libs/circular_buffer/doc/circular_buffer.html + /// because I liked their interface. + /// + public class CircularBuffer : IEnumerable + { + private readonly T[] _buffer; + + /// + /// The _start. Index of the first element in buffer. + /// + private int _start; + + /// + /// The _end. Index after the last element in the buffer. + /// + private int _end; + + /// + /// The _size. Buffer size. + /// + private int _size; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// Buffer capacity. Must be positive. + /// + public CircularBuffer(int capacity) + : this(capacity, new T[] { }) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// Buffer capacity. Must be positive. + /// + /// + /// Items to fill buffer with. Items length must be less than capacity. + /// Suggestion: use Skip(x).Take(y).ToArray() to build this argument from + /// any enumerable. + /// + public CircularBuffer(int capacity, T[] items) + { + if (capacity < 1) + { + throw new ArgumentException( + "Circular buffer cannot have negative or zero capacity.", nameof(capacity)); + } + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + if (items.Length > capacity) + { + throw new ArgumentException( + "Too many items to fit circular buffer", nameof(items)); + } + + _buffer = new T[capacity]; + + Array.Copy(items, _buffer, items.Length); + _size = items.Length; + + _start = 0; + _end = _size == capacity ? 0 : _size; + } + + /// + /// Maximum capacity of the buffer. Elements pushed into the buffer after + /// maximum capacity is reached (IsFull = true), will remove an element. + /// + public int Capacity { get { return _buffer.Length; } } + + /// + /// Boolean indicating if Circular is at full capacity. + /// Adding more elements when the buffer is full will + /// cause elements to be removed from the other end + /// of the buffer. + /// + public bool IsFull + { + get + { + return Size == Capacity; + } + } + + /// + /// True if has no elements. + /// + public bool IsEmpty + { + get + { + return Size == 0; + } + } + + /// + /// Current buffer size (the number of elements that the buffer has). + /// + public int Size { get { return _size; } } + + /// + /// Element at the front of the buffer - this[0]. + /// + /// The value of the element of type T at the front of the buffer. + public T Front() + { + ThrowIfEmpty(); + return _buffer[_start]; + } + + /// + /// Element at the back of the buffer - this[Size - 1]. + /// + /// The value of the element of type T at the back of the buffer. + public T Back() + { + ThrowIfEmpty(); + return _buffer[(_end != 0 ? _end : Capacity) - 1]; + } + + /// + /// Index access to elements in buffer. + /// Index does not loop around like when adding elements, + /// valid interval is [0;Size[ + /// + /// Index of element to access. + /// Thrown when index is outside of [; Size[ interval. + public T this[int index] + { + get + { + if (IsEmpty) + { + throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index)); + } + if (index >= _size) + { + throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size)); + } + int actualIndex = InternalIndex(index); + return _buffer[actualIndex]; + } + set + { + if (IsEmpty) + { + throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index)); + } + if (index >= _size) + { + throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size)); + } + int actualIndex = InternalIndex(index); + _buffer[actualIndex] = value; + } + } + + /// + /// Pushes a new element to the back of the buffer. Back()/this[Size-1] + /// will now return this element. + /// + /// When the buffer is full, the element at Front()/this[0] will be + /// popped to allow for this new element to fit. + /// + /// Item to push to the back of the buffer + public void PushBack(T item) + { + if (IsFull) + { + _buffer[_end] = item; + Increment(ref _end); + _start = _end; + } + else + { + _buffer[_end] = item; + Increment(ref _end); + ++_size; + } + } + + /// + /// Pushes a new element to the front of the buffer. Front()/this[0] + /// will now return this element. + /// + /// When the buffer is full, the element at Back()/this[Size-1] will be + /// popped to allow for this new element to fit. + /// + /// Item to push to the front of the buffer + public void PushFront(T item) + { + if (IsFull) + { + Decrement(ref _start); + _end = _start; + _buffer[_start] = item; + } + else + { + Decrement(ref _start); + _buffer[_start] = item; + ++_size; + } + } + + /// + /// Removes the element at the back of the buffer. Decreasing the + /// Buffer size by 1. + /// + public void PopBack() + { + ThrowIfEmpty("Cannot take elements from an empty buffer."); + Decrement(ref _end); + _buffer[_end] = default(T); + --_size; + } + + /// + /// Removes the element at the front of the buffer. Decreasing the + /// Buffer size by 1. + /// + public void PopFront() + { + ThrowIfEmpty("Cannot take elements from an empty buffer."); + _buffer[_start] = default(T); + Increment(ref _start); + --_size; + } + + /// + /// Copies the buffer contents to an array, according to the logical + /// contents of the buffer (i.e. independent of the internal + /// order/contents) + /// + /// A new array with a copy of the buffer contents. + public T[] ToArray() + { + T[] newArray = new T[Size]; + int newArrayOffset = 0; + var segments = ToArraySegments(); + foreach (ArraySegment segment in segments) + { + Array.Copy(segment.Array, segment.Offset, newArray, newArrayOffset, segment.Count); + newArrayOffset += segment.Count; + } + return newArray; + } + + /// + /// Get the contents of the buffer as 2 ArraySegments. + /// Respects the logical contents of the buffer, where + /// each segment and items in each segment are ordered + /// according to insertion. + /// + /// Fast: does not copy the array elements. + /// Useful for methods like Send(IList<ArraySegment<Byte>>). + /// + /// Segments may be empty. + /// + /// An IList with 2 segments corresponding to the buffer content. + public IList> ToArraySegments() + { + return new[] { ArrayOne(), ArrayTwo() }; + } + + #region IEnumerable implementation + /// + /// Returns an enumerator that iterates through this buffer. + /// + /// An enumerator that can be used to iterate this collection. + public IEnumerator GetEnumerator() + { + var segments = ToArraySegments(); + foreach (ArraySegment segment in segments) + { + for (int i = 0; i < segment.Count; i++) + { + yield return segment.Array[segment.Offset + i]; + } + } + } + #endregion + #region IEnumerable implementation + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + #endregion + + private void ThrowIfEmpty(string message = "Cannot access an empty buffer.") + { + if (IsEmpty) + { + throw new InvalidOperationException(message); + } + } + + /// + /// Increments the provided index variable by one, wrapping + /// around if necessary. + /// + /// + private void Increment(ref int index) + { + if (++index == Capacity) + { + index = 0; + } + } + + /// + /// Decrements the provided index variable by one, wrapping + /// around if necessary. + /// + /// + private void Decrement(ref int index) + { + if (index == 0) + { + index = Capacity; + } + index--; + } + + /// + /// Converts the index in the argument to an index in _buffer + /// + /// + /// The transformed index. + /// + /// + /// External index. + /// + private int InternalIndex(int index) + { + return _start + (index < (Capacity - _start) ? index : index - Capacity); + } + + // doing ArrayOne and ArrayTwo methods returning ArraySegment as seen here: + // http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1957cccdcb0c4ef7d80a34a990065818d + // http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1f5081a54afbc2dfc1a7fb20329df7d5b + // should help a lot with the code. + + #region Array items easy access. + // The array is composed by at most two non-contiguous segments, + // the next two methods allow easy access to those. + + private ArraySegment ArrayOne() + { + if (IsEmpty) + { + return new ArraySegment(new T[0]); + } + else if (_start < _end) + { + return new ArraySegment(_buffer, _start, _end - _start); + } + else + { + return new ArraySegment(_buffer, _start, _buffer.Length - _start); + } + } + + private ArraySegment ArrayTwo() + { + if (IsEmpty) + { + return new ArraySegment(new T[0]); + } + else if (_start < _end) + { + return new ArraySegment(_buffer, _end, 0); + } + else + { + return new ArraySegment(_buffer, 0, _end); + } + } + #endregion + } +} diff --git a/Source/Framework/Util/MathFunctions.cs b/Source/Framework/Util/MathFunctions.cs index 6215dd460..5a8a0cea2 100644 --- a/Source/Framework/Util/MathFunctions.cs +++ b/Source/Framework/Util/MathFunctions.cs @@ -18,6 +18,7 @@ using Framework.Constants; using Framework.GameMath; using System; +using System.Collections.Generic; using System.Numerics; public static class MathFunctions @@ -269,6 +270,22 @@ public static class MathFunctions return (ushort)((byte)l | (ushort)h << 8); } + public static double Variance(this IEnumerable source) + { + int n = 0; + double mean = 0; + double M2 = 0; + + foreach (var x in source) + { + n = n + 1; + double delta = x - mean; + mean = mean + delta / n; + M2 += delta * (x - mean); + } + return M2 / (n - 1); + } + //3d math public static Box toWorldSpace(Matrix4x4 rotation, Vector3 translation, Box box) { diff --git a/Source/Framework/Util/Time.cs b/Source/Framework/Util/Time.cs index c15044014..3be268b1d 100644 --- a/Source/Framework/Util/Time.cs +++ b/Source/Framework/Util/Time.cs @@ -82,6 +82,12 @@ public static class Time return newMSTime - oldMSTime; } + public static uint GetMSTimeDiff(uint oldMSTime, DateTime newTime) + { + uint newMSTime = (uint)(newTime - ApplicationStartTime).TotalMilliseconds; + return GetMSTimeDiff(oldMSTime, newMSTime); + } + public static uint GetMSTimeDiffToNow(uint oldMSTime) { var newMSTime = GetMSTime(); diff --git a/Source/Game/Entities/Player/Player.Fields.cs b/Source/Game/Entities/Player/Player.Fields.cs index 4b5b816ef..4bfc61d01 100644 --- a/Source/Game/Entities/Player/Player.Fields.cs +++ b/Source/Game/Entities/Player/Player.Fields.cs @@ -212,10 +212,6 @@ namespace Game.Entities // only changed for direct client control (possess, vehicle etc.), not stuff you control using pet commands public Unit m_unitMovedByMe; Team m_team; - public Stack m_timeSyncQueue = new(); - uint m_timeSyncTimer; - public uint m_timeSyncClient; - public uint m_timeSyncServer; ReputationMgr reputationMgr; QuestObjectiveCriteriaManager m_questObjectiveCriteriaMgr; public AtLoginFlags atLoginFlags; diff --git a/Source/Game/Entities/Player/Player.cs b/Source/Game/Entities/Player/Player.cs index 8d231a54b..11be02d5c 100644 --- a/Source/Game/Entities/Player/Player.cs +++ b/Source/Game/Entities/Player/Player.cs @@ -74,8 +74,6 @@ namespace Game.Entities m_logintime = GameTime.GetGameTime(); m_Last_tick = m_logintime; - m_timeSyncServer = GameTime.GetGameTimeMS(); - m_dungeonDifficulty = Difficulty.Normal; m_raidDifficulty = Difficulty.NormalRaid; m_legacyRaidDifficulty = Difficulty.Raid10N; @@ -533,13 +531,6 @@ namespace Game.Entities else m_zoneUpdateTimer -= diff; } - if (m_timeSyncTimer > 0 && !IsBeingTeleportedFar()) - { - if (diff >= m_timeSyncTimer) - SendTimeSync(); - else - m_timeSyncTimer -= diff; - } if (IsAlive()) { @@ -848,29 +839,6 @@ namespace Game.Entities Session.SendPacket(data); } - //Time - void ResetTimeSync() - { - m_timeSyncTimer = 0; - m_timeSyncClient = 0; - m_timeSyncServer = GameTime.GetGameTimeMS(); - } - void SendTimeSync() - { - m_timeSyncQueue.Push(m_movementCounter++); - - TimeSyncRequest packet = new(); - packet.SequenceIndex = m_timeSyncQueue.Last(); - SendPacket(packet); - - // Schedule next sync in 10 sec - m_timeSyncTimer = 10000; - m_timeSyncServer = GameTime.GetGameTimeMS(); - - if (m_timeSyncQueue.Count > 3) - Log.outError(LogFilter.Network, "Not received CMSG_TIME_SYNC_RESP for over 30 seconds from player {0} ({1}), possible cheater", GetGUID().ToString(), GetName()); - } - public DeclinedName GetDeclinedNames() { return _declinedname; } public void CreateGarrison(uint garrSiteId) @@ -5460,10 +5428,10 @@ namespace Game.Entities if (!m_teleport_options.HasAnyFlag(TeleportToOptions.Seamless)) { m_movementCounter = 0; - ResetTimeSync(); + GetSession().ResetTimeSync(); } - SendTimeSync(); + GetSession().SendTimeSync(); GetSocial().SendSocialList(this, SocialFlag.All); diff --git a/Source/Game/Handlers/MovementHandler.cs b/Source/Game/Handlers/MovementHandler.cs index d9d16d7b4..5d47b91ce 100644 --- a/Source/Game/Handlers/MovementHandler.cs +++ b/Source/Game/Handlers/MovementHandler.cs @@ -26,6 +26,7 @@ using Game.Networking; using Game.Networking.Packets; using System; using System.Collections.Generic; +using System.Linq; namespace Game { @@ -140,14 +141,8 @@ namespace Game if (opcode == ClientOpcodes.MoveFallLand || opcode == ClientOpcodes.MoveStartSwim || opcode == ClientOpcodes.MoveSetFly) mover.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.LandingOrFlight); // Parachutes - uint mstime = GameTime.GetGameTimeMS(); - - if (m_clientTimeDelay == 0) - m_clientTimeDelay = mstime - movementInfo.Time; - - movementInfo.Time = movementInfo.Time + m_clientTimeDelay; - movementInfo.Guid = mover.GetGUID(); + movementInfo.Time = AdjustClientMovementTime(movementInfo.Time); mover.m_movementInfo = movementInfo; // Some vehicles allow the passenger to turn by himself @@ -205,7 +200,7 @@ namespace Game plrMover.RemovePlayerFlag(PlayerFlags.IsOutOfBounds); if (opcode == ClientOpcodes.MoveJump) - { + { plrMover.RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags2.Jump); // Mind Control Unit.ProcSkillsAndAuras(plrMover, null, ProcFlags.Jump, ProcFlags.None, ProcFlagsSpellType.MaskAll, ProcFlagsSpellPhase.None, ProcFlagsHit.None, null, null, null); } @@ -347,7 +342,7 @@ namespace Game // resurrect character at enter into instance where his corpse exist after add to map if (mapEntry.IsDungeon() && !player.IsAlive()) - { + { if (player.GetCorpseLocation().GetMapId() == mapEntry.Id) { player.ResurrectPlayer(0.5f, false); @@ -619,7 +614,7 @@ namespace Game return; } - moveApplyMovementForceAck.Ack.Status.Time += m_clientTimeDelay; + moveApplyMovementForceAck.Ack.Status.Time = AdjustClientMovementTime(moveApplyMovementForceAck.Ack.Status.Time); MoveUpdateApplyMovementForce updateApplyMovementForce = new(); updateApplyMovementForce.Status = moveApplyMovementForceAck.Ack.Status; @@ -641,7 +636,7 @@ namespace Game return; } - moveRemoveMovementForceAck.Ack.Status.Time += m_clientTimeDelay; + moveRemoveMovementForceAck.Ack.Status.Time = AdjustClientMovementTime(moveRemoveMovementForceAck.Ack.Status.Time); MoveUpdateRemoveMovementForce updateRemoveMovementForce = new(); updateRemoveMovementForce.Status = moveRemoveMovementForceAck.Ack.Status; @@ -683,7 +678,7 @@ namespace Game } } - setModMovementForceMagnitudeAck.Ack.Status.Time += m_clientTimeDelay; + setModMovementForceMagnitudeAck.Ack.Status.Time = AdjustClientMovementTime(setModMovementForceMagnitudeAck.Ack.Status.Time); MoveUpdateSpeed updateModMovementForceMagnitude = new(ServerOpcodes.MoveUpdateModMovementForceMagnitude); updateModMovementForceMagnitude.Status = setModMovementForceMagnitudeAck.Ack.Status; @@ -759,5 +754,74 @@ namespace Game if (GetPlayer().pvpInfo.IsHostile) GetPlayer().CastSpell(GetPlayer(), 2479, true); } + + [WorldPacketHandler(ClientOpcodes.TimeSyncResponse, Processing = PacketProcessing.ThreadSafe)] + void HandleTimeSyncResponse(TimeSyncResponse timeSyncResponse) + { + if (!_pendingTimeSyncRequests.ContainsKey(timeSyncResponse.SequenceIndex)) + return; + + uint serverTimeAtSent = _pendingTimeSyncRequests.LookupByKey(timeSyncResponse.SequenceIndex); + _pendingTimeSyncRequests.Remove(timeSyncResponse.SequenceIndex); + + // time it took for the request to travel to the client, for the client to process it and reply and for response to travel back to the server. + // we are going to make 2 assumptions: + // 1) we assume that the request processing time equals 0. + // 2) we assume that the packet took as much time to travel from server to client than it took to travel from client to server. + uint roundTripDuration = Time.GetMSTimeDiff(serverTimeAtSent, timeSyncResponse.GetReceivedTime()); + uint lagDelay = roundTripDuration / 2; + + /* + clockDelta = serverTime - clientTime + where + serverTime: time that was displayed on the clock of the SERVER at the moment when the client processed the SMSG_TIME_SYNC_REQUEST packet. + clientTime: time that was displayed on the clock of the CLIENT at the moment when the client processed the SMSG_TIME_SYNC_REQUEST packet. + + Once clockDelta has been computed, we can compute the time of an event on server clock when we know the time of that same event on the client clock, + using the following relation: + serverTime = clockDelta + clientTime + */ + long clockDelta = (long)(serverTimeAtSent + lagDelay) - (long)timeSyncResponse.ClientTime; + _timeSyncClockDeltaQueue.PushFront(Tuple.Create(clockDelta, roundTripDuration)); + ComputeNewClockDelta(); + } + + void ComputeNewClockDelta() + { + // implementation of the technique described here: https://web.archive.org/web/20180430214420/http://www.mine-control.com/zack/timesync/timesync.html + // to reduce the skew induced by dropped TCP packets that get resent. + + //accumulator_set < uint32, features < tag::mean, tag::median, tag::variance(lazy) > > latencyAccumulator; + List latencyList = new(); + foreach (var pair in _timeSyncClockDeltaQueue) + latencyList.Add(pair.Item2); + + uint latencyMedian = (uint)Math.Round(latencyList.Average(p => p));//median(latencyAccumulator)); + uint latencyStandardDeviation = (uint)Math.Round(Math.Sqrt(latencyList.Variance()));//variance(latencyAccumulator))); + + //accumulator_set> clockDeltasAfterFiltering; + List clockDeltasAfterFiltering = new(); + uint sampleSizeAfterFiltering = 0; + foreach (var pair in _timeSyncClockDeltaQueue) + { + if (pair.Item2 < latencyStandardDeviation + latencyMedian) + { + clockDeltasAfterFiltering.Add(pair.Item1); + sampleSizeAfterFiltering++; + } + } + + if (sampleSizeAfterFiltering != 0) + { + long meanClockDelta = (long)(Math.Round(clockDeltasAfterFiltering.Average())); + if (Math.Abs(meanClockDelta - _timeSyncClockDelta) > 25) + _timeSyncClockDelta = meanClockDelta; + } + else if (_timeSyncClockDelta == 0) + { + var back = _timeSyncClockDeltaQueue.Back(); + _timeSyncClockDelta = back.Item1; + } + } } } diff --git a/Source/Game/Handlers/TimeHandler.cs b/Source/Game/Handlers/TimeHandler.cs index 41327fc76..e5d4ae9da 100644 --- a/Source/Game/Handlers/TimeHandler.cs +++ b/Source/Game/Handlers/TimeHandler.cs @@ -31,24 +31,5 @@ namespace Game response.Time = GameTime.GetGameTime(); SendPacket(response); } - - [WorldPacketHandler(ClientOpcodes.TimeSyncResponse, Processing = PacketProcessing.Inplace)] - void HandleTimeSyncResponse(TimeSyncResponse packet) - { - Log.outDebug(LogFilter.Network, "CMSG_TIME_SYNC_RESP"); - - if (packet.SequenceIndex != _player.m_timeSyncQueue.FirstOrDefault()) - Log.outError(LogFilter.Network, "Wrong time sync counter from player {0} (cheater?)", _player.GetName()); - - Log.outDebug(LogFilter.Network, "Time sync received: counter {0}, client ticks {1}, time since last sync {2}", packet.SequenceIndex, packet.ClientTime, packet.ClientTime - _player.m_timeSyncClient); - - uint ourTicks = packet.ClientTime + (GameTime.GetGameTimeMS() - _player.m_timeSyncServer); - - // diff should be small - Log.outDebug(LogFilter.Network, "Our ticks: {0}, diff {1}, latency {2}", ourTicks, ourTicks - packet.ClientTime, GetLatency()); - - _player.m_timeSyncClient = packet.ClientTime; - _player.m_timeSyncQueue.Pop(); - } } } diff --git a/Source/Game/Networking/Packet.cs b/Source/Game/Networking/Packet.cs index 3ecbbb99f..c777b04bc 100644 --- a/Source/Game/Networking/Packet.cs +++ b/Source/Game/Networking/Packet.cs @@ -220,7 +220,11 @@ namespace Game.Networking public uint GetOpcode() { return opcode; } + public DateTime GetReceivedTime() { return m_receivedTime; } + public void SetReceiveTime(DateTime receivedTime) { m_receivedTime = receivedTime; } + uint opcode; + DateTime m_receivedTime; // only set for a specific set of opcodes, for performance reasons. } public class PacketHeader diff --git a/Source/Game/Networking/Packets/MiscPackets.cs b/Source/Game/Networking/Packets/MiscPackets.cs index 6e028ea94..3aa451263 100644 --- a/Source/Game/Networking/Packets/MiscPackets.cs +++ b/Source/Game/Networking/Packets/MiscPackets.cs @@ -275,6 +275,8 @@ namespace Game.Networking.Packets ClientTime = _worldPacket.ReadUInt32(); } + public DateTime GetReceivedTime() { return _worldPacket.GetReceivedTime(); } + public uint ClientTime; // Client ticks in ms public uint SequenceIndex; // Same index as in request } diff --git a/Source/Game/Networking/WorldSocket.cs b/Source/Game/Networking/WorldSocket.cs index b60d1e64a..bf98db7ee 100644 --- a/Source/Game/Networking/WorldSocket.cs +++ b/Source/Game/Networking/WorldSocket.cs @@ -340,6 +340,9 @@ namespace Game.Networking break; } + if (opcode == ClientOpcodes.TimeSyncResponse) + packet.SetReceiveTime(DateTime.Now); + // Our Idle timer will reset on any non PING opcodes on login screen, allowing us to catch people idling. _worldSession.ResetTimeOutTime(false); @@ -812,10 +815,7 @@ namespace Game.Networking lock (_worldSessionLock) { if (_worldSession != null) - { _worldSession.SetLatency(ping.Latency); - _worldSession.ResetClientTimeDelay(); - } else { Log.outError(LogFilter.Network, "WorldSocket:HandlePing: peer sent CMSG_PING, but is not authenticated or got recently kicked, address = {0}", GetRemoteIpAddress()); diff --git a/Source/Game/Server/WorldSession.cs b/Source/Game/Server/WorldSession.cs index 91d06eadc..76eaacf14 100644 --- a/Source/Game/Server/WorldSession.cs +++ b/Source/Game/Server/WorldSession.cs @@ -15,8 +15,10 @@ * along with this program. If not, see . */ +using Framework.Collections; using Framework.Constants; using Framework.Database; +using Framework.Realm; using Game.Accounts; using Game.BattleGrounds; using Game.BattlePets; @@ -32,7 +34,6 @@ using System.IO; using System.Numerics; using System.Text; using System.Threading.Tasks; -using Framework.Realm; namespace Game { @@ -331,6 +332,18 @@ namespace Game if (m_Socket[(int)ConnectionType.Realm] != null && m_Socket[(int)ConnectionType.Realm].IsOpen() && _warden != null) _warden.Update(); + if (!updater.ProcessUnsafe()) // <=> updater is of type MapSessionFilter + { + // Send time sync packet every 10s. + if (_timeSyncTimer > 0) + { + if (diff >= _timeSyncTimer) + SendTimeSync(); + else + _timeSyncTimer -= diff; + } + } + ProcessQueryCallbacks(); if (updater.ProcessUnsafe()) @@ -828,12 +841,42 @@ namespace Game } } + public void ResetTimeSync() + { + _timeSyncNextCounter = 0; + _pendingTimeSyncRequests.Clear(); + } + + public void SendTimeSync() + { + TimeSyncRequest timeSyncRequest = new(); + timeSyncRequest.SequenceIndex = _timeSyncNextCounter; + SendPacket(timeSyncRequest); + + _pendingTimeSyncRequests[_timeSyncNextCounter] = Time.GetMSTime(); + + // Schedule next sync in 10 sec (except for the 2 first packets, which are spaced by only 5s) + _timeSyncTimer = _timeSyncNextCounter == 0 ? 5000 : 10000u; + _timeSyncNextCounter++; + } + + uint AdjustClientMovementTime(uint time) + { + long movementTime = (long)time + _timeSyncClockDelta; + if (_timeSyncClockDelta == 0 || movementTime < 0 || movementTime > 0xFFFFFFFF) + { + Log.outWarn(LogFilter.Misc, "The computed movement time using clockDelta is erronous. Using fallback instead"); + return GameTime.GetGameTimeMS(); + } + else + return (uint)movementTime; + } + public Locale GetSessionDbcLocale() { return m_sessionDbcLocale; } public Locale GetSessionDbLocaleIndex() { return m_sessionDbLocaleIndex; } public uint GetLatency() { return m_latency; } public void SetLatency(uint latency) { m_latency = latency; } - public void ResetClientTimeDelay() { m_clientTimeDelay = 0; } public void ResetTimeOutTime(bool onlyActive) { if (GetPlayer()) @@ -893,7 +936,6 @@ namespace Game Locale m_sessionDbcLocale; Locale m_sessionDbLocaleIndex; uint m_latency; - uint m_clientTimeDelay; AccountData[] _accountData = new AccountData[(int)AccountDataTypes.Max]; uint[] tutorials = new uint[SharedConst.MaxAccountTutorialValues]; TutorialsFlag tutorialsChanged; @@ -916,6 +958,13 @@ namespace Game ObjectGuid m_currentBankerGUID; + CircularBuffer> _timeSyncClockDeltaQueue = new(6); // first member: clockDelta. Second member: latency of the packet exchange that was used to compute that clockDelta. + long _timeSyncClockDelta; + + Dictionary _pendingTimeSyncRequests = new(); // key: counter. value: server time when packet with that counter was sent. + uint _timeSyncNextCounter; + uint _timeSyncTimer; + CollectionMgr _collectionMgr; ConnectToKey _instanceConnectKey;