iw4x-client/src/Components/Modules/Party.cpp

567 lines
16 KiB
C++
Raw Normal View History

2022-02-27 07:53:44 -05:00
#include <STDInclude.hpp>
#include <Utils/InfoString.hpp>
#include "Download.hpp"
#include "Gamepad.hpp"
#include "Party.hpp"
#include "ServerList.hpp"
#include "Stats.hpp"
#include "Voice.hpp"
2017-01-19 16:23:59 -05:00
#include <version.hpp>
2017-01-19 16:23:59 -05:00
namespace Components
{
class JoinContainer
{
public:
Network::Address target;
std::string challenge;
std::string motd;
DWORD joinTime;
bool valid;
int matchType;
Utils::InfoString info;
// Party-specific stuff
DWORD requestTime;
bool awaitingPlaylist;
};
static JoinContainer Container;
std::map<std::uint64_t, Network::Address> Party::LobbyMap;
2017-01-19 16:23:59 -05:00
2022-05-20 18:12:46 -04:00
Dvar::Var Party::PartyEnable;
2017-01-19 16:23:59 -05:00
SteamID Party::GenerateLobbyId()
{
SteamID id;
id.accountID = Game::Sys_Milliseconds();
id.universe = 1;
id.accountType = 8;
id.accountInstance = 0x40000;
2017-01-19 16:23:59 -05:00
return id;
}
Network::Address Party::Target()
{
return Container.target;
2017-01-19 16:23:59 -05:00
}
void Party::Connect(Network::Address target)
{
2018-10-09 04:53:15 -04:00
Node::Add(target);
2017-01-19 16:23:59 -05:00
Container.valid = true;
Container.awaitingPlaylist = false;
Container.joinTime = Game::Sys_Milliseconds();
Container.target = target;
Container.challenge = Utils::Cryptography::Rand::GenerateChallenge();
2017-01-19 16:23:59 -05:00
Network::SendCommand(Container.target, "getinfo", Container.challenge);
2017-01-19 16:23:59 -05:00
Command::Execute("openmenu popup_reconnectingtoparty");
}
2018-12-17 08:29:18 -05:00
const char* Party::GetLobbyInfo(SteamID lobby, const std::string& key)
2017-01-19 16:23:59 -05:00
{
if (LobbyMap.contains(lobby.bits))
2017-01-19 16:23:59 -05:00
{
Network::Address address = LobbyMap[lobby.bits];
2017-01-19 16:23:59 -05:00
if (key == "addr"s)
2017-01-19 16:23:59 -05:00
{
return Utils::String::VA("%d", address.getIP().full);
}
if (key == "port"s)
2017-01-19 16:23:59 -05:00
{
return Utils::String::VA("%d", address.getPort());
}
}
return "212";
}
void Party::RemoveLobby(SteamID lobby)
{
LobbyMap.erase(lobby.bits);
2017-01-19 16:23:59 -05:00
}
2018-12-17 08:29:18 -05:00
void Party::ConnectError(const std::string& message)
2017-01-19 16:23:59 -05:00
{
Command::Execute("closemenu popup_reconnectingtoparty");
Dvar::Var("partyend_reason").set(message);
Command::Execute("openmenu menu_xboxlive_partyended");
}
std::string Party::GetMotd()
{
return Container.motd;
2017-01-19 16:23:59 -05:00
}
2023-02-08 14:57:27 -05:00
std::string Party::GetHostName()
{
return Container.info.get("hostname");
}
int Party::GetMaxClients()
{
const auto value = Container.info.get("sv_maxclients");
return std::strtol(value.data(), nullptr, 10);
}
2017-01-19 16:23:59 -05:00
bool Party::PlaylistAwaiting()
{
return Container.awaitingPlaylist;
2017-01-19 16:23:59 -05:00
}
void Party::PlaylistContinue()
{
Dvar::Var("xblive_privateserver").set(false);
// Ensure we can join
*Game::g_lobbyCreateInProgress = false;
Container.awaitingPlaylist = false;
2017-01-19 16:23:59 -05:00
SteamID id = GenerateLobbyId();
2017-01-19 16:23:59 -05:00
// Temporary workaround
// TODO: Patch the 127.0.0.1 -> loopback mapping in the party code
if (Container.target.isLoopback())
2017-01-19 16:23:59 -05:00
{
if (*Game::numIP)
{
Container.target.setIP(*Game::localIP);
Container.target.setType(Game::netadrtype_t::NA_IP);
2017-01-19 16:23:59 -05:00
Logger::Print("Trying to connect to party with loopback address, using a local ip instead: {}\n", Container.target.getString());
2017-01-19 16:23:59 -05:00
}
else
{
Logger::Print("Trying to connect to party with loopback address, but no local ip was found.\n");
}
}
LobbyMap[id.bits] = Container.target;
2017-01-19 16:23:59 -05:00
Game::Steam_JoinLobby(id, 0);
}
2018-12-17 08:29:18 -05:00
void Party::PlaylistError(const std::string& error)
2017-01-19 16:23:59 -05:00
{
Container.valid = false;
Container.awaitingPlaylist = false;
2017-01-19 16:23:59 -05:00
ConnectError(error);
2017-01-19 16:23:59 -05:00
}
DWORD Party::UIDvarIntStub(char* dvar)
{
2017-03-01 13:56:36 -05:00
if (!_stricmp(dvar, "onlinegame") && !Stats::IsMaxLevel())
2017-01-19 16:23:59 -05:00
{
return 0x649E660;
}
return Utils::Hook::Call<DWORD(char*)>(0x4D5390)(dvar);
}
bool Party::IsInLobby()
{
return (!Dedicated::IsRunning() && PartyEnable.get<bool>() && Dvar::Var("party_host").get<bool>());
}
bool Party::IsInUserMapLobby()
{
2023-02-06 14:34:08 -05:00
return (IsInLobby() && Maps::IsUserMap((*Game::ui_mapname)->current.string));
}
2022-05-20 18:12:46 -04:00
bool Party::IsEnabled()
{
return PartyEnable.get<bool>();
}
2017-01-19 16:23:59 -05:00
Party::Party()
{
PartyEnable = Dvar::Register<bool>("party_enable", Dedicated::IsEnabled(), Game::DVAR_NONE, "Enable party system");
Dvar::Register<bool>("xblive_privatematch", true, Game::DVAR_INIT, "");
2017-01-19 16:23:59 -05:00
// various changes to SV_DirectConnect-y stuff to allow non-party joinees
Utils::Hook::Set<WORD>(0x460D96, 0x90E9);
Utils::Hook::Set<BYTE>(0x460F0A, 0xEB);
Utils::Hook::Set<BYTE>(0x401CA4, 0xEB);
Utils::Hook::Set<BYTE>(0x401C15, 0xEB);
// disable configstring checksum matching (it's unreliable at most)
Utils::Hook::Set<BYTE>(0x4A75A7, 0xEB); // SV_SpawnServer
Utils::Hook::Set<BYTE>(0x5AC2CF, 0xEB); // CL_ParseGamestate
Utils::Hook::Set<BYTE>(0x5AC2C3, 0xEB); // CL_ParseGamestate
2017-06-22 04:35:45 -04:00
// AnonymousAddRequest
2017-01-19 16:23:59 -05:00
Utils::Hook::Set<BYTE>(0x5B5E18, 0xEB);
Utils::Hook::Set<BYTE>(0x5B5E64, 0xEB);
Utils::Hook::Nop(0x5B5E5C, 2);
// HandleClientHandshake
Utils::Hook::Set<BYTE>(0x5B6EA5, 0xEB);
Utils::Hook::Set<BYTE>(0x5B6EF3, 0xEB);
Utils::Hook::Nop(0x5B6EEB, 2);
// Allow local connections
Utils::Hook::Set<BYTE>(0x4D43DA, 0xEB);
// LobbyID mismatch
Utils::Hook::Nop(0x4E50D6, 2);
Utils::Hook::Set<BYTE>(0x4E50DA, 0xEB);
// causes 'does current Steam lobby match' calls in Steam_JoinLobby to be ignored
Utils::Hook::Set<BYTE>(0x49D007, 0xEB);
2023-02-20 07:21:07 -05:00
// function checking party heartbeat timeouts, cause random issues
Utils::Hook::Nop(0x4E532D, 5); // PartyHost_TimeoutMembers
2017-01-19 16:23:59 -05:00
// Steam_JoinLobby call causes migration
Utils::Hook::Nop(0x5AF851, 5);
Utils::Hook::Set<BYTE>(0x5AF85B, 0xEB);
// Allow xpartygo in public lobbies
Utils::Hook::Set<BYTE>(0x5A969E, 0xEB);
Utils::Hook::Nop(0x5A96BE, 2);
// Always open lobby menu when connecting
// It's not possible to entirely patch it via code
//Utils::Hook::Set<BYTE>(0x5B1698, 0xEB);
//Utils::Hook::Nop(0x5029F2, 6);
//Utils::Hook::SetString(0x70573C, "menu_xboxlive_lobby");
// Disallow selecting team in private match
//Utils::Hook::Nop(0x5B2BD8, 6);
// Force teams, even if not private match
Utils::Hook::Set<BYTE>(0x487BB2, 0xEB);
// Force xblive_privatematch 0 and rename it
//Utils::Hook::Set<BYTE>(0x420A6A, 4);
Utils::Hook::Set<BYTE>(0x420A6C, 0);
2020-12-09 14:13:34 -05:00
Utils::Hook::Set<const char*>(0x420A6E, "xblive_privateserver");
2017-01-19 16:23:59 -05:00
// Remove migration shutdown, it causes crashes and will be destroyed when erroring anyways
Utils::Hook::Nop(0x5A8E1C, 12);
Utils::Hook::Nop(0x5A8E33, 11);
// Enable XP Bar
Utils::Hook(0x62A2A7, UIDvarIntStub, HOOK_CALL).install()->quick();
2017-01-19 16:23:59 -05:00
// Set NAT to open
Utils::Hook::Set<int>(0x79D898, 1);
// Disable host migration
Utils::Hook::Set<BYTE>(0x5B58B2, 0xEB);
Utils::Hook::Set<BYTE>(0x4D6171, 0);
2017-02-01 22:20:53 -05:00
Utils::Hook::Nop(0x4077A1, 5); // PartyMigrate_Frame
2017-01-19 16:23:59 -05:00
2017-06-22 04:35:45 -04:00
// Patch playlist stuff for non-party behavior
static Game::dvar_t* partyEnable = PartyEnable.get<Game::dvar_t*>();
2017-01-19 16:23:59 -05:00
Utils::Hook::Set<Game::dvar_t**>(0x4A4093, &partyEnable);
Utils::Hook::Set<Game::dvar_t**>(0x4573F1, &partyEnable);
Utils::Hook::Set<Game::dvar_t**>(0x5B1A0C, &partyEnable);
// Invert corresponding jumps
Utils::Hook::Xor<BYTE>(0x4A409B, 1);
Utils::Hook::Xor<BYTE>(0x4573FA, 1);
Utils::Hook::Xor<BYTE>(0x5B1A17, 1);
// Set ui_maxclients to sv_maxclients
2020-12-09 14:13:34 -05:00
Utils::Hook::Set<const char*>(0x42618F, "sv_maxclients");
Utils::Hook::Set<const char*>(0x4D3756, "sv_maxclients");
Utils::Hook::Set<const char*>(0x5E3772, "sv_maxclients");
2017-01-19 16:23:59 -05:00
// Unlatch maxclient dvars
Utils::Hook::Xor<BYTE>(0x426187, Game::DVAR_LATCH);
Utils::Hook::Xor<BYTE>(0x4D374E, Game::DVAR_LATCH);
Utils::Hook::Xor<BYTE>(0x5E376A, Game::DVAR_LATCH);
Utils::Hook::Xor<DWORD>(0x4261A1, Game::DVAR_LATCH);
Utils::Hook::Xor<DWORD>(0x4D376D, Game::DVAR_LATCH);
Utils::Hook::Xor<DWORD>(0x5E3789, Game::DVAR_LATCH);
2017-01-19 16:23:59 -05:00
Command::Add("connect", [](Command::Params* params)
2017-01-19 16:23:59 -05:00
{
2022-03-17 14:50:20 -04:00
if (params->size() < 2)
2017-01-19 16:23:59 -05:00
{
return;
}
if (Game::CL_IsCgameInitialized())
2017-05-29 14:43:03 -04:00
{
Command::Execute("disconnect", false);
Command::Execute(Utils::String::VA("%s", params->join(0).data()), false);
}
else
{
Connect(Network::Address(params->get(1)));
2017-05-29 14:43:03 -04:00
}
2017-01-19 16:23:59 -05:00
});
Command::Add("reconnect", [](Command::Params*)
2017-01-19 16:23:59 -05:00
{
Connect(Container.target);
2017-01-19 16:23:59 -05:00
});
2022-06-16 10:15:26 -04:00
if (!Dedicated::IsEnabled() && !ZoneBuilder::IsEnabled())
2017-01-19 16:23:59 -05:00
{
2022-06-16 10:15:26 -04:00
Scheduler::Loop([]
2017-01-19 16:23:59 -05:00
{
if (Container.valid)
2017-01-19 16:23:59 -05:00
{
if ((Game::Sys_Milliseconds() - Container.joinTime) > 10'000)
2022-06-16 10:15:26 -04:00
{
Container.valid = false;
ConnectError("Server connection timed out.");
2022-06-16 10:15:26 -04:00
}
2017-01-19 16:23:59 -05:00
}
if (Container.awaitingPlaylist)
2017-01-19 16:23:59 -05:00
{
if ((Game::Sys_Milliseconds() - Container.requestTime) > 5'000)
2022-06-16 10:15:26 -04:00
{
Container.awaitingPlaylist = false;
ConnectError("Playlist request timed out.");
2022-06-16 10:15:26 -04:00
}
2017-01-19 16:23:59 -05:00
}
2022-06-16 10:15:26 -04:00
}, Scheduler::Pipeline::CLIENT);
}
2017-01-19 16:23:59 -05:00
// Basic info handler
2022-09-24 13:04:37 -04:00
Network::OnClientPacket("getInfo", [](const Network::Address& address, [[maybe_unused]] const std::string& data)
2017-01-19 16:23:59 -05:00
{
auto botCount = 0;
auto clientCount = 0;
auto maxClientCount = *Game::svs_clientCount;
const auto securityLevel = Dvar::Var("sv_securityLevel").get<int>();
const auto* password = *Game::g_password ? (*Game::g_password)->current.string : "";
2017-01-19 16:23:59 -05:00
if (maxClientCount)
2017-01-19 16:23:59 -05:00
{
for (int i = 0; i < maxClientCount; ++i)
2017-01-19 16:23:59 -05:00
{
2022-08-20 06:09:41 -04:00
if (Game::svs_clients[i].header.state >= Game::CS_CONNECTED)
2017-01-19 16:23:59 -05:00
{
if (Game::svs_clients[i].bIsTestClient) ++botCount;
2017-06-12 15:01:56 -04:00
else ++clientCount;
2017-01-19 16:23:59 -05:00
}
}
}
else
{
maxClientCount = *Game::party_maxplayers ? (*Game::party_maxplayers)->current.integer : 18;
clientCount = Game::PartyHost_CountMembers(Game::g_lobbyData);
2017-01-19 16:23:59 -05:00
}
Utils::InfoString info;
info.set("challenge", Utils::ParseChallenge(data));
info.set("gamename", "IW4");
info.set("hostname", (*Game::sv_hostname)->current.string);
info.set("gametype", (*Game::sv_gametype)->current.string);
info.set("fs_game", (*Game::fs_gameDirVar)->current.string);
info.set("xuid", Utils::String::VA("%llX", Steam::SteamUser()->GetSteamID().bits));
info.set("clients", std::to_string(clientCount));
info.set("bots", std::to_string(botCount));
info.set("sv_maxclients", std::to_string(maxClientCount));
info.set("protocol", std::to_string(PROTOCOL));
2017-01-19 16:23:59 -05:00
info.set("shortversion", SHORTVERSION);
info.set("checksum", std::to_string(Game::Sys_Milliseconds()));
info.set("mapname", Dvar::Var("mapname").get<std::string>());
2022-12-25 12:23:53 -05:00
info.set("isPrivate", *password ? "1" : "0");
2017-01-19 16:23:59 -05:00
info.set("hc", (Dvar::Var("g_hardcore").get<bool>() ? "1" : "0"));
info.set("securityLevel", std::to_string(securityLevel));
info.set("sv_running", (Dedicated::IsRunning() ? "1" : "0"));
info.set("aimAssist", (Gamepad::sv_allowAimAssist.get<bool>() ? "1" : "0"));
info.set("voiceChat", (Voice::SV_VoiceEnabled() ? "1" : "0"));
2017-01-19 16:23:59 -05:00
// Ensure mapname is set
if (info.get("mapname").empty() || IsInLobby())
{
info.set("mapname", Dvar::Var("ui_mapname").get<const char*>());
}
2017-04-06 16:22:47 -04:00
if (Maps::GetUserMap()->isValid())
{
info.set("usermaphash", Utils::String::VA("%i", Maps::GetUserMap()->getHash()));
}
else if (IsInUserMapLobby())
2017-01-19 16:23:59 -05:00
{
info.set("usermaphash", Utils::String::VA("%i", Maps::GetUsermapHash(info.get("mapname"))));
2017-01-19 16:23:59 -05:00
}
if (Dedicated::IsEnabled())
2017-01-19 16:23:59 -05:00
{
info.set("sv_motd", Dedicated::SVMOTD.get<std::string>());
2017-01-19 16:23:59 -05:00
}
// Set matchtype
// 0 - No match, connecting not possible
// 1 - Party, use Steam_JoinLobby to connect
// 2 - Match, use CL_ConnectFromParty to connect
2022-05-20 18:12:46 -04:00
if (PartyEnable.get<bool>() && Dvar::Var("party_host").get<bool>()) // Party hosting
2017-01-19 16:23:59 -05:00
{
info.set("matchtype", "1");
}
else if (Dvar::Var("sv_running").get<bool>()) // Match hosting
{
info.set("matchtype", "2");
}
else
{
info.set("matchtype", "0");
}
info.set("wwwDownload", (Download::SV_wwwDownload.get<bool>() ? "1" : "0"));
info.set("wwwUrl", Download::SV_wwwBaseUrl.get<std::string>());
Network::SendCommand(address, "infoResponse", "\\" + info.build());
2017-01-19 16:23:59 -05:00
});
2022-08-19 19:10:35 -04:00
Network::OnClientPacket("infoResponse", [](const Network::Address& address, [[maybe_unused]] const std::string& data)
2017-01-19 16:23:59 -05:00
{
const Utils::InfoString info(data);
2017-01-19 16:23:59 -05:00
// Handle connection
if (Container.valid)
{
if (Container.target == address)
{
2023-01-01 06:47:50 -05:00
// Invalidate handler for future packets
Container.valid = false;
Container.info = info;
Container.matchType = atoi(info.get("matchtype").data());
2023-01-01 06:47:50 -05:00
auto securityLevel = static_cast<std::uint32_t>(atoi(info.get("securityLevel").data()));
bool isUsermap = !info.get("usermaphash").empty();
auto usermapHash = static_cast<std::uint32_t>(atoi(info.get("usermaphash").data()));
2023-01-01 06:47:50 -05:00
std::string mod = (*Game::fs_gameDirVar)->current.string;
2017-01-19 16:23:59 -05:00
2023-01-01 06:47:50 -05:00
// set fast server stuff here so its updated when we go to download stuff
if (info.get("wwwDownload") == "1"s)
{
Download::SV_wwwDownload.set(true);
Download::SV_wwwBaseUrl.set(info.get("wwwUrl"));
}
else
{
Download::SV_wwwDownload.set(false);
Download::SV_wwwBaseUrl.set("");
}
if (info.get("challenge") != Container.challenge)
2023-01-01 06:47:50 -05:00
{
ConnectError("Invalid join response: Challenge mismatch.");
2023-01-01 06:47:50 -05:00
}
else if (securityLevel > Auth::GetSecurityLevel())
{
Command::Execute("closemenu popup_reconnectingtoparty");
Auth::IncreaseSecurityLevel(securityLevel, "reconnect");
}
else if (!Container.matchType)
2023-01-01 06:47:50 -05:00
{
ConnectError("Server is not hosting a match.");
2023-01-01 06:47:50 -05:00
}
else if (Container.matchType > 2 || Container.matchType < 0)
2023-01-01 06:47:50 -05:00
{
ConnectError("Invalid join response: Unknown matchtype");
2023-01-01 06:47:50 -05:00
}
else if (Container.info.get("mapname").empty() || Container.info.get("gametype").empty())
2023-01-01 06:47:50 -05:00
{
ConnectError("Invalid map or gametype.");
2023-01-01 06:47:50 -05:00
}
else if (Container.info.get("isPrivate") == "1"s && !Dvar::Var("password").get<std::string>().length())
2023-01-01 06:47:50 -05:00
{
ConnectError("A password is required to join this server! Set it at the bottom of the serverlist.");
2023-01-01 06:47:50 -05:00
}
else if (isUsermap && usermapHash != Maps::GetUsermapHash(info.get("mapname")))
{
Command::Execute("closemenu popup_reconnectingtoparty");
Download::InitiateMapDownload(info.get("mapname"), info.get("isPrivate") == "1");
}
else if (!info.get("fs_game").empty() && Utils::String::ToLower(mod) != Utils::String::ToLower(info.get("fs_game")))
{
Command::Execute("closemenu popup_reconnectingtoparty");
Download::InitiateClientDownload(info.get("fs_game"), info.get("isPrivate") == "1"s);
}
2023-01-05 04:59:09 -05:00
else if ((*Game::fs_gameDirVar)->current.string[0] != '\0' && info.get("fs_game").empty())
2023-01-01 06:47:50 -05:00
{
Game::Dvar_SetString(*Game::fs_gameDirVar, "");
2023-01-01 06:47:50 -05:00
if (Dvar::Var("cl_modVidRestart").get<bool>())
{
Command::Execute("vid_restart", false);
}
2023-01-01 06:47:50 -05:00
Command::Execute("reconnect", false);
}
else
{
if (!Maps::CheckMapInstalled(Container.info.get("mapname"), true)) return;
2023-01-01 06:47:50 -05:00
Container.motd = info.get("sv_motd");
2023-01-01 06:47:50 -05:00
if (Container.matchType == 1) // Party
2023-01-01 06:47:50 -05:00
{
// Send playlist request
Container.requestTime = Game::Sys_Milliseconds();
Container.awaitingPlaylist = true;
Network::SendCommand(Container.target, "getplaylist", Dvar::Var("password").get<std::string>());
2023-01-01 06:47:50 -05:00
// This is not a safe method
// TODO: Fix actual error!
if (Game::CL_IsCgameInitialized())
{
Command::Execute("disconnect", true);
}
}
else if (Container.matchType == 2) // Match
2023-01-01 06:47:50 -05:00
{
int clients;
int maxClients;
try
{
clients = std::stoi(Container.info.get("clients"));
maxClients = std::stoi(Container.info.get("sv_maxclients"));
}
catch ([[maybe_unused]] const std::exception& ex)
{
ConnectError("Invalid info string");
return;
}
if (clients >= maxClients)
{
Party::ConnectError("@EXE_SERVERISFULL");
}
else
{
Dvar::Var("xblive_privateserver").set(true);
Game::Menus_CloseAll(Game::uiContext);
Game::_XSESSION_INFO hostInfo;
Game::CL_ConnectFromParty(0, &hostInfo, *Container.target.get(), 0, 0, Container.info.get("mapname").data(), Container.info.get("gametype").data());
2023-01-01 06:47:50 -05:00
}
}
}
}
}
2023-01-01 06:47:50 -05:00
ServerList::Insert(address, info);
Friends::UpdateServer(address, info.get("hostname"), info.get("mapname"));
2017-01-19 16:23:59 -05:00
});
}
}