From afb99428736949f061d002a68b259b1dfa83a43a Mon Sep 17 00:00:00 2001 From: Bill Rich Date: Thu, 12 Mar 2026 16:30:38 +0000 Subject: [PATCH] feat(gui): Add Random button to LAN and Online game lobbies --- GeneralsMD/Code/GameEngine/CMakeLists.txt | 1 + .../OnlineServices_LobbyInterface.h | 2 + .../Include/GameNetwork/RandomAssign.h | 8 + .../GUICallbacks/Menus/LanGameOptionsMenu.cpp | 18 + .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 28 ++ .../OnlineServices_LobbyInterface.cpp | 42 ++- .../Source/GameNetwork/RandomAssign.cpp | 330 ++++++++++++++++++ 7 files changed, 428 insertions(+), 1 deletion(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h create mode 100644 GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 5e6d97be8dc..8681e093209 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -1116,6 +1116,7 @@ set(GAMEENGINE_SRC # Source/GameNetwork/GameSpy/Thread/ThreadUtils.cpp # Source/GameNetwork/GameSpyOverlay.cpp Source/GameNetwork/GUIUtil.cpp + Source/GameNetwork/RandomAssign.cpp # Source/GameNetwork/IPEnumeration.cpp # Source/GameNetwork/LANAPI.cpp # Source/GameNetwork/LANAPICallbacks.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h index 506609d8249..1d80e84650e 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h @@ -183,6 +183,8 @@ class NGMP_OnlineServices_LobbyInterface void UpdateCurrentLobby_AITeam(int slot, int team); void UpdateCurrentLobby_AIStartPos(int slot, int startpos); + void UpdateCurrentLobby_BulkSlotUpdate(NGMPGame* game); + void UpdateCurrentLobbyMaxCameraHeight(uint16_t maxCameraHeight); void SetJoinability(ELobbyJoinability joinabilityFlag); diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h new file mode 100644 index 00000000000..9d9287b7bf7 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h @@ -0,0 +1,8 @@ +#pragma once + +class GameInfo; + +#include + +void performRandomAssign(GameInfo *game, const std::vector &lockedTemplates); +std::vector buildLockedTemplates(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp index 4072ce98ad5..b4f948a3294 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp @@ -59,6 +59,7 @@ #include "Common/MultiplayerSettings.h" #include "GameClient/GameText.h" #include "GameNetwork/GUIUtil.h" +#include "GameNetwork/RandomAssign.h" extern char *LANnextScreen; @@ -110,6 +111,7 @@ static NameKeyType textEntryChatID = NAMEKEY_INVALID; static NameKeyType textEntryMapDisplayID = NAMEKEY_INVALID; static NameKeyType buttonBackID = NAMEKEY_INVALID; static NameKeyType buttonStartID = NAMEKEY_INVALID; +static NameKeyType buttonRandomizeID = NAMEKEY_INVALID; static NameKeyType buttonEmoteID = NAMEKEY_INVALID; static NameKeyType buttonSelectMapID = NAMEKEY_INVALID; static NameKeyType checkboxLimitSuperweaponsID = NAMEKEY_INVALID; @@ -119,6 +121,7 @@ static NameKeyType windowMapID = NAMEKEY_INVALID; static GameWindow *parentLanGameOptions = nullptr; static GameWindow *buttonBack = nullptr; static GameWindow *buttonStart = nullptr; +static GameWindow *buttonRandomize = nullptr; static GameWindow *buttonSelectMap = nullptr; static GameWindow *buttonEmote = nullptr; static GameWindow *textEntryChat = nullptr; @@ -674,6 +677,7 @@ void InitLanGameGadgets() parentLanGameOptionsID = TheNameKeyGenerator->nameToKey( "LanGameOptionsMenu.wnd:LanGameOptionsMenuParent" ); buttonBackID = TheNameKeyGenerator->nameToKey( "LanGameOptionsMenu.wnd:ButtonBack" ); buttonStartID = TheNameKeyGenerator->nameToKey( "LanGameOptionsMenu.wnd:ButtonStart" ); + buttonRandomizeID = TheNameKeyGenerator->nameToKey( "LanGameOptionsMenu.wnd:ButtonRandomize" ); textEntryChatID = TheNameKeyGenerator->nameToKey( "LanGameOptionsMenu.wnd:TextEntryChat" ); textEntryMapDisplayID = TheNameKeyGenerator->nameToKey( "LanGameOptionsMenu.wnd:TextEntryMapDisplay" ); listboxChatWindowLanGameID = TheNameKeyGenerator->nameToKey( "LanGameOptionsMenu.wnd:ListboxChatWindowLanGame" ); @@ -692,6 +696,8 @@ void InitLanGameGadgets() DEBUG_ASSERTCRASH(buttonSelectMap, ("Could not find the buttonSelectMap")); buttonStart = TheWindowManager->winGetWindowFromId( parentLanGameOptions,buttonStartID ); DEBUG_ASSERTCRASH(buttonStart, ("Could not find the buttonStart")); + buttonRandomize = TheWindowManager->winGetWindowFromId( parentLanGameOptions, buttonRandomizeID ); + DEBUG_ASSERTCRASH(buttonRandomize, ("Could not find the buttonRandomize")); buttonBack = TheWindowManager->winGetWindowFromId( parentLanGameOptions, buttonBackID); DEBUG_ASSERTCRASH(buttonBack, ("Could not find the buttonBack")); listboxChatWindowLanGame = TheWindowManager->winGetWindowFromId( parentLanGameOptions, listboxChatWindowLanGameID ); @@ -795,6 +801,7 @@ void DeinitLanGameGadgets() buttonSelectMap = nullptr; buttonStart = nullptr; buttonBack = nullptr; + buttonRandomize = nullptr; listboxChatWindowLanGame = nullptr; textEntryChat = nullptr; textEntryMapDisplay = nullptr; @@ -885,6 +892,7 @@ void LanGameOptionsMenuInit( WindowLayout *layout, void *userData ) //DEBUG_LOG(("LanGameOptionsMenuInit(): map is %s", TheLAN->GetMyGame()->getMap().str())); buttonStart->winSetText(TheGameText->fetch("GUI:Accept")); buttonSelectMap->winEnable( FALSE ); + buttonRandomize->winEnable( FALSE ); checkboxLimitSuperweapons->winEnable( FALSE ); // Can look but only host can touch comboBoxStartingCash->winEnable( FALSE ); // Ditto TheLAN->GetMyGame()->setMapCRC( TheLAN->GetMyGame()->getMapCRC() ); // force a recheck @@ -1275,6 +1283,16 @@ WindowMsgHandledType LanGameOptionsMenuSystem( GameWindow *window, UnsignedInt m } } + else if ( controlID == buttonRandomizeID ) + { + if (TheLAN->AmIHost()) + { + std::vector lockedTemplates = buildLockedTemplates(); + performRandomAssign(TheLAN->GetMyGame(), lockedTemplates); + TheLAN->RequestGameOptions(GenerateGameOptionsString(), true); + lanUpdateSlotList(); + } + } else if ( controlID == checkboxLimitSuperweaponsID ) { handleLimitSuperweaponsClick(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index cc8bee92b6a..23fc52d6b39 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -67,6 +67,8 @@ #include "GameNetwork/GameSpy/GSConfig.h" #include "GameNetwork/GeneralsOnline/NGMP_interfaces.h" +#include "GameNetwork/RandomAssign.h" +#include "GameClient/ChallengeGenerals.h" #include #include #include "../OnlineServices_Init.h" @@ -201,6 +203,7 @@ static NameKeyType buttonBackID = NAMEKEY_INVALID; static NameKeyType buttonStartID = NAMEKEY_INVALID; static NameKeyType buttonEmoteID = NAMEKEY_INVALID; static NameKeyType buttonSelectMapID = NAMEKEY_INVALID; +static NameKeyType buttonRandomizeID = NAMEKEY_INVALID; static NameKeyType windowMapID = NAMEKEY_INVALID; #if defined(GENERALS_ONLINE_ENABLE_MATCH_START_COUNTDOWN) @@ -218,6 +221,7 @@ static GameWindow *parentWOLGameSetup = NULL; static GameWindow *buttonBack = NULL; static GameWindow *buttonStart = NULL; static GameWindow *buttonSelectMap = NULL; +static GameWindow *buttonRandomize = NULL; static GameWindow *buttonEmote = NULL; static GameWindow *textEntryChat = NULL; static GameWindow *textEntryMapDisplay = NULL; @@ -1470,6 +1474,7 @@ void InitWOLGameGadgets() listboxGameSetupChatID = TheNameKeyGenerator->nameToKey( "GameSpyGameOptionsMenu.wnd:ListboxChatWindowGameSpyGameSetup" ); buttonEmoteID = TheNameKeyGenerator->nameToKey( "GameSpyGameOptionsMenu.wnd:ButtonEmote" ); buttonSelectMapID = TheNameKeyGenerator->nameToKey( "GameSpyGameOptionsMenu.wnd:ButtonSelectMap" ); + buttonRandomizeID = TheNameKeyGenerator->nameToKey( "GameSpyGameOptionsMenu.wnd:ButtonRandomize" ); checkBoxUseStatsID = TheNameKeyGenerator->nameToKey( "GameSpyGameOptionsMenu.wnd:CheckBoxUseStats" ); windowMapID = TheNameKeyGenerator->nameToKey( "GameSpyGameOptionsMenu.wnd:MapWindow" ); checkBoxLimitSuperweaponsID = TheNameKeyGenerator->nameToKey("GameSpyGameOptionsMenu.wnd:CheckboxLimitSuperweapons"); @@ -1483,6 +1488,8 @@ void InitWOLGameGadgets() parentWOLGameSetup = TheWindowManager->winGetWindowFromId( NULL, parentWOLGameSetupID ); buttonEmote = TheWindowManager->winGetWindowFromId( parentWOLGameSetup,buttonEmoteID ); buttonSelectMap = TheWindowManager->winGetWindowFromId( parentWOLGameSetup,buttonSelectMapID ); + buttonRandomize = TheWindowManager->winGetWindowFromId( parentWOLGameSetup, buttonRandomizeID ); + DEBUG_ASSERTCRASH(buttonRandomize, ("Could not find the buttonRandomize")); checkBoxUseStats = TheWindowManager->winGetWindowFromId( parentWOLGameSetup, checkBoxUseStatsID ); buttonStart = TheWindowManager->winGetWindowFromId( parentWOLGameSetup,buttonStartID ); buttonBack = TheWindowManager->winGetWindowFromId( parentWOLGameSetup, buttonBackID); @@ -1528,6 +1535,7 @@ void InitWOLGameGadgets() { checkBoxLimitSuperweapons->winEnable( false ); comboBoxStartingCash->winEnable( false ); + buttonRandomize->winEnable( false ); NameKeyType labelID = TheNameKeyGenerator->nameToKey("GameSpyGameOptionsMenu.wnd:StartingCashLabel"); TheWindowManager->winGetWindowFromId(parentWOLGameSetup, labelID)->winEnable( FALSE ); } @@ -1536,6 +1544,7 @@ void InitWOLGameGadgets() { checkBoxLimitSuperweapons->winEnable(true); comboBoxStartingCash->winEnable(true); + buttonRandomize->winEnable(true); } #endif @@ -1548,6 +1557,7 @@ void InitWOLGameGadgets() checkBoxLimitSuperweapons->winEnable( FALSE ); comboBoxStartingCash->winEnable( FALSE ); checkBoxLimitArmies->winEnable( FALSE ); + buttonRandomize->winEnable( FALSE ); NameKeyType labelID = TheNameKeyGenerator->nameToKey("GameSpyGameOptionsMenu.wnd:StartingCashLabel"); TheWindowManager->winGetWindowFromId(parentWOLGameSetup, labelID)->winEnable( FALSE ); } @@ -1688,6 +1698,7 @@ void DeinitWOLGameGadgets() parentWOLGameSetup = NULL; buttonEmote = NULL; buttonSelectMap = NULL; + buttonRandomize = NULL; buttonStart = NULL; buttonBack = NULL; listboxGameSetupChat = NULL; @@ -2085,6 +2096,7 @@ void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) buttonStart->winSetText(TheGameText->fetch("GUI:Accept")); buttonStart->winEnable( FALSE ); buttonSelectMap->winEnable( FALSE ); + buttonRandomize->winEnable( FALSE ); initialAcceptEnable = FALSE; WOLDisplaySlotList(); @@ -2356,6 +2368,7 @@ void WOLGameSetupMenuUpdate( WindowLayout * layout, void *userData) buttonStart->winSetText(TheGameText->fetch("GUI:Start")); buttonStart->winEnable(TRUE); buttonSelectMap->winEnable(TRUE); + buttonRandomize->winEnable(TRUE); initialAcceptEnable = TRUE; comboBoxStartingCash->winEnable(TRUE); @@ -3942,6 +3955,21 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg WOLMapSelectLayout->hide( FALSE ); WOLMapSelectLayout->bringForward(); } + else if ( controlID == buttonRandomizeID ) + { + NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); + if (pLobbyInterface != nullptr && pLobbyInterface->IsHost()) + { + NGMPGame* game = pLobbyInterface->GetCurrentGame(); + if (game) + { + std::vector lockedTemplates = buildLockedTemplates(); + performRandomAssign(game, lockedTemplates); + pLobbyInterface->UpdateCurrentLobby_BulkSlotUpdate(game); + WOLDisplaySlotList(); + } + } + } else if ( controlID == buttonStartID ) { savePlayerInfo(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index 39804f4d5d6..0874852d057 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -74,7 +74,8 @@ enum class ELobbyUpdateField AI_TEAM = 15, AI_START_POS = 16, MAX_CAMERA_HEIGHT = 17, - JOINABILITY = 18 + JOINABILITY = 18, + HOST_ACTION_BULK_SLOT_UPDATE = 19 }; void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_Map(AsciiString strMap, AsciiString strMapPath, bool bIsOfficial, int newMaxPlayers) @@ -468,6 +469,45 @@ void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_ForceReady() }); } +void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_BulkSlotUpdate(NGMPGame* game) +{ + ClearAutoReadyCountdown(); + if (TheNGMPGame && TheNGMPGame->IsCountdownStarted()) + TheNGMPGame->StopCountdown(); + + std::string strURI = std::format("{}/{}", NGMP_OnlineServicesManager::GetAPIEndpoint("Lobby"), m_CurrentLobby.lobbyID); + std::map mapHeaders; + + nlohmann::json j; + j["field"] = ELobbyUpdateField::HOST_ACTION_BULK_SLOT_UPDATE; + + nlohmann::json slotsArray = nlohmann::json::array(); + for (int i = 0; i < MAX_SLOTS; ++i) + { + const GameSlot *slot = game->getConstSlot(i); + if (slot && slot->isOccupied()) + { + nlohmann::json slotEntry; + slotEntry["slot_index"] = i; + slotEntry["side"] = slot->getPlayerTemplate(); + slotEntry["color"] = slot->getColor(); + slotEntry["start_pos"] = slot->getStartPos(); + slotEntry["team"] = slot->getTeamNumber(); + slotsArray.push_back(slotEntry); + } + } + j["slots"] = slotsArray; + std::string strPostData = j.dump(); + + NGMP_OnlineServicesManager::GetInstance()->GetHTTPManager()->SendPOSTRequest(strURI.c_str(), EIPProtocolVersion::DONT_CARE, mapHeaders, strPostData.c_str(), [=](bool bSuccess, int statusCode, std::string strBody, HTTPRequest* pReq) + { + if (!bSuccess || statusCode < 200 || statusCode >= 300) + { + DEBUG_LOG(("UpdateCurrentLobby_BulkSlotUpdate failed: success=%d, status=%d", bSuccess, statusCode)); + } + }); +} + void NGMP_OnlineServices_LobbyInterface::SendChatMessageToCurrentLobby(UnicodeString& strChatMsgUnicode, bool bIsAction) { std::shared_ptr pWS = NGMP_OnlineServicesManager::GetWebSocket();; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp new file mode 100644 index 00000000000..e8267aef2a5 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp @@ -0,0 +1,330 @@ +#include "PreRTS.h" + +#include "GameNetwork/RandomAssign.h" +#include "GameNetwork/GameInfo.h" +#include "Common/PlayerTemplate.h" +#include "Common/MultiplayerSettings.h" +#include "GameClient/MapUtil.h" +#include "GameClient/ChallengeGenerals.h" + +#include +#include +#include +#include + +// Build the list of valid template indices for random assignment. +// Same filtering as populateRandomSideAndColor (GameLogic.cpp). +static void buildValidTemplates(const GameInfo *game, const std::vector &lockedTemplates, std::vector &out) +{ + Int count = ThePlayerTemplateStore->getPlayerTemplateCount(); + for (Int c = 0; c < count; ++c) + { + const PlayerTemplate *fac = ThePlayerTemplateStore->getNthPlayerTemplate(c); + if (!fac) + continue; + + // Must have a starting building (filters out civilian etc) + if (fac->getStartingBuilding().isEmpty()) + continue; + + // Respect old factions only mode + if (game->oldFactionsOnly() && !fac->isOldFaction()) + continue; + + // Skip locked generals (list provided by caller from client code) + Bool isLocked = FALSE; + for (size_t k = 0; k < lockedTemplates.size(); ++k) + { + if (lockedTemplates[k] == c) + { + isLocked = TRUE; + break; + } + } + if (isLocked) + continue; + + out.push_back(c); + } +} + +// Phase 1: Assign random factions and colors. +// Mirrors populateRandomSideAndColor (GameLogic.cpp:691-781). +// Only touches slots that have PLAYERTEMPLATE_RANDOM / color == -1. +static void assignRandomFactions(GameInfo *game, const std::vector &validTemplates) +{ + for (Int i = 0; i < MAX_SLOTS; ++i) + { + GameSlot *slot = game->getSlot(i); + if (!slot || !slot->isOccupied()) + continue; + + // Assign faction if random + Int playerTemplateIdx = slot->getPlayerTemplate(); + if (playerTemplateIdx == PLAYERTEMPLATE_RANDOM && !validTemplates.empty()) + { + playerTemplateIdx = validTemplates[rand() % validTemplates.size()]; + slot->setPlayerTemplate(playerTemplateIdx); + } + + // Assign color if random (-1) + Int colorIdx = slot->getColor(); + if (colorIdx < 0 || colorIdx >= TheMultiplayerSettings->getNumColors()) + { + Int numColors = TheMultiplayerSettings->getNumColors(); + if (numColors > 0) + { + colorIdx = -1; + for (Int attempt = 0; attempt < numColors * 2 && colorIdx == -1; ++attempt) + { + Int candidate = rand() % numColors; + if (!game->isColorTaken(candidate)) + colorIdx = candidate; + } + if (colorIdx >= 0) + slot->setColor(colorIdx); + } + } + } +} + +// Phase 2: Assign start positions using distance-based placement. +// Mirrors populateRandomStartPosition (GameLogic.cpp:787-1053). +// Different teams are placed far apart, teammates close together. +// Only touches slots that have startPos == -1. +static void assignRandomPositions(GameInfo *game) +{ + Int i; + Int numPlayers = MAX_SLOTS; + const MapMetaData *md = TheMapCache ? TheMapCache->findMap(game->getMap()) : nullptr; + if (md) + numPlayers = md->m_numPlayers; + + if (numPlayers <= 0) + return; + + // Build distance matrix between all start positions using map waypoints + static const WaypointMap s_emptyWaypoints = {}; + const WaypointMap &waypoints = md ? md->m_waypoints : s_emptyWaypoints; + Real startSpotDistance[MAX_SLOTS][MAX_SLOTS]; + for (i = 0; i < MAX_SLOTS; ++i) + { + for (Int j = 0; j < MAX_SLOTS; ++j) + { + if (i != j && i < numPlayers && j < numPlayers) + { + AsciiString w1, w2; + w1.format("Player_%d_Start", i + 1); + w2.format("Player_%d_Start", j + 1); + WaypointMap::const_iterator c1 = waypoints.find(w1); + WaypointMap::const_iterator c2 = waypoints.find(w2); + if (c1 == waypoints.end() || c2 == waypoints.end()) + { + startSpotDistance[i][j] = 1000000.0f; + } + else + { + Coord3D p1 = c1->second; + Coord3D p2 = c2->second; + startSpotDistance[i][j] = sqrt(sqr(p1.x - p2.x) + sqr(p1.y - p2.y)); + } + } + else + { + startSpotDistance[i][j] = 0.0f; + } + } + } + + // Track which positions are already taken (deliberately assigned) + Bool taken[MAX_SLOTS]; + for (i = 0; i < MAX_SLOTS; ++i) + taken[i] = (i < numPlayers) ? FALSE : TRUE; + + Bool hasStartSpotBeenPicked = FALSE; + for (i = 0; i < MAX_SLOTS; ++i) + { + GameSlot *slot = game->getSlot(i); + if (!slot || !slot->isOccupied() || slot->getPlayerTemplate() == PLAYERTEMPLATE_OBSERVER) + continue; + + Int posIdx = slot->getStartPos(); + if (posIdx >= 0 && posIdx < numPlayers) + { + hasStartSpotBeenPicked = TRUE; + taken[posIdx] = TRUE; + } + } + + // Track first position per team for teammate clustering + Int teamPosIdx[MAX_SLOTS]; + for (i = 0; i < MAX_SLOTS; ++i) + teamPosIdx[i] = -1; + + // Seed teamPosIdx from already-assigned slots + for (i = 0; i < MAX_SLOTS; ++i) + { + const GameSlot *slot = game->getConstSlot(i); + if (!slot || !slot->isOccupied() || slot->getPlayerTemplate() == PLAYERTEMPLATE_OBSERVER) + continue; + Int posIdx = slot->getStartPos(); + if (posIdx >= 0 && posIdx < numPlayers) + { + Int team = slot->getTeamNumber(); + if (team >= 0 && teamPosIdx[team] == -1) + teamPosIdx[team] = posIdx; + } + } + + // Assign positions for non-observer slots that don't have one yet + for (i = 0; i < MAX_SLOTS; ++i) + { + GameSlot *slot = game->getSlot(i); + if (!slot || !slot->isOccupied() || slot->getPlayerTemplate() == PLAYERTEMPLATE_OBSERVER) + continue; + + Int posIdx = slot->getStartPos(); + if (posIdx >= 0 && posIdx < numPlayers) + continue; // already assigned + + Int team = slot->getTeamNumber(); + + if (!hasStartSpotBeenPicked) + { + // First player: pick randomly + posIdx = -1; + for (Int attempt = 0; attempt < numPlayers * 2 && posIdx == -1; ++attempt) + { + Int candidate = rand() % numPlayers; + if (!taken[candidate]) + posIdx = candidate; + } + if (posIdx < 0) + continue; + hasStartSpotBeenPicked = TRUE; + slot->setStartPos(posIdx); + taken[posIdx] = TRUE; + if (team >= 0) + teamPosIdx[team] = posIdx; + } + else if (team < 0 || teamPosIdx[team] == -1) + { + // New team or no team: pick position farthest from all taken positions + Real farthestDistance = 0.0f; + Int farthestIndex = -1; + for (posIdx = 0; posIdx < numPlayers; ++posIdx) + { + if (taken[posIdx]) + continue; + + Real dist = 0.0f; + for (Int n = 0; n < numPlayers; ++n) + { + if (taken[n] && n != posIdx) + dist += startSpotDistance[posIdx][n]; + } + if (farthestIndex < 0 || dist > farthestDistance) + { + farthestDistance = dist; + farthestIndex = posIdx; + } + } + + if (farthestIndex >= 0) + { + slot->setStartPos(farthestIndex); + taken[farthestIndex] = TRUE; + if (team >= 0) + teamPosIdx[team] = farthestIndex; + } + } + else + { + // Teammate: pick position closest to team's existing position + Real closestDist = FLT_MAX; + Int closestIdx = -1; + for (Int n = 0; n < numPlayers; ++n) + { + if (!taken[n] && startSpotDistance[teamPosIdx[team]][n] < closestDist) + { + closestDist = startSpotDistance[teamPosIdx[team]][n]; + closestIdx = n; + } + } + if (closestIdx >= 0) + { + slot->setStartPos(closestIdx); + taken[closestIdx] = TRUE; + } + } + } + + // Assign observer slots to an existing player's position + Int numPlayersInGame = 0; + for (i = 0; i < MAX_SLOTS; ++i) + { + const GameSlot *slot = game->getConstSlot(i); + if (slot->isOccupied() && slot->getPlayerTemplate() != PLAYERTEMPLATE_OBSERVER) + ++numPlayersInGame; + } + for (i = 0; i < MAX_SLOTS; ++i) + { + GameSlot *slot = game->getSlot(i); + if (!slot || !slot->isOccupied() || slot->getPlayerTemplate() != PLAYERTEMPLATE_OBSERVER) + continue; + + Int posIdx = -1; + if (numPlayersInGame == 0) + { + posIdx = 0; + } + else + { + // Pick a random position that IS taken by a real player + for (Int attempt = 0; attempt < numPlayers * 2 && posIdx == -1; ++attempt) + { + Int candidate = rand() % numPlayers; + if (game->isStartPositionTaken(candidate)) + posIdx = candidate; + } + } + if (posIdx >= 0) + slot->setStartPos(posIdx); + } +} + +void performRandomAssign(GameInfo *game, const std::vector &lockedTemplates) +{ + if (!game) + return; + + srand(static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + + // Phase 1: Assign factions and colors for random slots + std::vector validTemplates; + buildValidTemplates(game, lockedTemplates, validTemplates); + assignRandomFactions(game, validTemplates); + + // Phase 2: Assign start positions using distance-based placement + assignRandomPositions(game); + + // Reset accepted state since we changed settings + game->resetAccepted(); +} + +std::vector buildLockedTemplates() +{ + std::vector lockedTemplates; + Int templateCount = ThePlayerTemplateStore->getPlayerTemplateCount(); + for (Int t = 0; t < templateCount; ++t) + { + const PlayerTemplate *fac = ThePlayerTemplateStore->getNthPlayerTemplate(t); + if (fac) + { + const GeneralPersona *general = TheChallengeGenerals->getGeneralByTemplateName(fac->getName()); + if (general && !general->isStartingEnabled()) + lockedTemplates.push_back(t); + } + } + return lockedTemplates; +}