Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Spawner.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
<ClCompile Include="$(ThisDir)\src\Misc\VideoMode.cpp" />
<!-- Spawner -->
<ClCompile Include="$(ThisDir)\src\Spawner\CustomMixes.cpp" />
<ClCompile Include="$(ThisDir)\src\Spawner\FrameGate.cpp" />
<ClCompile Include="$(ThisDir)\src\Spawner\MPStatsFix.cpp" />
<ClCompile Include="$(ThisDir)\src\Spawner\NetHack.cpp" />
<ClCompile Include="$(ThisDir)\src\Spawner\ProtocolZero.cpp" />
<ClCompile Include="$(ThisDir)\src\Spawner\ProtocolZero.Hook.cpp" />
Expand Down Expand Up @@ -84,6 +86,7 @@
<ClInclude Include="$(ThisDir)\src\Misc\Bugfixes.Desyncs.h" />
<ClInclude Include="$(ThisDir)\src\UI\Dialogs.h" />
<!-- Spawner -->
<ClInclude Include="$(ThisDir)\src\Spawner\FrameGate.h" />
<ClInclude Include="$(ThisDir)\src\Spawner\NetHack.h" />
<ClInclude Include="$(ThisDir)\src\Spawner\ProtocolZero.h" />
<ClInclude Include="$(ThisDir)\src\Spawner\ProtocolZero.LatencyLevel.h" />
Expand Down
2 changes: 1 addition & 1 deletion YRpp
Submodule YRpp updated 2 files
+76 −0 ConnectionClass.h
+16 −0 TheirSync.h
202 changes: 202 additions & 0 deletions src/Spawner/FrameGate.cpp
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#include "FrameGate.h"

#include <Helpers/Macro.h>
#include <Fundamentals.h>
#include <Unsorted.h>
#include <SessionClass.h>
#include <GeneralDefinitions.h>
#include <IPXManagerClass.h>
#include <EventClass.h>
#include <Utilities/Debug.h>

#include <climits>

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<int>(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<unsigned int>(peers[i].__recv) >= static_cast<unsigned int>(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<unsigned>(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<const EventClass*>(evBytes);
if (ev->Type != EventType::FrameInfo && ev->Type != EventType::FrameSync)
return;

if (Unsorted::CurrentFrame < guardUntilFrame)
return;

const int F = static_cast<int>(ev->Frame);

const int C = ev->FrameInfo.CommandCount;
const int recv = reinterpret_cast<const TheirSync*>(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;
}
64 changes: 64 additions & 0 deletions src/Spawner/FrameGate.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

/**
* 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 <TheirSync.h>

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);
};
67 changes: 67 additions & 0 deletions src/Spawner/MPStatsFix.cpp
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

/**
* 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 <Helpers/Macro.h>
#include <IPXManagerClass.h>
#include <SessionClass.h>
#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<int>(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<unsigned int>(their[i].__recv) < static_cast<unsigned int>(their[i].__send))
{
culprit = i;
break;
}
}

if (culprit >= 0)
++SessionClass::Instance.MPStats[culprit].CommandCoundStalls;

return Continue;
}
1 change: 1 addition & 0 deletions src/Spawner/Spawner.Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 2 additions & 0 deletions src/Spawner/Spawner.Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class SpawnerConfig
int MaxAhead;
int PreCalcMaxAhead;
byte MaxLatencyLevel;
bool FrameAwareGate;
bool ForceMultiplayer;

// Tunnel Options
Expand Down Expand Up @@ -201,6 +202,7 @@ class SpawnerConfig
, MaxAhead { -1 }
, PreCalcMaxAhead { 0 }
, MaxLatencyLevel { 0xFF }
, FrameAwareGate { true }
, ForceMultiplayer { false }

// Tunnel Options
Expand Down
3 changes: 3 additions & 0 deletions src/Spawner/Spawner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "NetHack.h"
#include "ProtocolZero.h"
#include "ProtocolZero.LatencyLevel.h"
#include "FrameGate.h"
#include <Utilities/Debug.h>
#include <Utilities/DumperTypes.h>
#include <Misc/Bugfixes.Desyncs.h>
Expand Down Expand Up @@ -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;
Expand Down