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;