feat(Core/Movement): time synchronisation to better interpret client timestamps (#5300)

This commit is contained in:
Chaouki Dhib
2021-04-23 15:53:09 +02:00
committed by GitHub
parent 970d371442
commit 2d21bfc915
11 changed files with 367 additions and 96 deletions

View File

@@ -9,6 +9,7 @@
#include "Common.h"
#include "ByteBuffer.h"
#include "Duration.h"
class WorldPacket : public ByteBuffer
{
@@ -18,6 +19,11 @@ public:
{
}
explicit WorldPacket(uint16 opcode, size_t res = 200) : ByteBuffer(res), m_opcode(opcode) { }
WorldPacket(WorldPacket&& packet, TimePoint receivedTime) : ByteBuffer(std::move(packet)), m_opcode(packet.m_opcode), m_receivedTime(receivedTime)
{
}
// copy constructor
WorldPacket(const WorldPacket& packet) : ByteBuffer(packet), m_opcode(packet.m_opcode)
{
@@ -39,7 +45,10 @@ public:
[[nodiscard]] uint16 GetOpcode() const { return m_opcode; }
void SetOpcode(uint16 opcode) { m_opcode = opcode; }
[[nodiscard]] TimePoint GetReceivedTime() const { return m_receivedTime; }
protected:
uint16 m_opcode{0};
TimePoint m_receivedTime; // only set for a specific set of opcodes, for performance reasons.
};
#endif

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
*
* This file was based on
* https://embeddedartistry.com/blog/2017/05/17/creating-a-circular-buffer-in-c-and-c/
* https://github.com/embeddedartistry/embedded-resources/blob/master/examples/cpp/circular_buffer.cpp
*/
#ifndef AZEROTHCORE_CIRCULAR_BUFFER_H
#define AZEROTHCORE_CIRCULAR_BUFFER_H
#include <memory>
#include <mutex>
#include <vector>
template <typename T>
class CircularBuffer {
public:
explicit CircularBuffer(size_t size) :
buf_(std::unique_ptr<T[]>(new T[size])),
max_size_(size)
{
}
void put(T item)
{
std::lock_guard<std::mutex> lock(mutex_);
buf_[head_] = item;
if (full_)
{
tail_ = (tail_ + 1) % max_size_;
}
head_ = (head_ + 1) % max_size_;
full_ = head_ == tail_;
}
bool empty() const
{
//if head and tail are equal, we are empty
return (!full_ && (head_ == tail_));
}
bool full() const
{
//If tail is ahead the head by 1, we are full
return full_;
}
size_t capacity() const
{
return max_size_;
}
size_t size() const
{
size_t size = max_size_;
if (!full_)
{
if (head_ >= tail_)
{
size = head_ - tail_;
}
else
{
size += head_ - tail_;
}
}
return size;
}
// the implementation of this function is simplified by the fact that head_ will never be lower than tail_
// when compared to the original implementation of this class
std::vector<T> content() {
std::lock_guard<std::mutex> lock(mutex_);
return std::vector<T>(buf_.get(), buf_.get() + size());
}
T peak_back() {
std::lock_guard<std::mutex> lock(mutex_);
return empty() ? T() : buf_[tail_];
}
private:
std::mutex mutex_;
std::unique_ptr<T[]> buf_;
size_t head_ = 0;
size_t tail_ = 0;
const size_t max_size_;
bool full_ = 0;
};
#endif

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
*/
#ifndef _MATH_UTIL_H
#define _MATH_UTIL_H
#include <vector>
#include <iterator>
#include <algorithm>
#include <numeric>
// based on https://stackoverflow.com/questions/7616511/calculate-mean-and-standard-deviation-from-a-vector-of-samples-in-c-using-boos/12405793#comment32490316_12405793
template <typename Container, typename T = typename std::decay<decltype(*std::begin(std::declval<Container>()))>::type>
inline T standard_deviation(Container&& c)
{
auto b = std::begin(c), e = std::end(c);
auto size = std::distance(b, e);
auto sum = std::accumulate(b, e, T());
auto mean = sum / size;
if (size == 1)
return (T) 0;
T accum = T();
for (const auto d : c)
accum += (d - mean) * (d - mean);
return std::sqrt(accum / (size - 1));
}
template <typename Container, typename T = typename std::decay<decltype(*std::begin(std::declval<Container>()))>::type>
inline T mean(Container&& c)
{
auto b = std::begin(c), e = std::end(c);
auto size = std::distance(b, e);
auto sum = std::accumulate(b, e, T());
return sum / size;
}
// based off https://www.geeksforgeeks.org/finding-median-of-unsorted-array-in-linear-time-using-c-stl/
template <typename T>
inline T median(std::vector<T> a)
{
size_t n = a.size();
// If size of the arr[] is even
if (n % 2 == 0) {
// Applying nth_element
// on n/2th index
std::nth_element(a.begin(),
a.begin() + n / 2,
a.end());
// Applying nth_element
// on (n-1)/2 th index
std::nth_element(a.begin(),
a.begin() + (n - 1) / 2,
a.end());
// Find the average of value at
// index N/2 and (N-1)/2
return (T)(a[(n - 1) / 2]
+ a[n / 2])
/ 2.0;
}
// If size of the arr[] is odd
else {
// Applying nth_element
// on n/2
std::nth_element(a.begin(),
a.begin() + n / 2,
a.end());
// Value at index (N/2)th
// is the median
return (T)a[n / 2];
}
}
#endif

View File

@@ -929,11 +929,6 @@ Player::Player(WorldSession* session): Unit(true), m_mover(this)
m_ChampioningFaction = 0;
m_timeSyncCounter = 0;
m_timeSyncTimer = 0;
m_timeSyncClient = 0;
m_timeSyncServer = 0;
for (uint8 i = 0; i < MAX_POWERS; ++i)
m_powerFraction[i] = 0;
@@ -1833,14 +1828,6 @@ void Player::Update(uint32 p_time)
m_zoneUpdateTimer -= p_time;
}
if (m_timeSyncTimer > 0)
{
if (p_time >= m_timeSyncTimer)
SendTimeSync();
else
m_timeSyncTimer -= p_time;
}
if (IsAlive())
{
m_regenTimer += p_time;
@@ -23804,8 +23791,8 @@ void Player::SendInitialPacketsAfterAddToMap()
{
UpdateVisibilityForPlayer(true);
ResetTimeSync();
SendTimeSync();
GetSession()->ResetTimeSync();
GetSession()->SendTimeSync();
CastSpell(this, 836, true); // LOGINEFFECT
@@ -27558,25 +27545,6 @@ uint8 Player::GetMostPointsTalentTree() const
return maxIndex;
}
void Player::ResetTimeSync()
{
m_timeSyncCounter = 0;
m_timeSyncTimer = 0;
m_timeSyncClient = 0;
m_timeSyncServer = World::GetGameTimeMS();
}
void Player::SendTimeSync()
{
WorldPacket data(SMSG_TIME_SYNC_REQ, 4);
data << uint32(m_timeSyncCounter++);
GetSession()->SendPacket(&data);
// Schedule next sync in 10 sec
m_timeSyncTimer = 10000;
m_timeSyncServer = World::GetGameTimeMS();
}
void Player::SetReputation(uint32 factionentry, uint32 value)
{
GetReputationMgr().SetReputation(sFactionStore.LookupEntry(factionentry), value);

View File

@@ -2823,9 +2823,6 @@ protected:
ItemDurationList m_itemSoulboundTradeable;
std::mutex m_soulboundTradableLock;
void ResetTimeSync();
void SendTimeSync();
uint64 m_resurrectGUID;
uint32 m_resurrectMap;
float m_resurrectX, m_resurrectY, m_resurrectZ;
@@ -2966,11 +2963,6 @@ private:
uint32 m_ChampioningFaction;
uint32 m_timeSyncCounter;
uint32 m_timeSyncTimer;
uint32 m_timeSyncClient;
uint32 m_timeSyncServer;
InstanceTimeMap _instanceResetTimes;
uint32 _pendingBindId;
uint32 _pendingBindTimer;

View File

@@ -1356,14 +1356,6 @@ void WorldSession::HandleSetTitleOpcode(WorldPacket& recv_data)
GetPlayer()->SetUInt32Value(PLAYER_CHOSEN_TITLE, title);
}
void WorldSession::HandleTimeSyncResp(WorldPacket& recv_data)
{
uint32 counter, clientTicks;
recv_data >> counter >> clientTicks;
//uint32 ourTicks = clientTicks + (World::GetGameTimeMS() - _player->m_timeSyncServer);
_player->m_timeSyncClient = clientTicks;
}
void WorldSession::HandleResetInstancesOpcode(WorldPacket& /*recv_data*/)
{
#if defined(ENABLE_EXTRAS) && defined(ENABLE_EXTRA_LOGS)

View File

@@ -14,6 +14,7 @@
#include "GameGraveyard.h"
#include "InstanceSaveMgr.h"
#include "Log.h"
#include "MathUtil.h"
#include "MapManager.h"
#include "ObjectMgr.h"
#include "Opcodes.h"
@@ -419,11 +420,6 @@ void WorldSession::HandleMovementOpcodes(WorldPacket& recvData)
if (mover->GetGUID() != _player->GetGUID())
movementInfo.flags &= ~MOVEMENTFLAG_WALKING;
uint32 mstime = World::GetGameTimeMS();
/*----------------------*/
if(m_clientTimeDelay == 0)
m_clientTimeDelay = mstime > movementInfo.time ? std::min(mstime - movementInfo.time, (uint32)100) : 0;
// Xinef: do not allow to move with UNIT_FLAG_DISABLE_MOVE
if (mover->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_DISABLE_MOVE))
{
@@ -442,8 +438,16 @@ void WorldSession::HandleMovementOpcodes(WorldPacket& recvData)
/* process position-change */
WorldPacket data(opcode, recvData.size());
//movementInfo.time = movementInfo.time + m_clientTimeDelay + MOVEMENT_PACKET_TIME_DELAY;
movementInfo.time = mstime; // pussywizard: set to time of relocation (server time), constant addition may smoothen movement clientside, but client sees target on different position than the real serverside position
int64 movementTime = (int64)movementInfo.time + _timeSyncClockDelta;
if (_timeSyncClockDelta == 0 || movementTime < 0 || movementTime > 0xFFFFFFFF)
{
LOG_INFO("misc", "The computed movement time using clockDelta is erronous. Using fallback instead");
movementInfo.time = getMSTime();
}
else
{
movementInfo.time = (uint32) movementTime;
}
movementInfo.guid = mover->GetGUID();
WriteMovementInfo(&data, &movementInfo);
@@ -796,3 +800,76 @@ void WorldSession::HandleMoveTimeSkippedOpcode(WorldPacket& recvData)
data << timeSkipped;
GetPlayer()->SendMessageToSet(&data, false);
}
void WorldSession::HandleTimeSyncResp(WorldPacket& recvData)
{
#if defined(ENABLE_EXTRAS) && defined(ENABLE_EXTRA_LOGS)
LOG_DEBUG("network", "CMSG_TIME_SYNC_RESP");
#endif
uint32 counter, clientTimestamp;
recvData >> counter >> clientTimestamp;
if (_pendingTimeSyncRequests.count(counter) == 0)
return;
uint32 serverTimeAtSent = _pendingTimeSyncRequests.at(counter);
_pendingTimeSyncRequests.erase(counter);
// 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.
uint32 roundTripDuration = getMSTimeDiff(serverTimeAtSent, recvData.GetReceivedTime());
uint32 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
*/
int64 clockDelta = (int64)serverTimeAtSent + (int64)lagDelay - (int64)clientTimestamp;
_timeSyncClockDeltaQueue.put(std::pair<int64, uint32>(clockDelta, roundTripDuration));
ComputeNewClockDelta();
}
void WorldSession::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.
std::vector<uint32> latencies;
std::vector<int64> clockDeltasAfterFiltering;
for (auto pair : _timeSyncClockDeltaQueue.content())
latencies.push_back(pair.second);
uint32 latencyMedian = median(latencies);
uint32 latencyStandardDeviation = standard_deviation(latencies);
uint32 sampleSizeAfterFiltering = 0;
for (auto pair : _timeSyncClockDeltaQueue.content())
{
if (pair.second <= latencyMedian + latencyStandardDeviation) {
clockDeltasAfterFiltering.push_back(pair.first);
sampleSizeAfterFiltering++;
}
}
if (sampleSizeAfterFiltering != 0)
{
int64 meanClockDelta = static_cast<int64>(mean(clockDeltasAfterFiltering));
if (std::abs(meanClockDelta - _timeSyncClockDelta) > 25)
_timeSyncClockDelta = meanClockDelta;
}
else if (_timeSyncClockDelta == 0)
{
std::pair<int64, uint32> back = _timeSyncClockDeltaQueue.peak_back();
_timeSyncClockDelta = back.first;
}
}

View File

@@ -926,7 +926,7 @@ OpcodeHandler opcodeTable[NUM_MSG_TYPES] =
/*0x38D*/ { "CMSG_MOVE_CHNG_TRANSPORT", STATUS_LOGGEDIN, PROCESS_THREADSAFE, &WorldSession::HandleMovementOpcodes },
/*0x38E*/ { "MSG_PARTY_ASSIGNMENT", STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandlePartyAssignmentOpcode },
/*0x38F*/ { "SMSG_OFFER_PETITION_ERROR", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_ServerSide },
/*0x390*/ { "SMSG_TIME_SYNC_REQ", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_ServerSide },
/*0x390*/ { "SMSG_TIME_SYNC_REQ", STATUS_NEVER, PROCESS_THREADSAFE, &WorldSession::Handle_ServerSide },
/*0x391*/ { "CMSG_TIME_SYNC_RESP", STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleTimeSyncResp },
/*0x392*/ { "CMSG_SEND_LOCAL_EVENT", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL },
/*0x393*/ { "CMSG_SEND_GENERAL_TRIGGER", STATUS_NEVER, PROCESS_INPLACE, &WorldSession::Handle_NULL },

View File

@@ -110,14 +110,16 @@ WorldSession::WorldSession(uint32 id, WorldSocket* sock, AccountTypes sec, uint8
m_sessionDbcLocale(sWorld->GetDefaultDbcLocale()),
m_sessionDbLocaleIndex(locale),
m_latency(0),
m_clientTimeDelay(0),
m_TutorialsChanged(false),
recruiterId(recruiter),
isRecruiter(isARecruiter),
m_currentVendorEntry(0),
m_currentBankerGUID(0),
timeWhoCommandAllowed(0),
_calendarEventCreationCooldown(0)
_calendarEventCreationCooldown(0),
_timeSyncClockDeltaQueue(6),
_timeSyncClockDelta(0),
_pendingTimeSyncRequests()
{
memset(m_Tutorials, 0, sizeof(m_Tutorials));
@@ -126,6 +128,9 @@ WorldSession::WorldSession(uint32 id, WorldSocket* sock, AccountTypes sec, uint8
_kicked = false;
_shouldSetOfflineInDB = true;
_timeSyncNextCounter = 0;
_timeSyncTimer = 0;
if (sock)
{
m_Address = sock->GetRemoteAddress();
@@ -253,7 +258,7 @@ void WorldSession::QueuePacket(WorldPacket* new_packet)
/// Update the WorldSession (triggered by World update)
bool WorldSession::Update(uint32 diff, PacketFilter& updater)
{
if (updater.ProcessLogout())
if (updater.ProcessUnsafe())
{
UpdateTimeOutTime(diff);
@@ -263,7 +268,7 @@ bool WorldSession::Update(uint32 diff, PacketFilter& updater)
m_Socket->CloseSocket("Client didn't send anything for too long");
}
HandleTeleportTimeout(updater.ProcessLogout());
HandleTeleportTimeout(updater.ProcessUnsafe());
uint32 _startMSTime = getMSTime();
WorldPacket* packet = nullptr;
@@ -390,7 +395,7 @@ bool WorldSession::Update(uint32 diff, PacketFilter& updater)
if (m_Socket && !m_Socket->IsClosed())
ProcessQueryCallbacks();
if (updater.ProcessLogout())
if (updater.ProcessUnsafe())
{
if (m_Socket && !m_Socket->IsClosed() && _warden)
{
@@ -415,6 +420,22 @@ bool WorldSession::Update(uint32 diff, PacketFilter& updater)
}
}
if (!updater.ProcessUnsafe()) // <=> updater is of type MapSessionFilter
{
// Send time sync packet every 10s.
if (_timeSyncTimer > 0)
{
if (diff >= _timeSyncTimer)
{
SendTimeSync();
}
else
{
_timeSyncTimer -= diff;
}
}
}
return true;
}
@@ -1648,3 +1669,22 @@ uint32 WorldSession::DosProtection::GetMaxPacketCounterAllowed(uint16 opcode) co
return maxPacketCounterAllowed;
}
void WorldSession::ResetTimeSync()
{
_timeSyncNextCounter = 0;
_pendingTimeSyncRequests.clear();
}
void WorldSession::SendTimeSync()
{
WorldPacket data(SMSG_TIME_SYNC_REQ, 4);
data << uint32(_timeSyncNextCounter);
SendPacket(&data);
_pendingTimeSyncRequests[_timeSyncNextCounter] = getMSTime();
// Schedule next sync in 10 sec (except for the 2 first packets, which are spaced by only 5s)
_timeSyncTimer = _timeSyncNextCounter == 0 ? 5000 : 10000;
_timeSyncNextCounter++;
}

View File

@@ -15,6 +15,7 @@
#include "AuthDefines.h"
#include "AddonMgr.h"
#include "BanManager.h"
#include "CircularBuffer.h"
#include "Common.h"
#include "DatabaseEnv.h"
#include "GossipDef.h"
@@ -23,6 +24,7 @@
#include "World.h"
#include "WorldPacket.h"
#include <utility>
#include <map>
class Creature;
class GameObject;
@@ -124,7 +126,7 @@ public:
virtual ~PacketFilter() = default;
virtual bool Process(WorldPacket* /*packet*/) { return true; }
[[nodiscard]] virtual bool ProcessLogout() const { return true; }
[[nodiscard]] virtual bool ProcessUnsafe() const { return true; }
protected:
WorldSession* const m_pSession;
@@ -138,7 +140,7 @@ public:
bool Process(WorldPacket* packet) override;
//in Map::Update() we do not process player logout!
[[nodiscard]] bool ProcessLogout() const override { return false; }
[[nodiscard]] bool ProcessUnsafe() const override { return false; }
};
//class used to filer only thread-unsafe packets from queue
@@ -357,7 +359,6 @@ public:
uint32 GetLatency() const { return m_latency; }
void SetLatency(uint32 latency) { m_latency = latency; }
void ResetClientTimeDelay() { m_clientTimeDelay = 0; }
std::atomic<time_t> m_timeOutTime;
void UpdateTimeOutTime(uint32 diff)
@@ -387,6 +388,9 @@ public:
time_t GetCalendarEventCreationCooldown() const { return _calendarEventCreationCooldown; }
void SetCalendarEventCreationCooldown(time_t cooldown) { _calendarEventCreationCooldown = cooldown; }
// Time Synchronisation
void ResetTimeSync();
void SendTimeSync();
public: // opcodes handlers
void Handle_NULL(WorldPacket& recvPacket); // not used
void Handle_EarlyProccess(WorldPacket& recvPacket); // just mark packets processed in WorldSocket::OnRead
@@ -1028,7 +1032,6 @@ private:
LocaleConstant m_sessionDbcLocale;
LocaleConstant m_sessionDbLocaleIndex;
uint32 m_latency;
uint32 m_clientTimeDelay;
AccountData m_accountData[NUM_ACCOUNT_DATA_TYPES];
uint32 m_Tutorials[MAX_ACCOUNT_TUTORIAL_VALUES];
bool m_TutorialsChanged;
@@ -1044,6 +1047,14 @@ private:
bool _shouldSetOfflineInDB;
// Packets cooldown
time_t _calendarEventCreationCooldown;
CircularBuffer<std::pair<int64, uint32>> _timeSyncClockDeltaQueue; // first member: clockDelta. Second member: latency of the packet exchange that was used to compute that clockDelta.
int64 _timeSyncClockDelta;
void ComputeNewClockDelta();
std::map<uint32, uint32> _pendingTimeSyncRequests; // key: counter. value: server time when packet with that counter was sent.
uint32 _timeSyncNextCounter;
uint32 _timeSyncTimer;
};
#endif
/// @}

View File

@@ -669,15 +669,13 @@ int WorldSocket::ProcessIncoming(WorldPacket* new_pct)
switch (opcode)
{
case CMSG_PING:
try
{
try
{
return HandlePing(*new_pct);
}
catch (ByteBufferPositionException const&) {}
LOG_ERROR("server", "WorldSocket::ReadDataHandler(): client sent malformed CMSG_PING");
return -1;
return HandlePing(*new_pct);
}
catch (ByteBufferPositionException const&) { }
LOG_ERROR("server", "WorldSocket::ReadDataHandler(): client sent malformed CMSG_PING");
return -1;
case CMSG_AUTH_SESSION:
if (m_Session)
{
@@ -689,27 +687,11 @@ int WorldSocket::ProcessIncoming(WorldPacket* new_pct)
if (m_Session)
m_Session->ResetTimeOutTime(true);
return 0;
case CMSG_TIME_SYNC_RESP:
new_pct = new WorldPacket(std::move(*new_pct), std::chrono::steady_clock::now());
break;
default:
{
std::lock_guard<std::mutex> guard(m_SessionLock);
if (m_Session != nullptr)
{
// Our Idle timer will reset on any non PING opcodes.
// Catches people idling on the login screen and any lingering ingame connections.
m_Session->ResetTimeOutTime(false);
// OK, give the packet to WorldSession
aptr.release();
m_Session->QueuePacket (new_pct);
return 0;
}
else
{
LOG_ERROR("server", "WorldSocket::ProcessIncoming: Client not authed opcode = %u", uint32(opcode));
return -1;
}
}
break;
}
}
catch (ByteBufferException const&)
@@ -724,7 +706,25 @@ int WorldSocket::ProcessIncoming(WorldPacket* new_pct)
return -1;
}
ACE_NOTREACHED (return 0);
std::lock_guard<std::mutex> guard(m_SessionLock);
if (m_Session != nullptr)
{
// Our Idle timer will reset on any non PING or TIME_SYNC opcodes.
// Catches people idling on the login screen and any lingering ingame connections.
if (opcode != CMSG_PING && opcode != CMSG_TIME_SYNC_RESP)
{
m_Session->ResetTimeOutTime(false);
}
// OK, give the packet to WorldSession
aptr.release();
m_Session->QueuePacket(new_pct);
return 0;
}
LOG_ERROR("server", "WorldSocket::ProcessIncoming: Client not authed opcode = %u", uint32(opcode));
return -1;
}
int WorldSocket::HandleAuthSession(WorldPacket& recvPacket)
@@ -1074,7 +1074,6 @@ int WorldSocket::HandlePing(WorldPacket& recvPacket)
if (m_Session)
{
m_Session->SetLatency (latency);
m_Session->ResetClientTimeDelay();
}
else
{