diff --git a/Spawner.vcxproj b/Spawner.vcxproj
index 5df113d1..26dfcea9 100644
--- a/Spawner.vcxproj
+++ b/Spawner.vcxproj
@@ -53,6 +53,8 @@
+
+
@@ -84,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/FrameGate.cpp b/src/Spawner/FrameGate.cpp
new file mode 100644
index 00000000..2b3fdc8b
--- /dev/null
+++ b/src/Spawner/FrameGate.cpp
@@ -0,0 +1,202 @@
+/**
+* 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 "FrameGate.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+bool FrameGate::Enabled = true;
+
+namespace
+{
+ int SafeThrough[FrameGate::MaxPeers];
+ bool Inited = false;
+
+ int lastMaxAhead = -1;
+ int lastFSR = -1;
+ int guardUntilFrame = 0;
+
+ int lateDataLogged = 0;
+
+ void ClearWatermarks()
+ {
+ for (int i = 0; i < FrameGate::MaxPeers; ++i)
+ SafeThrough[i] = INT_MIN;
+ }
+
+ void InitOnce()
+ {
+ if (Inited)
+ return;
+ ClearWatermarks();
+ Inited = true;
+ }
+
+ void CheckEpoch(int frame, int ma, int fsr)
+ {
+ bool shrink = (lastMaxAhead >= 0 && ma < lastMaxAhead)
+ || (lastFSR >= 0 && fsr < lastFSR);
+ if (shrink)
+ {
+ ClearWatermarks();
+ guardUntilFrame = frame + lastMaxAhead + fsr + 4;
+ Debug::Log("[FrameGate] timing shrink at frame %d: MaxAhead %d->%d FSR %d->%d, guard until frame %d\n",
+ frame, lastMaxAhead, ma, lastFSR, fsr, guardUntilFrame);
+ }
+ lastMaxAhead = ma;
+ lastFSR = fsr;
+ }
+}
+
+void FrameGate::Reset()
+{
+ InitOnce();
+ ClearWatermarks();
+ lastMaxAhead = -1;
+ lastFSR = -1;
+ guardUntilFrame = 0;
+ lateDataLogged = 0;
+}
+
+// True if peer i either meets vanilla's raw command-count test, or its commands
+// through the current frame are provably all in hand per SafeThrough[i].
+bool FrameGate::AllCommandsSatisfied(TheirSync* peers, int* gapIndex)
+{
+ InitOnce();
+ *gapIndex = -1;
+
+ int nconn = static_cast(IPXManagerClass::Instance.NumConnections);
+ if (nconn < 0) nconn = 0;
+ if (nconn > MaxPeers) nconn = MaxPeers;
+
+ if (!peers)
+ peers = Peers();
+
+ const int frame = Unsorted::CurrentFrame;
+ const int ma = Game::Network::MaxAhead;
+ const int fsr = Game::Network::FrameSendRate;
+
+ CheckEpoch(frame, ma, fsr);
+ const bool relaxOK = Enabled && frame >= guardUntilFrame;
+
+ bool allSat = true;
+
+ for (int i = 0; i < nconn; ++i)
+ {
+ if (static_cast(peers[i].__recv) >= static_cast(peers[i].__send))
+ continue;
+
+ if (relaxOK && SafeThrough[i] >= frame)
+ continue;
+
+ allSat = false;
+ if (*gapIndex < 0)
+ *gapIndex = i;
+ }
+
+ return allSat;
+}
+
+// Updates SafeThrough[peer] from one received FRAMEINFO/FRAMESYNC packet's own
+// Frame + cumulative CommandCount.
+void FrameGate::OnReceive(unsigned int theirEntry, const unsigned char* evBytes)
+{
+ InitOnce();
+ if (!Enabled || !evBytes)
+ return;
+
+ // Derive the array base from the YRpp binding rather than repeating its
+ // address, so the two cannot drift apart.
+ const auto theirBase = reinterpret_cast(Peers());
+ if (theirEntry < theirBase)
+ return;
+
+ const unsigned offset = theirEntry - theirBase;
+ if ((offset % sizeof(TheirSync)) != 0)
+ return;
+
+ const unsigned idx = offset / sizeof(TheirSync);
+ if (idx >= MaxPeers)
+ return;
+
+ const EventClass* ev = reinterpret_cast(evBytes);
+ if (ev->Type != EventType::FrameInfo && ev->Type != EventType::FrameSync)
+ return;
+
+ if (Unsorted::CurrentFrame < guardUntilFrame)
+ return;
+
+ const int F = static_cast(ev->Frame);
+
+ const int C = ev->FrameInfo.CommandCount;
+ const int recv = reinterpret_cast(theirEntry)->__recv;
+
+
+ if (ev->Type == EventType::FrameInfo && F < Unsorted::CurrentFrame && lateDataLogged < 32)
+ {
+ Debug::Log("[FrameGate] late data: peer=%u stamp=%d current=%d cum=%d recv=%d safeThrough=%d\n",
+ idx, F, Unsorted::CurrentFrame, C, recv, SafeThrough[idx]);
+ ++lateDataLogged;
+ }
+
+ if (C <= recv && (F - 1) > SafeThrough[idx])
+ SafeThrough[idx] = F - 1;
+}
+
+// Replaces vanilla's command-count loop in Wait_For_Players (the raw
+// recv>=sent test) with FrameGate::AllCommandsSatisfied.
+DEFINE_HOOK(0x6495D5, WaitForPlayers_FrameAwareGate, 0x7)
+{
+ enum { Satisfied = 0x6495F9, Gapped = 0x649610 };
+
+ const GameMode gm = SessionClass::Instance.GameMode;
+ if (gm != GameMode::LAN && gm != GameMode::Internet)
+ return 0;
+ if (!FrameGate::Enabled)
+ return 0;
+
+ GET_STACK(TheirSync*, peers, 0x748);
+
+ int gap = -1;
+ if (FrameGate::AllCommandsSatisfied(peers, &gap))
+ return Satisfied;
+
+ R->ESI(gap);
+ return Gapped;
+}
+
+// Feeds every received data/framesync packet to FrameGate::OnReceive so
+// SafeThrough stays current.
+DEFINE_HOOK(0x64A3F9, ProcessReceivePacket_FrameGateRecord, 0x9)
+{
+ GET(unsigned int, theirEntry, EBP);
+ GET(const unsigned char*, evBytes, EDI);
+
+ FrameGate::OnReceive(theirEntry, evBytes);
+ return 0;
+}
diff --git a/src/Spawner/FrameGate.h b/src/Spawner/FrameGate.h
new file mode 100644
index 00000000..3d902c77
--- /dev/null
+++ b/src/Spawner/FrameGate.h
@@ -0,0 +1,64 @@
+/**
+* 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 .
+*/
+
+/**
+* FrameGate - frame-aware advance gate.
+*
+* Vanilla gates frame advance on a raw per-peer command COUNT
+* (their[i].__recv >= their[i].__send). That test carries no frame
+* information, so a single lost command packet stalls the whole lobby even
+* though the missing commands are stamped for a frame nobody has reached yet.
+*
+* This replaces the count test with the one it was approximating: we may
+* execute the current frame if, for every peer, all of that peer's commands
+* stamped for frames <= the current frame are already in hand. We prove that
+* from the packet stream: a packet stamped F carrying cumulative count C means
+* "commands 1..C are all stamped <= F"; once our __recv >= C we hold them, so
+* every frame <= F is safe with respect to that peer.
+*
+*/
+
+#pragma once
+
+#include
+
+class FrameGate
+{
+public:
+ static const int MaxPeers = 8;
+
+ static bool Enabled;
+
+ static TheirSync* Peers()
+ {
+ return TheirSync::Array;
+ }
+
+ static void Reset();
+
+ // Called in place of the vanilla command-count loop. Returns true if every
+ // peer's commands for the current frame are in hand (vanilla recv>=send OR
+ // the frame-aware relaxation). On false, *gapIndex is the first blocking
+ // peer, for the engine's existing stall bookkeeping.
+ static bool AllCommandsSatisfied(TheirSync* peers, int* gapIndex);
+
+ // Records the watermark for one received data/framesync packet.
+ // theirEntry = &their[index]; ev = the packet header.
+ static void OnReceive(unsigned int theirEntry, const unsigned char* ev);
+};
diff --git a/src/Spawner/MPStatsFix.cpp b/src/Spawner/MPStatsFix.cpp
new file mode 100644
index 00000000..1e2ab2e4
--- /dev/null
+++ b/src/Spawner/MPStatsFix.cpp
@@ -0,0 +1,67 @@
+/**
+* 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 .
+*/
+
+/**
+* MPStatsFix - correct the command-count stall counter in Wait_For_Players.
+*
+* Vanilla increments MPStats[v79].CommandCoundStalls (0x6497DC) whenever a
+* peer's command count is behind, but indexes the write with v79 - the
+* lowest-frame peer from an unrelated loop - instead of the actual culprit.
+* When there is still frame runway (the common packet-loss case: a peer's
+* commands lag but MaxAhead has not run out), v79 is forced to -1 just before
+* this write, so the increment lands on MPStats[-1].CommandCoundStalls, which
+* is exactly &ProcessingFrames (0xA8B564) - the frame-timing accumulator used
+* elsewhere for frame-rate negotiation.
+*
+*/
+
+#include
+#include
+#include
+#include "FrameGate.h"
+
+// Replaces the mis-indexed "++MPStats[v79].CommandCoundStalls" at 0x6497DC.
+// Recomputes the real culprit - the first peer whose received command count
+// is behind its sent count, instead of trusting v79 (often -1) or a clobbered register.
+// Skips the write entirely if no peer is actually behind.
+DEFINE_HOOK(0x6497DC, WaitForPlayers_CommandStallStat_Fix, 0x7)
+{
+ enum { Continue = 0x6497E3 };
+
+ int nconn = static_cast(IPXManagerClass::Instance.NumConnections);
+ if (nconn > FrameGate::MaxPeers)
+ nconn = FrameGate::MaxPeers;
+
+ const TheirSync* their = FrameGate::Peers();
+
+ int culprit = -1;
+ for (int i = 0; i < nconn; ++i)
+ {
+ if (static_cast(their[i].__recv) < static_cast(their[i].__send))
+ {
+ culprit = i;
+ break;
+ }
+ }
+
+ if (culprit >= 0)
+ ++SessionClass::Instance.MPStats[culprit].CommandCoundStalls;
+
+ return Continue;
+}
diff --git a/src/Spawner/Spawner.Config.cpp b/src/Spawner/Spawner.Config.cpp
index 205eca9d..19ada310 100644
--- a/src/Spawner/Spawner.Config.cpp
+++ b/src/Spawner/Spawner.Config.cpp
@@ -85,6 +85,7 @@ 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);
+ FrameAwareGate = pINI->ReadBool(pSettingsSection, "FrameAwareGate", FrameAwareGate);
ForceMultiplayer = pINI->ReadBool(pSettingsSection, "ForceMultiplayer", ForceMultiplayer);
}
diff --git a/src/Spawner/Spawner.Config.h b/src/Spawner/Spawner.Config.h
index fca7b2be..c1f6f092 100644
--- a/src/Spawner/Spawner.Config.h
+++ b/src/Spawner/Spawner.Config.h
@@ -123,6 +123,7 @@ class SpawnerConfig
int MaxAhead;
int PreCalcMaxAhead;
byte MaxLatencyLevel;
+ bool FrameAwareGate;
bool ForceMultiplayer;
// Tunnel Options
@@ -201,6 +202,7 @@ class SpawnerConfig
, MaxAhead { -1 }
, PreCalcMaxAhead { 0 }
, MaxLatencyLevel { 0xFF }
+ , FrameAwareGate { true }
, ForceMultiplayer { false }
// Tunnel Options
diff --git a/src/Spawner/Spawner.cpp b/src/Spawner/Spawner.cpp
index 3b549611..589f77e6 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 "FrameGate.h"
#include
#include
#include
@@ -412,6 +413,8 @@ void Spawner::InitNetwork()
Game::Network::GameStockKeepingUnit = 0x2901;
ProtocolZero::Enable = (pSpawnerConfig->Protocol == 0);
+ FrameGate::Enabled = pSpawnerConfig->FrameAwareGate;
+ FrameGate::Reset();
if (ProtocolZero::Enable)
{
Game::Network::FrameSendRate = 2;