Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Credits
- Whole lot of desync fixes
- **[ZivDero](https://github.com/ZivDero)**
- Handicaps (difficulty & credits) support
- Multiplayer desync dialog
- **[Starkku](https://github.com/Starkku)**
- Allow customizing whether or not special house is ally to all players via spawn.ini option (#51)
- **[RAZER](https://github.com/CnCRAZER)**
Expand Down
8 changes: 8 additions & 0 deletions Spawner.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
<!-- Root -->
<ClCompile Include="src\UI\Dialogs.cpp" />
<ClCompile Include="src\UI\Hooks.cpp" />
<ClCompile Include="$(ThisDir)\src\UI\DesyncDialog.cpp" />
<ClCompile Include="$(ThisDir)\src\UI\DesyncDialog.Hook.cpp" />
<ResourceCompile Include="$(ThisDir)\src\version.rc" />
<ClCompile Include="$(ThisDir)\src\Main.cpp" />
<ClCompile Include="$(ThisDir)\src\Main.Config.cpp" />
Expand All @@ -27,6 +29,7 @@
<!-- Ext -->
<ClCompile Include="$(ThisDir)\src\Ext\Event\Body.cpp" />
<ClCompile Include="$(ThisDir)\src\Ext\INIClass\Body.cpp" />
<ClCompile Include="$(ThisDir)\src\Ext\Session\Body.cpp" />
<!-- CnCNetYR -->
<ClCompile Include="$(ThisDir)\src\CnCNetYR\AppIcon.cpp" />
<ClCompile Include="$(ThisDir)\src\CnCNetYR\Misc.cpp" />
Expand Down Expand Up @@ -68,6 +71,7 @@
<ClCompile Include="$(ThisDir)\src\Utilities\Patch.cpp" />
<!-- Dialogs -->
<ResourceCompile Include="$(ThisDir)\src\UI\MultiplayerGameOptionsDialog.rc" />
<ResourceCompile Include="$(ThisDir)\src\UI\DesyncDialog.rc" />
</ItemGroup>
<!-- Header files -->
<ItemGroup>
Expand All @@ -78,12 +82,16 @@
<!-- Ext -->
<ClInclude Include="$(ThisDir)\src\Ext\Event\Body.h" />
<ClInclude Include="$(ThisDir)\src\Ext\INIClass\Body.h" />
<ClInclude Include="$(ThisDir)\src\Ext\Session\Body.h" />
<!-- CnCNetYR -->
<ClInclude Include="$(ThisDir)\src\CnCNetYR\Ra2Mode.h" />
<!-- Misc -->
<ClInclude Include="$(ThisDir)\src\UI\Dialogs.h" />
<ClInclude Include="$(ThisDir)\src\UI\DesyncDialog.h" />
<ClInclude Include="$(ThisDir)\src\UI\DesyncDialog.Resource.h" />
<!-- Spawner -->
<ClInclude Include="$(ThisDir)\src\Spawner\NetHack.h" />
<ClInclude Include="$(ThisDir)\src\Spawner\GlobalPacketExt.h" />
<ClInclude Include="$(ThisDir)\src\Spawner\ProtocolZero.h" />
<ClInclude Include="$(ThisDir)\src\Spawner\ProtocolZero.LatencyLevel.h" />
<ClInclude Include="$(ThisDir)\src\Spawner\Spawner.Config.h" />
Expand Down
181 changes: 181 additions & 0 deletions src/Ext/Session/Body.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* yrpp-spawner
*
* Copyright(C) 2022-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 "Body.h"

#include <Spawner/GlobalPacketExt.h>

#include <SessionClass.h>
#include <HouseClass.h>
#include <IPX.h>
#include <IPXManagerClass.h>

#include <Utilities/Macro.h>

#include <cwchar>

bool SessionExt::IsOutOfSync[SessionExt::MaxPlayers] = {};

bool SessionExt::Is_Out_of_Sync(int house_id)
{
if (house_id < 0 || house_id >= MaxPlayers)
return false;

return IsOutOfSync[house_id];
}

void SessionExt::Mark_Player_As_Out_of_Sync(int house_id)
{
if (house_id < 0 || house_id >= MaxPlayers)
return;

IsOutOfSync[house_id] = true;
}

void SessionExt::Set_Master(int house_id)
{
SessionClass::Instance.MasterPlayerID() = house_id;

wchar_t* const master_name = SessionClass::Instance.MasterPlayerName();
master_name[0] = L'\0';

if (HouseClass::Array.Count > house_id && house_id >= 0)
{
if (HouseClass* house = HouseClass::Array[house_id])
{
// MasterPlayerName is wchar_t[21]; UIName is the same width.
wcsncpy(master_name, house->UIName, 20);
master_name[20] = L'\0';
}
}
}

void SessionExt::Announce_Master()
{
HouseClass* const me = HouseClass::CurrentPlayer;
if (me == nullptr)
return;

// Record ourselves as the master locally; we never receive our own packet.
Set_Master(me->ArrayIndex);

ExtGlobalPacketType packet {};
packet.Command = EXT_NET_HOST_ANNOUNCE;
packet.Heartbeat.HouseID = static_cast<char>(me->ArrayIndex);
packet.Heartbeat.IsHost = 1;

// Send to every other player. As in the engine's own beacons, index 0 is the
// local player, so start at 1. (Addresses come from the connections that were
// just created.)
auto& players = NodeNameType::Array;
for (int i = 1; i < players.Count; i++)
{
NodeNameType* const node = players[i];
if (node == nullptr)
continue;

IPXManagerClass::Instance.Send_Global_Message(
&packet, sizeof(packet), 1,
reinterpret_cast<IPXAddressClass*>(&node->Address), 0, 0);
}
IPXManagerClass::Instance.Service();
}

void SessionExt::Update_Master_After_Player_Removal()
{
// Mirrors Vinifera: decide the master from the connected-players list
// (NodeNameType::Array == SessionClass::Players, 0xA8DA74), which
// Destroy_Connection shrinks immediately - even while the desync dialog has
// game logic suspended. We must NOT use the houses' Defeated flag here: a
// departed house is only flagged defeated when its queued E_REMOVEPLAYER
// event runs, which never happens while suspended, so the old master would
// keep looking valid and never get replaced. node->HouseIndex is the house
// id (the same value Set_Master/MasterPlayerID use).
const int current = SessionClass::Instance.MasterPlayerID();
auto& players = NodeNameType::Array;

// If the current master is still connected, there is nothing to do.
if (current != -1)
{
for (int i = 0; i < players.Count; i++)
{
NodeNameType* const node = players[i];
if (node != nullptr && node->HouseIndex == current)
return;
}
}

// Otherwise promote the connected player with the lowest house id. This is
// deterministic, so every remaining client agrees without negotiation.
int new_master = -1;
for (int i = 0; i < players.Count; i++)
{
NodeNameType* const node = players[i];
if (node == nullptr)
continue;

const int id = node->HouseIndex;
if (id >= 0 && (new_master == -1 || id < new_master))
new_master = id;
}

if (new_master != -1 && new_master != current)
Set_Master(new_master);
}

/**
* Replacement for SessionClass::Am_I_Master (0x697E70).
*
* The vanilla implementation only consults MasterPlayerID/MasterPlayerName in
* GameMode::Internet (WOL) sessions, falling back to "the first non-defeated
* human house is the master" otherwise. Spawner multiplayer games run as
* GameMode::LAN, so the host/master we record through Set_Master (from the
* EXT_NET_HOST_ANNOUNCE at game start) would be ignored. Extend the check to LAN
* so the announced master is honoured - including after host migration. Mirrors
* Vinifera's SessionClassExt::_Am_I_Master.
*
* __fastcall(ECX=this, EDX unused, stack: who) reproduces the original __thiscall
* (retn 4); the whole function is replaced via DEFINE_FUNCTION_JUMP below.
*/
static bool __fastcall SessionClass_Am_I_Master(SessionClass* pThis, void*, HouseClass* who)
{
if (who == nullptr)
who = HouseClass::CurrentPlayer;

if ((pThis->GameMode == GameMode::Internet || pThis->GameMode == GameMode::LAN) && who != nullptr)
{
const int master = pThis->MasterPlayerID();
if (master != -1)
return who->ArrayIndex == master;

if (_wcsicmp(who->UIName, pThis->MasterPlayerName()) == 0)
return true;
}

// Fallback: the first non-defeated human house is the master.
for (int i = 0; i < HouseClass::Array.Count; i++)
{
HouseClass* const house = HouseClass::Array[i];
if (house && house->IsHumanPlayer && !house->Defeated)
return who == house;
}

return false;
}
DEFINE_FUNCTION_JUMP(LJMP, 0x697E70, SessionClass_Am_I_Master)
56 changes: 56 additions & 0 deletions src/Ext/Session/Body.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* yrpp-spawner
*
* Copyright(C) 2022-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/>.
*/

// Re-created bits of Vinifera's SessionClassExtension that the desync dialog
// needs. YR's SessionClass has no equivalent of these (it only has a single
// global OutOfSync bool and no per-player tracking), so this holds the new
// state spawner-side. The game's native master/host fields (MasterPlayerID /
// MasterPlayerName) DO exist and are written through here.

#pragma once

class HouseClass;

namespace SessionExt
{
// The engine supports up to 8 multiplayer houses.
constexpr int MaxPlayers = 8;

// --- Per-player out-of-sync tracking (the engine only has one global flag).
// Set by the desync-detection hook and read by the dialog to colour each
// player's status.
extern bool IsOutOfSync[MaxPlayers];

bool Is_Out_of_Sync(int house_id);
void Mark_Player_As_Out_of_Sync(int house_id);

// Assigns the game master/host, writing the engine's native MasterPlayerID
// and MasterPlayerName so SessionClass::Am_I_Master() agrees.
void Set_Master(int house_id);

// Called on the host at game start: records itself as the master and tells
// every other player who the host is (EXT_NET_HOST_ANNOUNCE), so MasterPlayerID
// is authoritative on all machines before any desync. Mirrors Vinifera's
// SessionClassExtension::Announce_Master.
void Announce_Master();

// Recomputes the master after a player has been removed: if the current
// master is gone, promotes the first remaining non-defeated human house.
void Update_Master_After_Player_Removal();
}
76 changes: 76 additions & 0 deletions src/Spawner/GlobalPacketExt.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* yrpp-spawner
*
* Copyright(C) 2022-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/>.
*/

// New, spawner-added global network packet types.
//
// This mirrors Vinifera's ExtGlobalPacketType: the engine's out-of-band global
// packet (GlobalPacketType) is 0x1C7 = 455 bytes, and the IPX layer accepts a
// raw buffer of that size, so an alternative struct of the same size can ride
// the very same channel. Command sits at offset 0 (matching GlobalPacketType),
// which is what Network_Call_Back's dispatch switch reads; commands the engine
// does not know fall through to its `default` case, which is where the receiver
// for these is meant to be hooked in.

#pragma once

// Command values for the spawner's own global packets.
//
// The engine's native command values run up to ~0x2F. These are placed well
// above that range so they never collide and always reach the unknown-command
// (default) dispatch path.
enum ExtNetCommandType : int
{
EXT_NET_DESYNC_HEARTBEAT = 0xE0, // Periodic keep-alive while the desync dialog is open; also detects departures.
EXT_NET_DESYNC_CONTINUE = 0xE1, // The host's decision to continue the game without the desynced players.
EXT_NET_DESYNC_CHAT = 0xE2, // A chat line typed in the desync dialog while game logic is halted.
EXT_NET_HOST_ANNOUNCE = 0xE3, // The host announcing itself at game start so everyone records the master.
};

#pragma pack(push, 1)
struct ExtGlobalPacketType
{
// Must alias GlobalPacketType::Command (offset 0) so the engine dispatch
// reads it correctly.
int Command;

// Sender's display name (ANSI; converted from the wide UIName on send).
char Name[32];

union
{
struct
{
char HouseID; // Sender's house (ArrayIndex).
char IsHost; // Non-zero if the sender is the game master.
} Heartbeat;

struct
{
char SenderHouseID;
char Text[200]; // ANSI chat text.
} Chat;

// Forces the whole struct to the engine's GlobalPacketType size so the
// IPX layer treats it identically.
char _padding[455 - sizeof(int) - 32];
};
};
#pragma pack(pop)

static_assert(sizeof(ExtGlobalPacketType) == 455, "ExtGlobalPacketType must match the engine's GlobalPacketType (0x1C7 bytes)");
1 change: 1 addition & 0 deletions src/Spawner/Spawner.Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ void SpawnerConfig::LoadFromINIFile(CCINIClass* pINI)
PreCalcMaxAhead = pINI->ReadInteger(pSettingsSection, "PreCalcMaxAhead", PreCalcMaxAhead);
MaxLatencyLevel = (byte)pINI->ReadInteger(pSettingsSection, "MaxLatencyLevel", (int)MaxLatencyLevel);
ForceMultiplayer = pINI->ReadBool(pSettingsSection, "ForceMultiplayer", ForceMultiplayer);
Host = pINI->ReadBool(pSettingsSection, "Host", Host);
}

{ // Tunnel Options
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 @@ -124,6 +124,7 @@ class SpawnerConfig
int PreCalcMaxAhead;
byte MaxLatencyLevel;
bool ForceMultiplayer;
bool Host; // True on the machine hosting the game; announces itself as the game master at start.

// Tunnel Options
int TunnelId;
Expand Down Expand Up @@ -202,6 +203,7 @@ class SpawnerConfig
, PreCalcMaxAhead { 0 }
, MaxLatencyLevel { 0xFF }
, ForceMultiplayer { false }
, Host { false }

// Tunnel Options
, TunnelId { 0 }
Expand Down
Loading
Loading