From 970d0a1b1a74ee04fd646f68d8b6d6036c54118e Mon Sep 17 00:00:00 2001 From: "[11EJ11]" Date: Wed, 5 Aug 2026 13:35:15 +1200 Subject: [PATCH 1/4] Add packet sending redundancy --- Spawner.vcxproj | 2 + YRpp | 2 +- src/Spawner/NetHack.cpp | 15 ++- src/Spawner/NetHack.h | 1 + src/Spawner/PacketRedundancy.cpp | 160 +++++++++++++++++++++++++++++++ src/Spawner/PacketRedundancy.h | 59 ++++++++++++ src/Spawner/Spawner.Config.cpp | 3 + src/Spawner/Spawner.Config.h | 6 ++ src/Spawner/Spawner.cpp | 6 ++ 9 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 src/Spawner/PacketRedundancy.cpp create mode 100644 src/Spawner/PacketRedundancy.h diff --git a/Spawner.vcxproj b/Spawner.vcxproj index 5df113d1..d3741f6d 100644 --- a/Spawner.vcxproj +++ b/Spawner.vcxproj @@ -54,6 +54,7 @@ + @@ -85,6 +86,7 @@ + diff --git a/YRpp b/YRpp index ef1c565a..602db14b 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit ef1c565ade4a9233177a7949034ed7bd245259f3 +Subproject commit 602db14b0ec09dad6e4e972465e4f1caf0bb932d 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..3b317628 --- /dev/null +++ b/src/Spawner/PacketRedundancy.cpp @@ -0,0 +1,160 @@ +/** +* 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 + +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; + } + + 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) + if (reinterpret_cast(IPXManagerClass::Instance.Connection[i]) == connection) + return i; + + 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; + } + + // Shouldn't happen (connection always belongs to one of the live slots), + // but fall back to bumping everyone rather than silently dropping the signal. + 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..d33f376a 100644 --- a/src/Spawner/Spawner.Config.cpp +++ b/src/Spawner/Spawner.Config.cpp @@ -85,6 +85,9 @@ 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); + 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..cd2178bf 100644 --- a/src/Spawner/Spawner.Config.h +++ b/src/Spawner/Spawner.Config.h @@ -123,6 +123,9 @@ class SpawnerConfig int MaxAhead; int PreCalcMaxAhead; byte MaxLatencyLevel; + bool PacketRedundancy; + int RedundancyCopies; + bool AdaptiveRedundancy; bool ForceMultiplayer; // Tunnel Options @@ -201,6 +204,9 @@ class SpawnerConfig , MaxAhead { -1 } , PreCalcMaxAhead { 0 } , MaxLatencyLevel { 0xFF } + , PacketRedundancy { true } + , RedundancyCopies { 2 } + , AdaptiveRedundancy { true } , ForceMultiplayer { false } // Tunnel Options diff --git a/src/Spawner/Spawner.cpp b/src/Spawner/Spawner.cpp index 3b549611..8b558ee8 100644 --- a/src/Spawner/Spawner.cpp +++ b/src/Spawner/Spawner.cpp @@ -22,6 +22,7 @@ #include "NetHack.h" #include "ProtocolZero.h" #include "ProtocolZero.LatencyLevel.h" +#include "PacketRedundancy.h" #include #include #include @@ -412,6 +413,11 @@ void Spawner::InitNetwork() Game::Network::GameStockKeepingUnit = 0x2901; ProtocolZero::Enable = (pSpawnerConfig->Protocol == 0); + + PacketRedundancy::Enabled = pSpawnerConfig->PacketRedundancy; + PacketRedundancy::Copies = PacketRedundancy::ClampCopies(pSpawnerConfig->RedundancyCopies); + PacketRedundancy::Adaptive = pSpawnerConfig->AdaptiveRedundancy; + PacketRedundancy::Reset(); if (ProtocolZero::Enable) { Game::Network::FrameSendRate = 2; From 14ae5c0a186cf3d323870518ffd4b4c04f302555 Mon Sep 17 00:00:00 2001 From: "[11EJ11]" Date: Wed, 5 Aug 2026 13:42:20 +1200 Subject: [PATCH 2/4] Add fast retransmit --- Spawner.vcxproj | 2 + src/Spawner/FastRetransmit.cpp | 288 +++++++++++++++++++++++++++++++++ src/Spawner/FastRetransmit.h | 62 +++++++ src/Spawner/Spawner.Config.cpp | 2 + src/Spawner/Spawner.Config.h | 4 + src/Spawner/Spawner.cpp | 4 + 6 files changed, 362 insertions(+) create mode 100644 src/Spawner/FastRetransmit.cpp create mode 100644 src/Spawner/FastRetransmit.h diff --git a/Spawner.vcxproj b/Spawner.vcxproj index d3741f6d..134103d5 100644 --- a/Spawner.vcxproj +++ b/Spawner.vcxproj @@ -53,6 +53,7 @@ + @@ -85,6 +86,7 @@ + diff --git a/src/Spawner/FastRetransmit.cpp b/src/Spawner/FastRetransmit.cpp new file mode 100644 index 00000000..33d43e67 --- /dev/null +++ b/src/Spawner/FastRetransmit.cpp @@ -0,0 +1,288 @@ +/** +* 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 + +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 (reinterpret_cast(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, ServiceSendQueue_RTTSample_FastRetransmit, 0x8) +{ + if (FastRetransmit::Enabled) + { + const auto* entry = reinterpret_cast(R->EBX()); + int delay = static_cast(R->EBP()) - entry->FirstTime; + FastRetransmit::SampleRTT(reinterpret_cast(R->EDI()), delay, 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, IPXSetTiming_FastRetransmit, 0x8) +{ + if (!FastRetransmit::Enabled) + return 0; + + int retryDelta = FastRetransmit::EffectiveRTO(); + if (retryDelta <= 0) + return 0; + + int original = R->Stack(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; +} + +// ConnectionClass::Service_Send_Queue, at the per-entry retransmit decision - +// also the one place a real retransmit is detected, so it feeds +// PacketRedundancy's loss signal even when FastRetransmit/Backoff are off. +DEFINE_HOOK(0x48C4AE, ServiceSendQueue_Backoff_FastRetransmit, 0x5) +{ + const auto* conn = reinterpret_cast(R->EDI()); + const auto* entry = reinterpret_cast(R->ESI()); + const int retryDelta = static_cast(conn->RetryDelta); + const int sendCount = entry->SendCount; + const int elapsed = static_cast(R->EBP()) - R->ECX(); + + if (!FastRetransmit::Enabled || !FastRetransmit::Backoff) + { + if (elapsed > retryDelta) + PacketRedundancy::NoteResend(reinterpret_cast(R->EDI())); + 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(reinterpret_cast(R->EDI())); + + R->EAX(eff); + R->EDX(R->EBP()); + return 0x48C4B3; +} 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/Spawner.Config.cpp b/src/Spawner/Spawner.Config.cpp index d33f376a..b6642bb4 100644 --- a/src/Spawner/Spawner.Config.cpp +++ b/src/Spawner/Spawner.Config.cpp @@ -85,6 +85,8 @@ 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); diff --git a/src/Spawner/Spawner.Config.h b/src/Spawner/Spawner.Config.h index cd2178bf..917babd0 100644 --- a/src/Spawner/Spawner.Config.h +++ b/src/Spawner/Spawner.Config.h @@ -123,6 +123,8 @@ class SpawnerConfig int MaxAhead; int PreCalcMaxAhead; byte MaxLatencyLevel; + bool FastRetransmit; + bool RetransmitBackoff; bool PacketRedundancy; int RedundancyCopies; bool AdaptiveRedundancy; @@ -204,6 +206,8 @@ class SpawnerConfig , MaxAhead { -1 } , PreCalcMaxAhead { 0 } , MaxLatencyLevel { 0xFF } + , FastRetransmit { true } + , RetransmitBackoff { true } , PacketRedundancy { true } , RedundancyCopies { 2 } , AdaptiveRedundancy { true } diff --git a/src/Spawner/Spawner.cpp b/src/Spawner/Spawner.cpp index 8b558ee8..ee3a159c 100644 --- a/src/Spawner/Spawner.cpp +++ b/src/Spawner/Spawner.cpp @@ -22,6 +22,7 @@ #include "NetHack.h" #include "ProtocolZero.h" #include "ProtocolZero.LatencyLevel.h" +#include "FastRetransmit.h" #include "PacketRedundancy.h" #include #include @@ -414,9 +415,12 @@ void Spawner::InitNetwork() 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) { From 1d40d4e844a604f748d5360fc53df18d9e234e99 Mon Sep 17 00:00:00 2001 From: "[11EJ11]" Date: Thu, 6 Aug 2026 16:16:13 +1200 Subject: [PATCH 3/4] Update PacketRedundancy peer lookup and submodule pointers --- Private | 2 +- YRpp | 2 +- src/Spawner/FastRetransmit.cpp | 3 ++- src/Spawner/PacketRedundancy.cpp | 18 ++++++++++++++---- 4 files changed, 18 insertions(+), 7 deletions(-) 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/YRpp b/YRpp index 602db14b..ae9a8418 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit 602db14b0ec09dad6e4e972465e4f1caf0bb932d +Subproject commit ae9a8418ee618201d69ecc2b0124cc24ea0b6147 diff --git a/src/Spawner/FastRetransmit.cpp b/src/Spawner/FastRetransmit.cpp index 33d43e67..600178c2 100644 --- a/src/Spawner/FastRetransmit.cpp +++ b/src/Spawner/FastRetransmit.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include bool FastRetransmit::Enabled = true; @@ -80,7 +81,7 @@ namespace if (nconn > arraySize) nconn = arraySize; for (int i = 0; i < nconn; ++i) - if (reinterpret_cast(IPXManagerClass::Instance.Connection[i]) == connection) + if (IPXManagerClass::Instance.Connection[i] == connection) return true; return false; } diff --git a/src/Spawner/PacketRedundancy.cpp b/src/Spawner/PacketRedundancy.cpp index 3b317628..ccba03db 100644 --- a/src/Spawner/PacketRedundancy.cpp +++ b/src/Spawner/PacketRedundancy.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include bool PacketRedundancy::Enabled = true; @@ -74,6 +75,8 @@ namespace 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) @@ -84,8 +87,17 @@ namespace if (nconn > arraySize) nconn = arraySize; for (int i = 0; i < nconn; ++i) - if (reinterpret_cast(IPXManagerClass::Instance.Connection[i]) == connection) - return 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; } @@ -116,8 +128,6 @@ void PacketRedundancy::NoteResend(const ConnectionClass* connection) return; } - // Shouldn't happen (connection always belongs to one of the live slots), - // but fall back to bumping everyone rather than silently dropping the signal. for (int i = 0; i < MaxPeers; ++i) BumpGauge(i); } From ea8e035a536814d13b400602644b5f689887d58e Mon Sep 17 00:00:00 2001 From: "[11EJ11]" Date: Fri, 7 Aug 2026 09:27:07 +1200 Subject: [PATCH 4/4] Update FastRetransmit hook naming and register access --- src/Spawner/FastRetransmit.cpp | 40 +++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Spawner/FastRetransmit.cpp b/src/Spawner/FastRetransmit.cpp index 600178c2..ce0dffb5 100644 --- a/src/Spawner/FastRetransmit.cpp +++ b/src/Spawner/FastRetransmit.cpp @@ -208,20 +208,22 @@ int FastRetransmit::CleanSamples() // 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, ServiceSendQueue_RTTSample_FastRetransmit, 0x8) +DEFINE_HOOK(0x48C436, ConnectionClass_ServiceSendQueue_RTTSample, 0x8) { if (FastRetransmit::Enabled) { - const auto* entry = reinterpret_cast(R->EBX()); - int delay = static_cast(R->EBP()) - entry->FirstTime; - FastRetransmit::SampleRTT(reinterpret_cast(R->EDI()), delay, entry->SendCount); + 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, IPXSetTiming_FastRetransmit, 0x8) +DEFINE_HOOK(0x540C60, IPXManagerClass_SetTiming_FastRetransmit, 0x8) { if (!FastRetransmit::Enabled) return 0; @@ -230,7 +232,7 @@ DEFINE_HOOK(0x540C60, IPXSetTiming_FastRetransmit, 0x8) if (retryDelta <= 0) return 0; - int original = R->Stack(0x4); + GET_STACK(int, original, 0x4); if (retryDelta < original) { R->Stack(0x4, retryDelta); @@ -248,21 +250,23 @@ DEFINE_HOOK(0x540C60, IPXSetTiming_FastRetransmit, 0x8) return 0; } -// ConnectionClass::Service_Send_Queue, at the per-entry retransmit decision - -// also the one place a real retransmit is detected, so it feeds -// PacketRedundancy's loss signal even when FastRetransmit/Backoff are off. -DEFINE_HOOK(0x48C4AE, ServiceSendQueue_Backoff_FastRetransmit, 0x5) +DEFINE_HOOK(0x48C4AE, ConnectionClass_ServiceSendQueue_Backoff, 0x5) { - const auto* conn = reinterpret_cast(R->EDI()); - const auto* entry = reinterpret_cast(R->ESI()); + 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 = static_cast(R->EBP()) - R->ECX(); + const int sendCount = entry->SendCount; + const int elapsed = now - lastTime; if (!FastRetransmit::Enabled || !FastRetransmit::Backoff) { if (elapsed > retryDelta) - PacketRedundancy::NoteResend(reinterpret_cast(R->EDI())); + PacketRedundancy::NoteResend(conn); return 0; } @@ -281,9 +285,9 @@ DEFINE_HOOK(0x48C4AE, ServiceSendQueue_Backoff_FastRetransmit, 0x5) int eff = static_cast(backedOff); if (elapsed > eff) - PacketRedundancy::NoteResend(reinterpret_cast(R->EDI())); + PacketRedundancy::NoteResend(conn); R->EAX(eff); - R->EDX(R->EBP()); - return 0x48C4B3; + R->EDX(now); + return Compare; }