#include namespace Components { Party::JoinContainer Party::Container; std::map Party::LobbyMap; Dvar::Var Party::PartyEnable; SteamID Party::GenerateLobbyId() { SteamID id; id.accountID = Game::Sys_Milliseconds(); id.universe = 1; id.accountType = 8; id.accountInstance = 0x40000; return id; } Network::Address Party::Target() { return Party::Container.target; } void Party::Connect(Network::Address target) { Node::Add(target); Party::Container.valid = true; Party::Container.awaitingPlaylist = false; Party::Container.joinTime = Game::Sys_Milliseconds(); Party::Container.target = target; Party::Container.challenge = Utils::Cryptography::Rand::GenerateChallenge(); Network::SendCommand(Party::Container.target, "getinfo", Party::Container.challenge); Command::Execute("openmenu popup_reconnectingtoparty"); } const char* Party::GetLobbyInfo(SteamID lobby, const std::string& key) { if (Party::LobbyMap.contains(lobby.bits)) { Network::Address address = Party::LobbyMap[lobby.bits]; if (key == "addr") { return Utils::String::VA("%d", address.getIP().full); } else if (key == "port") { return Utils::String::VA("%d", address.getPort()); } } return "212"; } void Party::RemoveLobby(SteamID lobby) { Party::LobbyMap.erase(lobby.bits); } void Party::ConnectError(const std::string& message) { Command::Execute("closemenu popup_reconnectingtoparty"); Dvar::Var("partyend_reason").set(message); Command::Execute("openmenu menu_xboxlive_partyended"); } std::string Party::GetMotd() { return Party::Container.motd; } Game::dvar_t* Party::RegisterMinPlayers(const char* name, int /*value*/, int /*min*/, int max, Game::DvarFlags flag, const char* description) { return Dvar::Register(name, 1, 1, max, Game::DVAR_INIT | flag, description).get(); } bool Party::PlaylistAwaiting() { return Party::Container.awaitingPlaylist; } void Party::PlaylistContinue() { Dvar::Var("xblive_privateserver").set(false); // Ensure we can join *Game::g_lobbyCreateInProgress = false; Party::Container.awaitingPlaylist = false; SteamID id = Party::GenerateLobbyId(); // Temporary workaround // TODO: Patch the 127.0.0.1 -> loopback mapping in the party code if (Party::Container.target.isLoopback()) { if (*Game::numIP) { Party::Container.target.setIP(*Game::localIP); Party::Container.target.setType(Game::netadrtype_t::NA_IP); Logger::Print("Trying to connect to party with loopback address, using a local ip instead: {}\n", Party::Container.target.getString()); } else { Logger::Print("Trying to connect to party with loopback address, but no local ip was found.\n"); } } Party::LobbyMap[id.bits] = Party::Container.target; Game::Steam_JoinLobby(id, 0); } void Party::PlaylistError(const std::string& error) { Party::Container.valid = false; Party::Container.awaitingPlaylist = false; Party::ConnectError(error); } DWORD Party::UIDvarIntStub(char* dvar) { if (!_stricmp(dvar, "onlinegame") && !Stats::IsMaxLevel()) { return 0x649E660; } return Utils::Hook::Call(0x4D5390)(dvar); } bool Party::IsInLobby() { return (!(*Game::com_sv_running)->current.enabled && PartyEnable.get() && Dvar::Var("party_host").get()); } bool Party::IsInUserMapLobby() { return (Party::IsInLobby() && Maps::IsUserMap(Dvar::Var("ui_mapname").get())); } bool Party::IsEnabled() { return PartyEnable.get(); } Party::Party() { Party::PartyEnable = Dvar::Register("party_enable", Dedicated::IsEnabled(), Game::DVAR_NONE, "Enable party system"); Dvar::Register("xblive_privatematch", true, Game::DVAR_INIT, ""); // various changes to SV_DirectConnect-y stuff to allow non-party joinees Utils::Hook::Set(0x460D96, 0x90E9); Utils::Hook::Set(0x460F0A, 0xEB); Utils::Hook::Set(0x401CA4, 0xEB); Utils::Hook::Set(0x401C15, 0xEB); // disable configstring checksum matching (it's unreliable at most) Utils::Hook::Set(0x4A75A7, 0xEB); // SV_SpawnServer Utils::Hook::Set(0x5AC2CF, 0xEB); // CL_ParseGamestate Utils::Hook::Set(0x5AC2C3, 0xEB); // CL_ParseGamestate // AnonymousAddRequest Utils::Hook::Set(0x5B5E18, 0xEB); Utils::Hook::Set(0x5B5E64, 0xEB); Utils::Hook::Nop(0x5B5E5C, 2); // HandleClientHandshake Utils::Hook::Set(0x5B6EA5, 0xEB); Utils::Hook::Set(0x5B6EF3, 0xEB); Utils::Hook::Nop(0x5B6EEB, 2); // Allow local connections Utils::Hook::Set(0x4D43DA, 0xEB); // LobbyID mismatch Utils::Hook::Nop(0x4E50D6, 2); Utils::Hook::Set(0x4E50DA, 0xEB); // causes 'does current Steam lobby match' calls in Steam_JoinLobby to be ignored Utils::Hook::Set(0x49D007, 0xEB); // functions checking party heartbeat timeouts, cause random issues Utils::Hook::Nop(0x4E532D, 5); // Steam_JoinLobby call causes migration Utils::Hook::Nop(0x5AF851, 5); Utils::Hook::Set(0x5AF85B, 0xEB); // Allow xpartygo in public lobbies Utils::Hook::Set(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(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(0x487BB2, 0xEB); // Force xblive_privatematch 0 and rename it //Utils::Hook::Set(0x420A6A, 4); Utils::Hook::Set(0x420A6C, 0); Utils::Hook::Set(0x420A6E, "xblive_privateserver"); // 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, Party::UIDvarIntStub, HOOK_CALL).install()->quick(); // Set NAT to open Utils::Hook::Set(0x79D898, 1); // Disable host migration Utils::Hook::Set(0x5B58B2, 0xEB); Utils::Hook::Set(0x4D6171, 0); Utils::Hook::Nop(0x4077A1, 5); // PartyMigrate_Frame // Patch playlist stuff for non-party behavior static Game::dvar_t* partyEnable = Party::PartyEnable.get(); Utils::Hook::Set(0x4A4093, &partyEnable); Utils::Hook::Set(0x4573F1, &partyEnable); Utils::Hook::Set(0x5B1A0C, &partyEnable); // Invert corresponding jumps Utils::Hook::Xor(0x4A409B, 1); Utils::Hook::Xor(0x4573FA, 1); Utils::Hook::Xor(0x5B1A17, 1); // Fix xstartlobby //Utils::Hook::Set(0x5B71CD, 0xEB); // Patch party_minplayers to 1 and protect it //Utils::Hook(0x4D5D51, Party::RegisterMinPlayers, HOOK_CALL).install()->quick(); // Set ui_maxclients to sv_maxclients Utils::Hook::Set(0x42618F, "sv_maxclients"); Utils::Hook::Set(0x4D3756, "sv_maxclients"); Utils::Hook::Set(0x5E3772, "sv_maxclients"); // Unlatch maxclient dvars Utils::Hook::Xor(0x426187, Game::DVAR_LATCH); Utils::Hook::Xor(0x4D374E, Game::DVAR_LATCH); Utils::Hook::Xor(0x5E376A, Game::DVAR_LATCH); Utils::Hook::Xor(0x4261A1, Game::DVAR_LATCH); Utils::Hook::Xor(0x4D376D, Game::DVAR_LATCH); Utils::Hook::Xor(0x5E3789, Game::DVAR_LATCH); // Patch Live_PlayerHasLoopbackAddr //Utils::Hook::Set(0x418F30, 0x90C3C033); Command::Add("connect", [](Command::Params* params) { if (params->size() < 2) { return; } if (Game::CL_IsCgameInitialized()) { Command::Execute("disconnect", false); Command::Execute(Utils::String::VA("%s", params->join(0).data()), false); } else { Party::Connect(Network::Address(params->get(1))); } }); Command::Add("reconnect", [](Command::Params*) { Party::Connect(Party::Container.target); }); if (!Dedicated::IsEnabled() && !ZoneBuilder::IsEnabled()) { Scheduler::Loop([] { if (Party::Container.valid) { if ((Game::Sys_Milliseconds() - Party::Container.joinTime) > 10'000) { Party::Container.valid = false; Party::ConnectError("Server connection timed out."); } } if (Party::Container.awaitingPlaylist) { if ((Game::Sys_Milliseconds() - Party::Container.requestTime) > 5'000) { Party::Container.awaitingPlaylist = false; Party::ConnectError("Playlist request timed out."); } } }, Scheduler::Pipeline::CLIENT); } // Basic info handler Network::OnClientPacket("getInfo", [](const Network::Address& address, [[maybe_unused]] const std::string& data) { int botCount = 0; int clientCount = 0; int maxclientCount = *Game::svs_clientCount; if (maxclientCount) { for (int i = 0; i < maxclientCount; ++i) { if (Game::svs_clients[i].header.state >= Game::CS_CONNECTED) { if (Game::svs_clients[i].bIsTestClient) ++botCount; else ++clientCount; } } } else { maxclientCount = Dvar::Var("party_maxplayers").get(); clientCount = Game::PartyHost_CountMembers(reinterpret_cast(0x1081C00)); } 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", Utils::String::VA("%i", clientCount)); info.set("bots", Utils::String::VA("%i", botCount)); info.set("sv_maxclients", Utils::String::VA("%i", maxclientCount)); info.set("protocol", Utils::String::VA("%i", PROTOCOL)); info.set("shortversion", SHORTVERSION); info.set("checksum", Utils::String::VA("%d", Game::Sys_Milliseconds())); info.set("mapname", Dvar::Var("mapname").get()); info.set("isPrivate", (Dvar::Var("g_password").get().size() ? "1" : "0")); info.set("hc", (Dvar::Var("g_hardcore").get() ? "1" : "0")); info.set("securityLevel", Utils::String::VA("%i", Dvar::Var("sv_securityLevel").get())); info.set("sv_running", ((*Game::com_sv_running)->current.enabled ? "1" : "0")); info.set("aimAssist", (Gamepad::sv_allowAimAssist.get() ? "1" : "0")); info.set("voiceChat", (Voice::SV_VoiceEnabled() ? "1" : "0")); // Ensure mapname is set if (info.get("mapname").empty() || Party::IsInLobby()) { info.set("mapname", Dvar::Var("ui_mapname").get()); } if (Maps::GetUserMap()->isValid()) { info.set("usermaphash", Utils::String::VA("%i", Maps::GetUserMap()->getHash())); } else if (Party::IsInUserMapLobby()) { info.set("usermaphash", Utils::String::VA("%i", Maps::GetUsermapHash(info.get("mapname")))); } if (Dedicated::IsEnabled()) { info.set("sv_motd", Dvar::Var("sv_motd").get()); } // Set matchtype // 0 - No match, connecting not possible // 1 - Party, use Steam_JoinLobby to connect // 2 - Match, use CL_ConnectFromParty to connect if (PartyEnable.get() && Dvar::Var("party_host").get()) // Party hosting { info.set("matchtype", "1"); } else if (Dvar::Var("sv_running").get()) // Match hosting { info.set("matchtype", "2"); } else { info.set("matchtype", "0"); } info.set("wwwDownload", (Dvar::Var("sv_wwwDownload").get() ? "1" : "0")); info.set("wwwUrl", Dvar::Var("sv_wwwBaseUrl").get()); Network::SendCommand(address, "infoResponse", "\\" + info.build()); }); Network::OnClientPacket("infoResponse", [](const Network::Address& address, [[maybe_unused]] const std::string& data) { Utils::InfoString info(data); // Handle connection if (Party::Container.valid) { if (Party::Container.target == address) { // Invalidate handler for future packets Party::Container.valid = false; Party::Container.info = info; Party::Container.matchType = atoi(info.get("matchtype").data()); uint32_t securityLevel = static_cast(atoi(info.get("securityLevel").data())); bool isUsermap = !info.get("usermaphash").empty(); unsigned int usermapHash = atoi(info.get("usermaphash").data()); std::string mod = (*Game::fs_gameDirVar)->current.string; // set fast server stuff here so its updated when we go to download stuff if (info.get("wwwDownload") == "1"s) { Dvar::Var("sv_wwwDownload").set(true); Dvar::Var("sv_wwwBaseUrl").set(info.get("wwwUrl")); } else { Dvar::Var("sv_wwwDownload").set(false); Dvar::Var("sv_wwwBaseUrl").set(""); } if (info.get("challenge") != Party::Container.challenge) { Party::ConnectError("Invalid join response: Challenge mismatch."); } else if (securityLevel > Auth::GetSecurityLevel()) { //Party::ConnectError(Utils::VA("Your security level (%d) is lower than the server's (%d)", Auth::GetSecurityLevel(), securityLevel)); Command::Execute("closemenu popup_reconnectingtoparty"); Auth::IncreaseSecurityLevel(securityLevel, "reconnect"); } else if (!Party::Container.matchType) { Party::ConnectError("Server is not hosting a match."); } else if (Party::Container.matchType > 2 || Party::Container.matchType < 0) { Party::ConnectError("Invalid join response: Unknown matchtype"); } else if (Party::Container.info.get("mapname").empty() || Party::Container.info.get("gametype").empty()) { Party::ConnectError("Invalid map or gametype."); } else if (Party::Container.info.get("isPrivate") == "1"s && !Dvar::Var("password").get().length()) { Party::ConnectError("A password is required to join this server! Set it at the bottom of the serverlist."); } 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); } else if (!Dvar::Var("fs_game").get().empty() && info.get("fs_game").empty()) { Game::Dvar_SetString(*Game::fs_gameDirVar, ""); if (Dvar::Var("cl_modVidRestart").get()) { Command::Execute("vid_restart", false); } Command::Execute("reconnect", false); } else { if (!Maps::CheckMapInstalled(Party::Container.info.get("mapname").data(), true)) return; Party::Container.motd = info.get("sv_motd"); if (Party::Container.matchType == 1) // Party { // Send playlist request Party::Container.requestTime = Game::Sys_Milliseconds(); Party::Container.awaitingPlaylist = true; Network::SendCommand(Party::Container.target, "getplaylist", Dvar::Var("password").get()); // This is not a safe method // TODO: Fix actual error! if (Game::CL_IsCgameInitialized()) { Command::Execute("disconnect", true); } } else if (Party::Container.matchType == 2) // Match { if (atoi(Party::Container.info.get("clients").data()) >= atoi(Party::Container.info.get("sv_maxclients").data())) { 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, *Party::Container.target.get(), 0, 0, Party::Container.info.get("mapname").data(), Party::Container.info.get("gametype").data()); } } } } } ServerList::Insert(address, info); Friends::UpdateServer(address, info.get("hostname"), info.get("mapname")); }); } }