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;