diff --git a/Private b/Private index a1553b4d..bb40e3af 160000 --- a/Private +++ b/Private @@ -1 +1 @@ -Subproject commit a1553b4dca9bf4b6b710ebc2052b254a6e1e7ffd +Subproject commit bb40e3af4b3ee1f39de9383deff4cc742076aa6f diff --git a/Spawner.vcxproj b/Spawner.vcxproj index 5df113d1..134103d5 100644 --- a/Spawner.vcxproj +++ b/Spawner.vcxproj @@ -53,7 +53,9 @@ + + @@ -84,7 +86,9 @@ + + diff --git a/YRpp b/YRpp index ef1c565a..ae9a8418 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit ef1c565ade4a9233177a7949034ed7bd245259f3 +Subproject commit ae9a8418ee618201d69ecc2b0124cc24ea0b6147 diff --git a/src/Spawner/FastRetransmit.cpp b/src/Spawner/FastRetransmit.cpp new file mode 100644 index 00000000..ce0dffb5 --- /dev/null +++ b/src/Spawner/FastRetransmit.cpp @@ -0,0 +1,293 @@ +/** +* yrpp-spawner +* +* Copyright(C) 2023-present CnCNet +* +* 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 . +*/ + +#include "FastRetransmit.h" +#include "PacketRedundancy.h" + +#include +#include +#include +#include +#include +#include +#include + +bool FastRetransmit::Enabled = true; +bool FastRetransmit::Backoff = true; + +namespace +{ + struct PeerEstimator + { + const ConnectionClass* connection; + bool initialized; + int srtt; + int rttvar; + int rto; + int cleanSamples; + }; + + PeerEstimator Peers[FastRetransmit::MaxPeers] = {}; + + int ClampTicks(int value) + { + if (value < FastRetransmit::MinTicks) + return FastRetransmit::MinTicks; + if (value > FastRetransmit::MaxTicks) + return FastRetransmit::MaxTicks; + return value; + } + + int ActiveConnectionCount() + { + const GameMode gm = SessionClass::Instance.GameMode; + if (gm != GameMode::LAN && gm != GameMode::Internet) + return 0; + + int nconn = static_cast(IPXManagerClass::Instance.NumConnections); + if (nconn < 0) nconn = 0; + if (nconn > FastRetransmit::MaxPeers) nconn = FastRetransmit::MaxPeers; + return nconn; + } + + // True if connection is still one of the engine's live connections. A + // reconnect replaces a peer's ConnectionClass with a new instance, and the + // old pointer never comes back - without this check a stale slot's frozen + // RTO sample sits in Peers[] forever and can dominate EffectiveRTO()'s max + // long after the connection it came from is gone. + bool IsLiveConnection(const ConnectionClass* connection) + { + if (!connection) + return false; + + int nconn = static_cast(IPXManagerClass::Instance.NumConnections); + const int arraySize = sizeof(IPXManagerClass::Instance.Connection) / sizeof(IPXManagerClass::Instance.Connection[0]); + if (nconn > arraySize) nconn = arraySize; + + for (int i = 0; i < nconn; ++i) + if (IPXManagerClass::Instance.Connection[i] == connection) + return true; + return false; + } + + void PruneDeadSlots() + { + for (int i = 0; i < FastRetransmit::MaxPeers; ++i) + if (Peers[i].connection && !IsLiveConnection(Peers[i].connection)) + Peers[i] = PeerEstimator{}; + } + + // Finds this connection's estimator slot, allocating a free one or evicting + // slot 0 if all MaxPeers slots are already in use. + PeerEstimator* FindSlot(const ConnectionClass* connection) + { + if (!connection) + return nullptr; + + PeerEstimator* freeSlot = nullptr; + for (int i = 0; i < FastRetransmit::MaxPeers; ++i) + { + if (Peers[i].connection == connection) + return &Peers[i]; + if (!Peers[i].connection && !freeSlot) + freeSlot = &Peers[i]; + } + + if (!freeSlot) + freeSlot = &Peers[0]; + + *freeSlot = PeerEstimator{}; + freeSlot->connection = connection; + return freeSlot; + } +} + +void FastRetransmit::Reset() +{ + for (int i = 0; i < MaxPeers; ++i) + Peers[i] = PeerEstimator{}; +} + +// Feeds one clean (non-retransmitted) RTT sample into that peer's smoothed estimate. +void FastRetransmit::SampleRTT(const ConnectionClass* connection, int delayTicks, int sendCount) +{ + if (!Enabled) + return; + + // Karn: ignore RTT measurements for retransmitted packets. + if (sendCount > 1) + return; + if (delayTicks < 0 || delayTicks > MaxTicks) + return; + + PeerEstimator* peer = FindSlot(connection); + if (!peer) + return; + + if (!peer->initialized) + { + peer->initialized = true; + peer->srtt = delayTicks; + peer->rttvar = delayTicks > 1 ? delayTicks / 2 : 1; + } + else + { + int err = peer->srtt - delayTicks; + if (err < 0) err = -err; + peer->rttvar = (peer->rttvar * 3 + err) / 4; + if (peer->rttvar < 1) peer->rttvar = 1; + peer->srtt = (peer->srtt * 7 + delayTicks) / 8; + } + + int margin = peer->rttvar * 4; + if (margin < MarginTicks) + margin = MarginTicks; + peer->rto = ClampTicks(peer->srtt + margin); + ++peer->cleanSamples; +} + +int FastRetransmit::ActivePeers() +{ + return ActiveConnectionCount(); +} + +int FastRetransmit::InitializedPeers() +{ + int count = 0; + for (int i = 0; i < MaxPeers; ++i) + if (Peers[i].connection && Peers[i].initialized) + ++count; + return count; +} + +// Worst (max) RTO among peers sampled so far, or 0 if none have a clean sample yet. +int FastRetransmit::EffectiveRTO() +{ + if (ActiveConnectionCount() <= 0) + return 0; + + PruneDeadSlots(); + + int rto = 0; + bool any = false; + for (int i = 0; i < MaxPeers; ++i) + { + if (Peers[i].connection && Peers[i].initialized) + { + any = true; + if (Peers[i].rto > rto) + rto = Peers[i].rto; + } + } + return any ? ClampTicks(rto) : 0; +} + +int FastRetransmit::CleanSamples() +{ + int total = 0; + for (int i = 0; i < MaxPeers; ++i) + total += Peers[i].cleanSamples; + return total; +} + +// ConnectionClass::Service_Send_Queue, at the point an ACK'd PACKET_DATA_ACK +// entry's round-trip is about to be folded into the queue's response time. +DEFINE_HOOK(0x48C436, ConnectionClass_ServiceSendQueue_RTTSample, 0x8) +{ + if (FastRetransmit::Enabled) + { + GET(const ConnectionClass*, conn, EDI); + GET(const SendQueueType*, entry, EBX); + GET(int, now, EBP); + + FastRetransmit::SampleRTT(conn, now - entry->FirstTime, entry->SendCount); + } + return 0; +} + +// IPXManagerClass::Set_Timing entry. We overwrite retrydelta in place from the +// maximum clean per-peer RTO. Only shortens, never lengthens. +DEFINE_HOOK(0x540C60, IPXManagerClass_SetTiming_FastRetransmit, 0x8) +{ + if (!FastRetransmit::Enabled) + return 0; + + int retryDelta = FastRetransmit::EffectiveRTO(); + if (retryDelta <= 0) + return 0; + + GET_STACK(int, original, 0x4); + if (retryDelta < original) + { + R->Stack(0x4, retryDelta); + + static int lastLogged = -1; + if (retryDelta != lastLogged) + { + lastLogged = retryDelta; + Debug::Log("[FastRetransmit] RetryDelta %d -> %d ticks (peers=%d/%d clean=%d)\n", + original, retryDelta, + FastRetransmit::InitializedPeers(), FastRetransmit::ActivePeers(), FastRetransmit::CleanSamples()); + } + } + + return 0; +} + +DEFINE_HOOK(0x48C4AE, ConnectionClass_ServiceSendQueue_Backoff, 0x5) +{ + enum { Compare = 0x48C4B3 }; + + GET(const ConnectionClass*, conn, EDI); + GET(const SendQueueType*, entry, ESI); + GET(int, now, EBP); + GET(int, lastTime, ECX); + + const int retryDelta = static_cast(conn->RetryDelta); + const int sendCount = entry->SendCount; + const int elapsed = now - lastTime; + + if (!FastRetransmit::Enabled || !FastRetransmit::Backoff) + { + if (elapsed > retryDelta) + PacketRedundancy::NoteResend(conn); + return 0; + } + + int prior = sendCount - 1; + if (prior < 0) prior = 0; + if (prior > FastRetransmit::BackoffCap) prior = FastRetransmit::BackoffCap; + + int base = retryDelta; + if (base < FastRetransmit::MinTicks) + base = FastRetransmit::MinTicks; + + long long backedOff = static_cast(base) + + (static_cast(base) * prior * FastRetransmit::BackoffStepHalves) / 2; + if (backedOff > 0x7FFFFFFF) + backedOff = 0x7FFFFFFF; + int eff = static_cast(backedOff); + + if (elapsed > eff) + PacketRedundancy::NoteResend(conn); + + R->EAX(eff); + R->EDX(now); + return Compare; +} diff --git a/src/Spawner/FastRetransmit.h b/src/Spawner/FastRetransmit.h new file mode 100644 index 00000000..9152d804 --- /dev/null +++ b/src/Spawner/FastRetransmit.h @@ -0,0 +1,62 @@ +/** +* yrpp-spawner +* +* Copyright(C) 2023-present CnCNet +* +* 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 . +*/ + +/** +* FastRetransmit - RTT-adaptive retransmit timer. +* +* The engine recomputes RetryDelta as Response_Time() + 10. Response_Time() +* includes ACK delays for retransmitted packets, so loss inflates the estimate +* that controls the next retransmit wait. This keeps a clean per-connection RTT +* estimator using Karn's rule and only shortens the engine's global timer once +* any peer has a clean sample. Because the engine has one RetryDelta for the +* whole match, we use the maximum peer RTO. +* https://en.wikipedia.org/wiki/Karn's_algorithm +*/ + +#pragma once + +class ConnectionClass; + +class FastRetransmit +{ +public: + static const int MaxPeers = 8; + + static bool Enabled; + + // Added to the smoothed round trip to absorb ordinary jitter. + static const int MarginTicks = 2; + // Never arm the timer shorter than this (guards the near-zero-RTT/LAN case). + static const int MinTicks = 2; + // Drop absurd / clock-glitch samples and cap computed RTOs to a sane range. + static const int MaxTicks = 250; + + // Gentle retransmit backoff. Requires both FastRetransmit and + // RetransmitBackoff to be enabled before it changes retransmit decisions. + static bool Backoff; + static const int BackoffStepHalves = 1; // +1/2 of base per prior retry + static const int BackoffCap = 4; // max prior-retries counted (up to 3x base) + + static void Reset(); + static void SampleRTT(const ConnectionClass* connection, int delayTicks, int sendCount); + static int EffectiveRTO(); + static int InitializedPeers(); + static int ActivePeers(); + static int CleanSamples(); +}; diff --git a/src/Spawner/NetHack.cpp b/src/Spawner/NetHack.cpp index 0b3c0378..3e1a588d 100644 --- a/src/Spawner/NetHack.cpp +++ b/src/Spawner/NetHack.cpp @@ -16,6 +16,7 @@ #include "NetHack.h" #include "Spawner.h" +#include "PacketRedundancy.h" #include #include @@ -52,7 +53,12 @@ int WINAPI NetHack::SendTo( tempDest.sin_port = player.Port; tempDest.sin_addr.S_un.S_addr = player.Ip; - return Tunnel::SendTo(sockfd, buf, len, flags, &tempDest, addrlen); + const int copies = PacketRedundancy::CopiesFor(buf, len, index); + + int ret = Tunnel::SendTo(sockfd, buf, len, flags, &tempDest, addrlen); + for (int i = 1; i < copies; ++i) + PacketRedundancy::NoteExtraSend(Tunnel::SendTo(sockfd, buf, len, flags, &tempDest, addrlen)); + return ret; } int WINAPI NetHack::RecvFrom( @@ -116,10 +122,11 @@ int WINAPI Tunnel::SendTo( *BufFrom = Tunnel::Id; *BufTo = dest_addr->sin_port; - dest_addr->sin_port = Tunnel::Port; - dest_addr->sin_addr.S_un.S_addr = Tunnel::Ip; + sockaddr_in sendDest = *dest_addr; + sendDest.sin_port = Tunnel::Port; + sendDest.sin_addr.S_un.S_addr = Tunnel::Ip; - return sendto(sockfd, TempBuf, len + 4, flags, (struct sockaddr*)dest_addr, addrlen); + return sendto(sockfd, TempBuf, len + 4, flags, (struct sockaddr*)&sendDest, addrlen); } int WINAPI Tunnel::RecvFrom( diff --git a/src/Spawner/NetHack.h b/src/Spawner/NetHack.h index 69ec0c13..197377b6 100644 --- a/src/Spawner/NetHack.h +++ b/src/Spawner/NetHack.h @@ -14,6 +14,7 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#pragma once #include struct ListAddress diff --git a/src/Spawner/PacketRedundancy.cpp b/src/Spawner/PacketRedundancy.cpp new file mode 100644 index 00000000..ccba03db --- /dev/null +++ b/src/Spawner/PacketRedundancy.cpp @@ -0,0 +1,170 @@ +/** +* yrpp-spawner +* +* Copyright(C) 2023-present CnCNet +* +* 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 . +*/ + +#include "PacketRedundancy.h" + +#include +#include +#include +#include +#include + +bool PacketRedundancy::Enabled = true; +int PacketRedundancy::Copies = 2; +bool PacketRedundancy::Adaptive = false; + +namespace +{ + constexpr size_t HeaderOffset = 4; + constexpr size_t CodeOffset = HeaderOffset + offsetof(CommHeaderType, Code); + + const int GaugeBump = 1000; + const int GaugeCap = 5000; + const int GaugeMsPerUnit = 1; + + int g_gauge[PacketRedundancy::MaxPeers] = {}; + DWORD g_lastTick[PacketRedundancy::MaxPeers] = {}; + + bool ValidPeer(int peer) + { + return peer >= 0 && peer < PacketRedundancy::MaxPeers; + } + + void DecayGauge(int peer) + { + if (!ValidPeer(peer)) + return; + + DWORD now = GetTickCount(); + if (g_lastTick[peer] == 0) + { + g_lastTick[peer] = now; + return; + } + + DWORD dt = now - g_lastTick[peer]; + g_lastTick[peer] = now; + int dec = static_cast(dt) / GaugeMsPerUnit; + g_gauge[peer] = (dec >= g_gauge[peer]) ? 0 : (g_gauge[peer] - dec); + } + + void BumpGauge(int peer) + { + if (!ValidPeer(peer)) + return; + + DecayGauge(peer); + g_gauge[peer] += GaugeBump; + if (g_gauge[peer] > GaugeCap) + g_gauge[peer] = GaugeCap; + } + + // Maps a connection to the peer index CopiesFor() is keyed by - the + // ListAddress slot, i.e. the spawn.ini player index minus one. + int PeerIndexForConnection(const ConnectionClass* connection) + { + if (!connection) + return -1; + + int nconn = static_cast(IPXManagerClass::Instance.NumConnections); + const int arraySize = sizeof(IPXManagerClass::Instance.Connection) / sizeof(IPXManagerClass::Instance.Connection[0]); + if (nconn > arraySize) nconn = arraySize; + + for (int i = 0; i < nconn; ++i) + { + const IPXConnClass* conn = IPXManagerClass::Instance.Connection[i]; + if (conn != connection) + continue; + + DWORD slot = 0; + memcpy(&slot, conn->Address.NodeAddress, sizeof(slot)); + + const int peer = static_cast(slot) - 1; + return ValidPeer(peer) ? peer : -1; + } + + return -1; + } +} + +void PacketRedundancy::Reset() +{ + for (int i = 0; i < MaxPeers; ++i) + { + g_gauge[i] = 0; + g_lastTick[i] = 0; + } +} + +int PacketRedundancy::ClampCopies(int copies) +{ + return copies == 1 ? 1 : 2; +} + +// Bumps the loss gauge for whichever peer this resend belongs to (or every +// peer if the connection can't be resolved, so the signal is never dropped). +void PacketRedundancy::NoteResend(const ConnectionClass* connection) +{ + int peer = PeerIndexForConnection(connection); + if (ValidPeer(peer)) + { + BumpGauge(peer); + return; + } + + for (int i = 0; i < MaxPeers; ++i) + BumpGauge(i); +} + +// Current loss gauge for peer, decayed up to now. +int PacketRedundancy::LossGauge(int peer) +{ + if (!ValidPeer(peer)) + return 0; + + DecayGauge(peer); + return g_gauge[peer]; +} + +void PacketRedundancy::NoteExtraSend(int sendResult) +{ + if (sendResult == -1) + Debug::Log("[PacketRedundancy] extra sendto() for a duplicate copy failed\n"); +} + +// Decides how many times to send this outbound datagram: only reliable +// (PACKET_DATA_ACK) packets duplicate, and only while enabled and (if +// Adaptive) actual loss is being observed for this peer. +int PacketRedundancy::CopiesFor(const char* buf, size_t len, int peer) +{ + if (!Enabled || !buf || len <= CodeOffset) + return 1; + + if (Copies < 2) + return 1; + + const auto* header = reinterpret_cast(buf + HeaderOffset); + if (header->Code != ConnectionEnum::PACKET_DATA_ACK) + return 1; + + if (Adaptive && LossGauge(peer) <= 0) + return 1; + + return Copies; +} diff --git a/src/Spawner/PacketRedundancy.h b/src/Spawner/PacketRedundancy.h new file mode 100644 index 00000000..e02f28af --- /dev/null +++ b/src/Spawner/PacketRedundancy.h @@ -0,0 +1,59 @@ +/** +* yrpp-spawner +* +* Copyright(C) 2023-present CnCNet +* +* 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 . +*/ + +/** +* PacketRedundancy - outbound redundancy for reliable command packets. +* +* Reliable command packets (CommHeader Code == PACKET_DATA_ACK) are sent twice +* on the wire so an isolated loss can be recovered without waiting for ARQ +* retransmit. The duplicate is byte-identical and rides below the engine's +* reliable/in-order layer, which acknowledges duplicate PacketIDs without +* delivering commands twice. +*/ + +#pragma once +#include + +class ConnectionClass; + +class PacketRedundancy +{ +public: + static const int MaxPeers = 8; + + static bool Enabled; + static int Copies; + // When true, only duplicate to peers where recent packet loss is observed. + static bool Adaptive; + + static void Reset(); + static int ClampCopies(int copies); + + // Number of times this outbound datagram should be sent to peer (1 == send + // once, no duplication). buf points at the on-wire game bytes + // ([CRC(4)][CommHeader...]). + static int CopiesFor(const char* buf, size_t len, int peer); + + // Loss signal for adaptive mode. + static void NoteResend(const ConnectionClass* connection); + static int LossGauge(int peer); + + // Logs a failed extra sendto() call for a duplicate copy. + static void NoteExtraSend(int sendResult); +}; diff --git a/src/Spawner/Spawner.Config.cpp b/src/Spawner/Spawner.Config.cpp index 205eca9d..b6642bb4 100644 --- a/src/Spawner/Spawner.Config.cpp +++ b/src/Spawner/Spawner.Config.cpp @@ -85,6 +85,11 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI) MaxAhead = pINI->ReadInteger(pSettingsSection, "MaxAhead", MaxAhead); PreCalcMaxAhead = pINI->ReadInteger(pSettingsSection, "PreCalcMaxAhead", PreCalcMaxAhead); MaxLatencyLevel = (byte)pINI->ReadInteger(pSettingsSection, "MaxLatencyLevel", (int)MaxLatencyLevel); + FastRetransmit = pINI->ReadBool(pSettingsSection, "FastRetransmit", FastRetransmit); + RetransmitBackoff = pINI->ReadBool(pSettingsSection, "RetransmitBackoff", RetransmitBackoff); + PacketRedundancy = pINI->ReadBool(pSettingsSection, "PacketRedundancy", PacketRedundancy); + RedundancyCopies = pINI->ReadInteger(pSettingsSection, "RedundancyCopies", RedundancyCopies); + AdaptiveRedundancy = pINI->ReadBool(pSettingsSection, "AdaptiveRedundancy", AdaptiveRedundancy); ForceMultiplayer = pINI->ReadBool(pSettingsSection, "ForceMultiplayer", ForceMultiplayer); } diff --git a/src/Spawner/Spawner.Config.h b/src/Spawner/Spawner.Config.h index fca7b2be..917babd0 100644 --- a/src/Spawner/Spawner.Config.h +++ b/src/Spawner/Spawner.Config.h @@ -123,6 +123,11 @@ class SpawnerConfig int MaxAhead; int PreCalcMaxAhead; byte MaxLatencyLevel; + bool FastRetransmit; + bool RetransmitBackoff; + bool PacketRedundancy; + int RedundancyCopies; + bool AdaptiveRedundancy; bool ForceMultiplayer; // Tunnel Options @@ -201,6 +206,11 @@ class SpawnerConfig , MaxAhead { -1 } , PreCalcMaxAhead { 0 } , MaxLatencyLevel { 0xFF } + , FastRetransmit { true } + , RetransmitBackoff { true } + , PacketRedundancy { true } + , RedundancyCopies { 2 } + , AdaptiveRedundancy { true } , ForceMultiplayer { false } // Tunnel Options diff --git a/src/Spawner/Spawner.cpp b/src/Spawner/Spawner.cpp index 3b549611..ee3a159c 100644 --- a/src/Spawner/Spawner.cpp +++ b/src/Spawner/Spawner.cpp @@ -22,6 +22,8 @@ #include "NetHack.h" #include "ProtocolZero.h" #include "ProtocolZero.LatencyLevel.h" +#include "FastRetransmit.h" +#include "PacketRedundancy.h" #include #include #include @@ -412,6 +414,14 @@ void Spawner::InitNetwork() Game::Network::GameStockKeepingUnit = 0x2901; ProtocolZero::Enable = (pSpawnerConfig->Protocol == 0); + + FastRetransmit::Enabled = pSpawnerConfig->FastRetransmit; + FastRetransmit::Backoff = pSpawnerConfig->FastRetransmit && pSpawnerConfig->RetransmitBackoff; + PacketRedundancy::Enabled = pSpawnerConfig->PacketRedundancy; + PacketRedundancy::Copies = PacketRedundancy::ClampCopies(pSpawnerConfig->RedundancyCopies); + PacketRedundancy::Adaptive = pSpawnerConfig->AdaptiveRedundancy; + FastRetransmit::Reset(); + PacketRedundancy::Reset(); if (ProtocolZero::Enable) { Game::Network::FrameSendRate = 2;