From c9ff9416001f3d383d4adc6b86ad73755ceab712 Mon Sep 17 00:00:00 2001 From: smallmodel <15067410+smallmodel@users.noreply.github.com> Date: Tue, 15 Apr 2025 00:31:34 +0200 Subject: [PATCH] Implement Instant Action from Breakthrough (#712) This adds a feature that makes it easy to join a populated low-ping server (matching criteria from configuration file). This feature has been available since MOHAA Breakthrough 2.30. --- code/client/cl_instantAction.cpp | 640 +++++++++++++++++++++++++++++++ code/client/cl_instantAction.h | 103 +++++ 2 files changed, 743 insertions(+) create mode 100644 code/client/cl_instantAction.cpp create mode 100644 code/client/cl_instantAction.h diff --git a/code/client/cl_instantAction.cpp b/code/client/cl_instantAction.cpp new file mode 100644 index 00000000..1501121a --- /dev/null +++ b/code/client/cl_instantAction.cpp @@ -0,0 +1,640 @@ +/* +=========================================================================== +Copyright (C) 2025 the OpenMoHAA team + +This file is part of OpenMoHAA source code. + +OpenMoHAA source code 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 2 of the License, +or (at your option) any later version. + +OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "cl_ui.h" +#include "cl_instantAction.h" +#include "cl_uiserverlist.h" +#include "../gamespy/sv_gamespy.h" + +Event EV_UIInstantAction_AcceptServer +( + "acceptserver", + EV_DEFAULT, + NULL, + NULL, + "Connect to the current server" +); + +Event EV_UIInstantAction_RejectServer +( + "rejectserver", + EV_DEFAULT, + NULL, + NULL, + "Reject the current server" +); + +Event EV_UIInstantAction_Cancel +( + "ia_cancel", + EV_DEFAULT, + NULL, + NULL, + "cancel the server update" +); + +Event EV_UIInstantAction_Refresh +( + "ia_refresh", + EV_DEFAULT, + NULL, + NULL, + "Refresh the server list" +); + +CLASS_DECLARATION(UIWidget, UIInstantAction, NULL) { + {&EV_UIInstantAction_AcceptServer, &UIInstantAction::Connect }, + {&EV_UIInstantAction_RejectServer, &UIInstantAction::Reject }, + {&EV_UIInstantAction_Cancel, &UIInstantAction::CancelRefresh}, + {&EV_UIInstantAction_Refresh, &UIInstantAction::Refresh }, + {NULL, NULL } +}; + +struct ServerListInstance { + int iServerType; + UIInstantAction *pServerList; +}; + +ServerListInstance g_IAServerListInst[2]; + +UIInstantAction::UIInstantAction() +{ + state = IA_INITIALIZE; + numFoundServers = 0; + numServers = 0; + minPlayers = 3; + startingMaxPing = 100; + endingMaxPing = 1500; + maxServers = -1; + servers = NULL; + doneList[0] = false; + doneList[1] = false; + serverList[0] = NULL; + serverList[1] = NULL; + + ReadIniFile(); + EnableServerInfo(false); + + menuManager.PassEventToWidget("ia_cancel_button", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_refresh_button", new Event(EV_Widget_Disable)); +} + +UIInstantAction::~UIInstantAction() +{ + CleanUp(); +} + +void UIInstantAction::CleanUp() +{ + if (serverList[0]) { + ServerListFree(serverList[0]); + serverList[0] = NULL; + } + + if (serverList[1]) { + ServerListFree(serverList[1]); + serverList[1] = NULL; + } + + if (servers) { + delete[] servers; + servers = NULL; + } +} + +void UIInstantAction::Init() +{ + const char *secret_key; + const char *game_name; + + static const unsigned int iNumConcurrent = 10; + + numFoundServers = 0; + numServers = 0; + doneList[0] = false; + doneList[1] = false; + + EnableServerInfo(false); + + menuManager.PassEventToWidget("ia_cancel_button", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_refresh_button", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_noserverfound", new Event(EV_Widget_Disable)); + + Cvar_Set("ia_search_percentage", va("%d %%", 0)); + + if (com_target_game->integer < target_game_e::TG_MOHTT) { + g_IAServerListInst[0].iServerType = com_target_game->integer; + g_IAServerListInst[0].pServerList = this; + + game_name = GS_GetGameName(com_target_game->integer); + secret_key = GS_GetGameKey(com_target_game->integer); + + serverList[0] = ServerListNew( + game_name, + game_name, + secret_key, + iNumConcurrent, + (void *)&IAServerListCallBack, + 1, + (void *)&g_IAServerListInst[0] + ); + + ServerListClear(serverList[0]); + } else { + g_IAServerListInst[0].iServerType = target_game_e::TG_MOHTT; + g_IAServerListInst[0].pServerList = this; + + game_name = GS_GetGameName(target_game_e::TG_MOHTT); + secret_key = GS_GetGameKey(target_game_e::TG_MOHTT); + + serverList[0] = ServerListNew( + game_name, + game_name, + secret_key, + iNumConcurrent, + (void *)&IAServerListCallBack, + 1, + (void *)&g_IAServerListInst[0] + ); + + ServerListClear(serverList[0]); + + g_IAServerListInst[1].iServerType = target_game_e::TG_MOHTA; + g_IAServerListInst[1].pServerList = this; + + game_name = GS_GetGameName(target_game_e::TG_MOHTA); + secret_key = GS_GetGameKey(target_game_e::TG_MOHTA); + + serverList[1] = ServerListNew( + game_name, + game_name, + secret_key, + iNumConcurrent, + (void *)&IAServerListCallBack, + 1, + (void *)&g_IAServerListInst[1] + ); + + ServerListClear(serverList[1]); + } + + state = IA_WAITING; + numFoundServers = 0; + + ServerListUpdate(serverList[0], true); + + if (serverList[1]) { + ServerListUpdate(serverList[1], true); + } + + menuManager.PassEventToWidget("ia_cancel_button", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("searchstatus", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("searchstatuslable", new Event(EV_Widget_Enable)); +} + +int UIInstantAction::GetServerIndex(int maxPing, int gameType) +{ + int bestPing = 1500; + int bestServer = -1; + int i; + + for (i = 0; i < numFoundServers; i++) { + const IAServer_t& IAServer = servers[i]; + + char *gameVer; + float fGameVer; + int ping; + int numPlayers; + + if (IAServer.rejected) { + continue; + } + + // Skip servers that don't match the provided game type + if (ServerGetIntValue(IAServer.server, "g_gametype_i", 1) != gameType) { + continue; + } + + // Skip servers with high ping + ping = ServerGetPing(IAServer.server); + if (ping > maxPing) { + continue; + } + + gameVer = ServerGetStringValue(IAServer.server, "gamever", "1.00"); + if (com_target_demo->integer && *gameVer != 'd') { + // Skip retail servers on demo game + continue; + } else if (!com_target_demo->integer && *gameVer == 'd') { + // Skip demo servers on retail game + continue; + } + + // Skip incompatible servers + fGameVer = atof(gameVer); + if (com_target_game->integer >= target_game_e::TG_MOHTT) { + if (IAServer.serverGame.serverType == target_game_e::TG_MOHTT) { + if (fabs(fGameVer) < 2.3f) { + continue; + } + } else { + if (fabs(fGameVer) < 2.1f) { + continue; + } + } + } else { + if (fabs(fGameVer - com_target_version->value) > 0.1f) { + continue; + } + } + + // Skip servers with a password + if (ServerGetIntValue(IAServer.server, "password", 0)) { + continue; + } + + // Skip servers that don't match the minimum number of players + numPlayers = ServerGetIntValue(IAServer.server, "numplayers", 0); + if (numPlayers < minPlayers) { + continue; + } + + // Skip full servers + if (numPlayers == ServerGetIntValue(IAServer.server, "maxplayers", 0)) { + continue; + } + + // Skip servers with an higher ping than the best one + if (ping >= bestPing) { + continue; + } + + // + // Found a potential server + // + + bestPing = ping; + bestServer = i; + } + + return bestServer; +} + +void UIInstantAction::ReadIniFile() +{ + char *buffer; + const char *p; + const char *pVal; + int intValue; + char value[32]; + + if (!FS_ReadFileEx("iaction.ini", (void **)&buffer, qtrue)) { + return; + } + + for (p = buffer; p; p = strstr(pVal, "\n")) { + if (sscanf(p, "%31s", value) != 1) { + break; + } + + pVal = strstr(p, "="); + if (!pVal) { + break; + } + + pVal++; + + if (sscanf(pVal, "%d", &intValue) != 1) { + break; + } + + if (!Q_stricmpn(value, "MinPlayers", 10)) { + minPlayers = intValue; + } + + if (!Q_stricmpn(value, "StartingMaxPing", 15)) { + startingMaxPing = intValue; + } + + if (!Q_stricmpn(value, "EndingMaxPing", 13)) { + endingMaxPing = intValue; + } + + if (!Q_stricmpn(value, "MaxServers", 10)) { + maxServers = intValue; + } + } +} + +void UIInstantAction::FindServer() +{ + int ping; + int i; + + currentServer = -1; + state = IA_NONE; + + for (ping = startingMaxPing; ping < endingMaxPing; ping += 100) { + // + // Find the best server starting from FFA gametype first + // + for (i = 1; i < 7; i++) { + currentServer = GetServerIndex(ping, i); + if (currentServer >= 0) { + break; + } + } + + if (currentServer >= 0) { + break; + } + } + + menuManager.PassEventToWidget("ia_refresh_button", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("ia_cancel_button", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("searchstatus", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("searchstatuslable", new Event(EV_Widget_Disable)); + + if (currentServer < 0) { + EnableServerInfo(false); + + menuManager.PassEventToWidget("ia_noserverfound", new Event(EV_Widget_Enable)); + return; + } + + const IAServer_t& IAServer = servers[currentServer]; + const char *hostname = ServerGetStringValue(IAServer.server, "hostname", "(NONE)"); + const char *gametype = ServerGetStringValue(IAServer.server, "gametype", "(NONE)"); + int numplayers = ServerGetIntValue(IAServer.server, "numplayers", 0); + int maxplayers = ServerGetIntValue(IAServer.server, "maxplayers", 0); + ping = ServerGetPing(IAServer.server); + + Cvar_Set("ia_servername", va(" %s", hostname)); + Cvar_Set("ia_ping", va("%d", ping)); + Cvar_Set("ia_gametype", va("%s", gametype)); + Cvar_Set("ia_players", va("%d", numplayers)); + Cvar_Set("ia_maxplayers", va("%d", maxplayers)); + + EnableServerInfo(true); +} + +void UIInstantAction::Connect(Event *ev) +{ + char *gameVer; + float fGameVer; + bool bDiffVersion; + char command[256]; + + if (currentServer < 0 || currentServer < numServers) { + return; + } + + const IAServer_t& IAServer = servers[currentServer]; + + gameVer = ServerGetStringValue(IAServer.server, "gamever", "1.00"); + if (gameVer[0] == 'd') { + gameVer++; + } + + // Skip incompatible servers + fGameVer = atof(gameVer); + bDiffVersion = false; + if (com_target_game->integer >= target_game_e::TG_MOHTT) { + if (IAServer.serverGame.serverType == target_game_e::TG_MOHTT) { + if (fabs(fGameVer) < 2.3f) { + bDiffVersion = true; + } + } else { + if (fabs(fGameVer) < 2.1f) { + bDiffVersion = true; + } + } + } else { + if (fabs(fGameVer - com_target_version->value) > 0.1f) { + bDiffVersion = true; + } + } + + if (bDiffVersion) { + if (fGameVer - com_target_version->value > 0) { + // Older version + UI_SetReturnMenuToCurrent(); + Cvar_Set("com_errormessage", va("Server is version %s, you are using %s", gameVer, "2.40")); + UI_PushMenu("wrongversion"); + } else { + // Server version is newer + Cvar_Set("dm_serverstatus", va("Can not connect to v%s server, you are using v%s", gameVer, "2.40")); + } + } + + UI_SetReturnMenuToCurrent(); + Cvar_Set("g_servertype", va("%d", servers[currentServer].serverGame.serverType)); + + Com_sprintf( + command, + sizeof(command), + "connect %s:%i\n", + ServerGetAddress(IAServer.server), + ServerGetIntValue(IAServer.server, "hostport", PORT_SERVER) + ); + Cbuf_AddText(command); +} + +void UIInstantAction::Reject(Event *ev) +{ + servers[currentServer].rejected = 1; + FindServer(); +} + +void UIInstantAction::Draw() +{ + switch (state) { + case IA_INITIALIZE: + Init(); + break; + case IA_UPDATE: + Update(); + break; + case IA_FINISHED: + FindServer(); + break; + default: + break; + } + + if (serverList[0]) { + ServerListThink(serverList[0]); + } + + if (serverList[1]) { + ServerListThink(serverList[1]); + } +} + +void UIInstantAction::Update() +{ + numFoundServers = 0; + + // count the total number of servers from both server list + numServers = ServerListCount(serverList[0]); + if (serverList[1]) { + numServers += ServerListCount(serverList[1]); + } + + state = IA_FINISHED; + servers = new IAServer_t[numServers]; + + ServerListHalt(serverList[0]); + if (serverList[1]) { + ServerListHalt(serverList[1]); + } + + ServerListThink(serverList[0]); + if (serverList[1]) { + ServerListThink(serverList[1]); + } + + state = IA_SEARCHING; + + // Start updating the first list + doneList[0] = false; + ServerListClear(serverList[0]); + ServerListUpdate(serverList[0], true); + + // Update the second optional list + if (serverList[1]) { + doneList[1] = false; + ServerListClear(serverList[1]); + ServerListUpdate(serverList[1], true); + } +} + +int UIInstantAction::AddServer(GServer server, const ServerGame_t& serverGame) +{ + servers[numFoundServers].server = server; + servers[numFoundServers].serverGame = serverGame; + servers[numFoundServers].rejected = false; + numFoundServers++; + + return numFoundServers; +} + +void UIInstantAction::CancelRefresh(Event *ev) +{ + state = IA_FINISHED; + ServerListHalt(serverList[0]); + ServerListHalt(serverList[1]); +} + +void UIInstantAction::Refresh(Event *ev) +{ + state = IA_INITIALIZE; +} + +void UIInstantAction::EnableServerInfo(bool enable) +{ + if (enable) { + menuManager.PassEventToWidget("iaservername_label", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("ia_servername_field", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("ia_ping_label", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("ia_ping_field", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("ia_gametype_label", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("ia_gametype_field", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("ia_players_label", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("ia_players_field", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("ia_maxplayers_label", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("ia_maxplayers_field", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("acceptserver", new Event(EV_Widget_Enable)); + menuManager.PassEventToWidget("rejectserver", new Event(EV_Widget_Enable)); + } else { + menuManager.PassEventToWidget("iaservername_label", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_servername_field", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_ping_label", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_ping_field", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_gametype_label", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_gametype_field", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_players_label", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_players_field", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_maxplayers_label", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("ia_maxplayers_field", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("acceptserver", new Event(EV_Widget_Disable)); + menuManager.PassEventToWidget("rejectserver", new Event(EV_Widget_Disable)); + } +} + +void UIInstantAction::IAServerListCallBack(GServerList serverlist, int msg, void *instance, void *param1, void *param2) +{ + const ServerListInstance *pInstance = (const ServerListInstance *)instance; + UIInstantAction *pServerList = pInstance->pServerList; + + if (msg == LIST_PROGRESS) { + if (pServerList->state == IA_WAITING) { + if (pInstance->iServerType == com_target_game->integer) { + pServerList->doneList[0] = true; + } + + if (com_target_game->integer >= target_game_e::TG_MOHTT && pInstance->iServerType == target_game_e::TG_MOHTA) { + pServerList->doneList[1] = true; + } + + if (pServerList->doneList[0] && (!pServerList->serverList[1] || pServerList->doneList[1])) { + pServerList->state = IA_UPDATE; + } + } else if (pServerList->state == IA_SEARCHING) { + ServerGame_t serverGame; + serverGame.serverType = pInstance->iServerType; + const int serverIndex = pServerList->AddServer((GServer)param1, serverGame); + + Cvar_Set("ia_search_percentage", va("%d %%", 100 * serverIndex / pServerList->numServers)); + + if (pServerList->maxServers >= 0 && serverIndex >= pServerList->maxServers) { + // Reached the maximum number of servers, stop there + pServerList->doneList[0] = true; + ServerListHalt(pServerList->serverList[0]); + + if (pServerList->serverList[1]) { + pServerList->doneList[1] = true; + ServerListHalt(pServerList->serverList[1]); + } + + pServerList->state = IA_FINISHED; + } + } + } else if (msg == LIST_STATECHANGED && ServerListState(serverlist) == GServerListState::sl_idle) { + if (pInstance->iServerType == com_target_game->integer) { + pServerList->doneList[0] = true; + } + + if (com_target_game->integer >= target_game_e::TG_MOHTT && pInstance->iServerType == target_game_e::TG_MOHTA) { + pServerList->doneList[1] = true; + } + + if (pServerList->doneList[0] && (!pServerList->serverList[1] || pServerList->doneList[1])) { + if (pServerList->state == IA_WAITING) { + pServerList->state = IA_UPDATE; + } + if (pServerList->state == IA_SEARCHING) { + pServerList->state = IA_FINISHED; + } + } + } +} diff --git a/code/client/cl_instantAction.h b/code/client/cl_instantAction.h new file mode 100644 index 00000000..9a838dc5 --- /dev/null +++ b/code/client/cl_instantAction.h @@ -0,0 +1,103 @@ +/* +=========================================================================== +Copyright (C) 2025 the OpenMoHAA team + +This file is part of OpenMoHAA source code. + +OpenMoHAA source code 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 2 of the License, +or (at your option) any later version. + +OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// Added in 2.30 +// Instantly find a server matching common criterias + +#pragma once + +#include "../gamespy/goaceng.h" + +typedef struct { + int serverType; +} ServerGame_t; + +typedef struct { + GServer server; + ServerGame_t serverGame; + bool rejected; +} IAServer_t; + +enum IAState_e { + IA_NONE, + IA_INITIALIZE, + IA_WAITING, + IA_UPDATE, + IA_SEARCHING, + IA_FINISHED +}; + +class UIInstantAction : public UIWidget +{ +public: + CLASS_PROTOTYPE(UIInstantAction); + +public: + UIInstantAction(); + ~UIInstantAction() override; + + void CleanUp(); + void Init(); + + int GetServerIndex(int maxPing, int gameType); + void ReadIniFile(); + void FindServer(); + void Connect(Event *ev); + void Reject(Event *ev); + void Draw(); + void Update(); + int AddServer(GServer server, const ServerGame_t& serverGame); + void CancelRefresh(Event *ev); + void Refresh(Event *ev); + void EnableServerInfo(bool enable); + +private: + static void IAServerListCallBack(GServerList serverlist, int msg, void *instance, void *param1, void *param2); + +private: + // + // List + // + bool doneList[2]; + GServerList serverList[2]; + int maxServers; + + // + // Current states + // + IAState_e state; + int numServers; + int numFoundServers; + + // + // Filters + // + int minPlayers; + int startingMaxPing; + int endingMaxPing; + + // + // Servers + // + IAServer_t *servers; + int currentServer; +};