From 98bb6459d5fc5d18df0cdf28cb6de62974a11d60 Mon Sep 17 00:00:00 2001 From: Bill Rich Date: Thu, 12 Mar 2026 16:30:38 +0000 Subject: [PATCH 1/8] 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 a84c0935e98..343f6652a15 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -1115,6 +1115,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 5e89ed8d313..0f4a9eab5b8 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h @@ -226,6 +226,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 53cda5a2a2e..1dc26985c7e 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 d8c0b2c2eba..c121de98a7b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -68,6 +68,8 @@ #include "GameNetwork/GameSpy/GSConfig.h" #include "GameNetwork/GeneralsOnline/NGMP_interfaces.h" +#include "GameNetwork/RandomAssign.h" +#include "GameClient/ChallengeGenerals.h" #include #include #include @@ -203,6 +205,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) @@ -220,6 +223,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; @@ -1620,6 +1624,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"); @@ -1633,6 +1638,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); @@ -1679,6 +1686,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 ); } @@ -1687,6 +1695,7 @@ void InitWOLGameGadgets() { checkBoxLimitSuperweapons->winEnable(true); comboBoxStartingCash->winEnable(true); + buttonRandomize->winEnable(true); } #endif @@ -1699,6 +1708,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 ); } @@ -1840,6 +1850,7 @@ void DeinitWOLGameGadgets() parentWOLGameSetup = NULL; buttonEmote = NULL; buttonSelectMap = NULL; + buttonRandomize = NULL; buttonStart = NULL; buttonBack = NULL; listboxGameSetupChat = NULL; @@ -2240,6 +2251,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(); @@ -2531,6 +2543,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); @@ -4137,6 +4150,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 4b5b9da9cbc..4323375203d 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(UnicodeString strMap, AsciiString strMapPath, bool bIsOfficial, int newMaxPlayers) @@ -493,6 +494,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; +} From b1ea80a4033e862a5eec59907314b17c9f963e7c Mon Sep 17 00:00:00 2001 From: Bill Rich Date: Tue, 15 Sep 2026 10:04:21 -0700 Subject: [PATCH 2/8] fix(gui): Harden the lobby Random button and announce every roll Announces each randomize to the whole lobby with a roll counter so re-rolling is visible to everyone present; the assignment itself keeps drawing from the clock-seeded CRT RNG, independent of the server-minted lobby seed that match start uses. Also clamps the start-spot tables to MAX_SLOTS, picks colors uniformly from the untaken set, null-guards the button lookups for older .wnd files, guards the auto-ready countdown reset like the other host updates, and adds the license headers and change tags. --- .../Include/GameNetwork/RandomAssign.h | 33 +++++++++++- .../GUICallbacks/Menus/LanGameOptionsMenu.cpp | 14 +++++- .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 21 ++++++-- .../OnlineServices_LobbyInterface.cpp | 3 ++ .../Source/GameNetwork/RandomAssign.cpp | 50 ++++++++++++++----- 5 files changed, 102 insertions(+), 19 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h index 9d9287b7bf7..be438aeaee7 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h @@ -1,8 +1,39 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** 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 . +*/ + +// TheSuperHackers @feature bill-rich 15/09/2026 Lobby "Randomize" button: resolves every slot still set +// to Random (faction, color, start position) in the lobby, so a group no longer has +// to start and abandon a match just to learn the engine's random draw. + #pragma once -class GameInfo; +#include "Lib/BaseType.h" #include +class GameInfo; + +// Resolve random factions, colors and start positions on every occupied slot of +// the lobby that still has them unset. Slots with explicit choices are left alone. +// Draws from the C runtime RNG seeded from the clock, deliberately NOT from the +// game-logic RNG, so the result says nothing about the draw the engine will make +// from the lobby seed at match start. void performRandomAssign(GameInfo *game, const std::vector &lockedTemplates); + +// Player-template indices of generals that are not unlocked on this client. 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 1dc26985c7e..864761c8dec 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp @@ -122,6 +122,9 @@ static GameWindow *parentLanGameOptions = nullptr; static GameWindow *buttonBack = nullptr; static GameWindow *buttonStart = nullptr; static GameWindow *buttonRandomize = nullptr; +// TheSuperHackers @feature bill-rich 15/09/2026 How many times the host pressed Randomize in this +// lobby; announced with every roll so re-rolling is visible to everyone present. +static Int s_randomizeCount = 0; static GameWindow *buttonSelectMap = nullptr; static GameWindow *buttonEmote = nullptr; static GameWindow *textEntryChat = nullptr; @@ -829,6 +832,7 @@ void DeinitLanGameGadgets() //------------------------------------------------------------------------------------------------- void LanGameOptionsMenuInit( WindowLayout *layout, void *userData ) { + s_randomizeCount = 0; if (TheLAN->GetMyGame() && TheLAN->GetMyGame()->isGameInProgress()) { // If we init while the game is in progress, we are really returning to the menu @@ -892,7 +896,8 @@ 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 ); + if (buttonRandomize) + 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 @@ -1285,12 +1290,19 @@ WindowMsgHandledType LanGameOptionsMenuSystem( GameWindow *window, UnsignedInt m } else if ( controlID == buttonRandomizeID ) { + // TheSuperHackers @feature bill-rich 15/09/2026 host-only Randomize: resolve the random slots, + // broadcast the new options and tell the room it happened. if (TheLAN->AmIHost()) { std::vector lockedTemplates = buildLockedTemplates(); performRandomAssign(TheLAN->GetMyGame(), lockedTemplates); TheLAN->RequestGameOptions(GenerateGameOptionsString(), true); lanUpdateSlotList(); + + ++s_randomizeCount; + UnicodeString strInform; + strInform.format(TheGameText->FETCH_OR_SUBSTITUTE("GUI:HostRandomizedSlots", L"Host randomized factions, colors and start positions (roll %d)"), s_randomizeCount); + TheLAN->RequestChat(strInform, LANAPIInterface::LANCHAT_SYSTEM); } } else if ( controlID == checkboxLimitSuperweaponsID ) 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 c121de98a7b..4275d49184f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -224,6 +224,9 @@ static GameWindow *buttonBack = NULL; static GameWindow *buttonStart = NULL; static GameWindow *buttonSelectMap = NULL; static GameWindow *buttonRandomize = NULL; +// TheSuperHackers @feature bill-rich 15/09/2026 How many times the host pressed Randomize in this +// lobby; announced with every roll so re-rolling is visible to everyone present. +static Int s_randomizeCount = 0; static GameWindow *buttonEmote = NULL; static GameWindow *textEntryChat = NULL; static GameWindow *textEntryMapDisplay = NULL; @@ -1686,7 +1689,7 @@ void InitWOLGameGadgets() { checkBoxLimitSuperweapons->winEnable( false ); comboBoxStartingCash->winEnable( false ); - buttonRandomize->winEnable( false ); + if (buttonRandomize) buttonRandomize->winEnable( false ); NameKeyType labelID = TheNameKeyGenerator->nameToKey("GameSpyGameOptionsMenu.wnd:StartingCashLabel"); TheWindowManager->winGetWindowFromId(parentWOLGameSetup, labelID)->winEnable( FALSE ); } @@ -1695,7 +1698,7 @@ void InitWOLGameGadgets() { checkBoxLimitSuperweapons->winEnable(true); comboBoxStartingCash->winEnable(true); - buttonRandomize->winEnable(true); + if (buttonRandomize) buttonRandomize->winEnable(true); } #endif @@ -1708,7 +1711,7 @@ void InitWOLGameGadgets() checkBoxLimitSuperweapons->winEnable( FALSE ); comboBoxStartingCash->winEnable( FALSE ); checkBoxLimitArmies->winEnable( FALSE ); - buttonRandomize->winEnable( FALSE ); + if (buttonRandomize) buttonRandomize->winEnable( FALSE ); NameKeyType labelID = TheNameKeyGenerator->nameToKey("GameSpyGameOptionsMenu.wnd:StartingCashLabel"); TheWindowManager->winGetWindowFromId(parentWOLGameSetup, labelID)->winEnable( FALSE ); } @@ -1889,6 +1892,7 @@ Bool initialAcceptEnable = FALSE; //------------------------------------------------------------------------------------------------- void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) { + s_randomizeCount = 0; NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); if (pLobbyInterface == nullptr) { @@ -2251,7 +2255,7 @@ void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) buttonStart->winSetText(TheGameText->fetch("GUI:Accept")); buttonStart->winEnable( FALSE ); buttonSelectMap->winEnable( FALSE ); - buttonRandomize->winEnable( FALSE ); + if (buttonRandomize) buttonRandomize->winEnable( FALSE ); initialAcceptEnable = FALSE; WOLDisplaySlotList(); @@ -2543,7 +2547,7 @@ void WOLGameSetupMenuUpdate( WindowLayout * layout, void *userData) buttonStart->winSetText(TheGameText->fetch("GUI:Start")); buttonStart->winEnable(TRUE); buttonSelectMap->winEnable(TRUE); - buttonRandomize->winEnable(TRUE); + if (buttonRandomize) buttonRandomize->winEnable(TRUE); initialAcceptEnable = TRUE; comboBoxStartingCash->winEnable(TRUE); @@ -4152,6 +4156,8 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg } else if ( controlID == buttonRandomizeID ) { + // TheSuperHackers @feature bill-rich 15/09/2026 host-only Randomize: resolve the random slots, + // push them to the lobby service and tell the room it happened. NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); if (pLobbyInterface != nullptr && pLobbyInterface->IsHost()) { @@ -4162,6 +4168,11 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg performRandomAssign(game, lockedTemplates); pLobbyInterface->UpdateCurrentLobby_BulkSlotUpdate(game); WOLDisplaySlotList(); + + ++s_randomizeCount; + UnicodeString strInform; + strInform.format(TheGameText->FETCH_OR_SUBSTITUTE("GUI:HostRandomizedSlots", L"Host randomized factions, colors and start positions (roll %d)"), s_randomizeCount); + pLobbyInterface->SendAnnouncementMessageToCurrentLobby(strInform, true); } } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index 4323375203d..00ce8d51bc7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -496,7 +496,10 @@ void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_ForceReady() void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_BulkSlotUpdate(NGMPGame* game) { + // reset autostart if host changes anything (because ready flag will reset too) +#if !defined(GENERALS_ONLINE_DISABLE_AUTO_ACCEPT) ClearAutoReadyCountdown(); +#endif if (TheNGMPGame && TheNGMPGame->IsCountdownStarted()) TheNGMPGame->StopCountdown(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp index e8267aef2a5..5912a4a09b9 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp @@ -1,3 +1,27 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** 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 . +*/ + +// TheSuperHackers @feature bill-rich 15/09/2026 Lobby "Randomize" button. The placement mirrors +// populateRandomSideAndColor / populateRandomStartPosition in GameLogic.cpp so the +// lobby result looks like what a match start would have produced, but it is drawn +// from an independent, clock-seeded RNG: nothing here consumes or predicts the +// game-logic RNG that match start seeds from the lobby seed. + #include "PreRTS.h" #include "GameNetwork/RandomAssign.h" @@ -9,7 +33,6 @@ #include #include -#include #include // Build the list of valid template indices for random assignment. @@ -71,19 +94,18 @@ static void assignRandomFactions(GameInfo *game, const std::vector &validTe Int colorIdx = slot->getColor(); if (colorIdx < 0 || colorIdx >= TheMultiplayerSettings->getNumColors()) { + // Pick uniformly among the colors nobody holds yet, so a full lobby + // never ends up with a slot left at -1 because a blind retry loop + // kept landing on taken colors. + std::vector freeColors; Int numColors = TheMultiplayerSettings->getNumColors(); - if (numColors > 0) + for (Int c = 0; c < numColors; ++c) { - 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); + if (!game->isColorTaken(c)) + freeColors.push_back(c); } + if (!freeColors.empty()) + slot->setColor(freeColors[rand() % freeColors.size()]); } } } @@ -102,6 +124,9 @@ static void assignRandomPositions(GameInfo *game) if (numPlayers <= 0) return; + // The per-spot tables below are MAX_SLOTS wide; never index past them. + if (numPlayers > MAX_SLOTS) + numPlayers = MAX_SLOTS; // Build distance matrix between all start positions using map waypoints static const WaypointMap s_emptyWaypoints = {}; @@ -298,7 +323,8 @@ void performRandomAssign(GameInfo *game, const std::vector &lockedTemplates if (!game) return; - srand(static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + // Clock-seeded CRT RNG, independent of the game-logic RNG (see header). + srand((unsigned int)timeGetTime()); // Phase 1: Assign factions and colors for random slots std::vector validTemplates; From ce8b574f969c89da75b016d623ec04a735ff4b79 Mon Sep 17 00:00:00 2001 From: Bill Rich Date: Tue, 15 Sep 2026 15:00:18 -0700 Subject: [PATCH 3/8] fix(gui): Label the Random button without a text table change The button text falls back to an English default when GUI:Randomize is missing from the text table, so the layout no longer depends on a .csf update. --- .../GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp | 2 ++ .../GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp | 2 ++ 2 files changed, 4 insertions(+) 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 864761c8dec..429d20cfec9 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp @@ -701,6 +701,8 @@ void InitLanGameGadgets() DEBUG_ASSERTCRASH(buttonStart, ("Could not find the buttonStart")); buttonRandomize = TheWindowManager->winGetWindowFromId( parentLanGameOptions, buttonRandomizeID ); DEBUG_ASSERTCRASH(buttonRandomize, ("Could not find the buttonRandomize")); + if (buttonRandomize) + buttonRandomize->winSetText(TheGameText->FETCH_OR_SUBSTITUTE("GUI:Randomize", L"Randomize")); // no .csf change needed buttonBack = TheWindowManager->winGetWindowFromId( parentLanGameOptions, buttonBackID); DEBUG_ASSERTCRASH(buttonBack, ("Could not find the buttonBack")); listboxChatWindowLanGame = TheWindowManager->winGetWindowFromId( parentLanGameOptions, listboxChatWindowLanGameID ); 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 4275d49184f..d90321021cf 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -1643,6 +1643,8 @@ void InitWOLGameGadgets() buttonSelectMap = TheWindowManager->winGetWindowFromId( parentWOLGameSetup,buttonSelectMapID ); buttonRandomize = TheWindowManager->winGetWindowFromId( parentWOLGameSetup, buttonRandomizeID ); DEBUG_ASSERTCRASH(buttonRandomize, ("Could not find the buttonRandomize")); + if (buttonRandomize) + buttonRandomize->winSetText(TheGameText->FETCH_OR_SUBSTITUTE("GUI:Randomize", L"Randomize")); // no .csf change needed checkBoxUseStats = TheWindowManager->winGetWindowFromId( parentWOLGameSetup, checkBoxUseStatsID ); buttonStart = TheWindowManager->winGetWindowFromId( parentWOLGameSetup,buttonStartID ); buttonBack = TheWindowManager->winGetWindowFromId( parentWOLGameSetup, buttonBackID); From b13e11780ea4c588e927961dd81fb3189eb3ca32 Mon Sep 17 00:00:00 2001 From: Bill Rich Date: Thu, 24 Sep 2026 08:36:58 -0700 Subject: [PATCH 4/8] fix(gui): Roll Randomize on a scratch copy online and send only what changed Review follow-up. Online, the lobby service owns the slots, so the roll is computed on a scratch copy of the game slots, only the slots the roll changed are sent (with the occupant's user id so the service can skip a slot that changed hands, and without the team field the roll never touches), and the announcement waits for the service to accept the update; on failure the local slots are re-synced from the lobby. Observer slots are left alone (match start resolves them), an unknown map hands out no start spots, the roll draws from a private mt19937 instead of reseeding the CRT RNG, locked generals are filtered with the same test the faction combo uses, a rolled host faction is not persisted as the preferred one, the button lookups no longer assert on older layouts, and the announcements follow the Topic: message style. --- .../OnlineServices_LobbyInterface.h | 6 +- .../Include/GameNetwork/RandomAssign.h | 25 ++-- .../GUICallbacks/Menus/LanGameOptionsMenu.cpp | 28 ++-- .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 76 +++++++++-- .../OnlineServices_LobbyInterface.cpp | 41 ++++-- .../Source/GameNetwork/RandomAssign.cpp | 126 ++++++++---------- 6 files changed, 181 insertions(+), 121 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h index 0f4a9eab5b8..3bee3441ec9 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h @@ -1,5 +1,7 @@ #pragma once +#include "GameNetwork/RandomAssign.h" + #include "NGMP_include.h" #include "OnlineServices_RoomsInterface.h" #include "GameNetwork/GameInfo.h" @@ -226,7 +228,9 @@ class NGMP_OnlineServices_LobbyInterface void UpdateCurrentLobby_AITeam(int slot, int team); void UpdateCurrentLobby_AIStartPos(int slot, int startpos); - void UpdateCurrentLobby_BulkSlotUpdate(NGMPGame* game); + // TheSuperHackers @feature bill-rich 15/09/2026 Randomize: send the slots a roll changed (only the fields it + // changed, with the occupant's user id so the service can skip a slot that changed hands) and report the outcome. + void UpdateCurrentLobby_BulkSlotUpdate(const std::vector& changes, std::function onDone); void UpdateCurrentLobbyMaxCameraHeight(uint16_t maxCameraHeight); diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h index be438aeaee7..64cbf35303e 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h @@ -28,12 +28,21 @@ class GameInfo; -// Resolve random factions, colors and start positions on every occupied slot of -// the lobby that still has them unset. Slots with explicit choices are left alone. -// Draws from the C runtime RNG seeded from the clock, deliberately NOT from the -// game-logic RNG, so the result says nothing about the draw the engine will make -// from the lobby seed at match start. -void performRandomAssign(GameInfo *game, const std::vector &lockedTemplates); +// One slot the roll changed. A field left at -1 was not touched by the roll. +struct RandomSlotAssignment +{ + Int slotIndex = -1; + Int side = -1; + Int color = -1; + Int startPos = -1; +}; -// Player-template indices of generals that are not unlocked on this client. -std::vector buildLockedTemplates(); +// Resolve random factions, colors and start positions on every occupied player +// slot of the lobby that still has them unset. Slots with explicit choices and +// observer slots are left alone (observers are resolved at match start, as in +// retail). Draws from a private RNG seeded from the clock, deliberately NOT from +// the game-logic RNG, so the result says nothing about the draw the engine will +// make from the lobby seed at match start. outChanges, when given, receives the +// slots that changed and what changed, for callers that must push the roll +// through an authoritative service instead of applying it locally. +void performRandomAssign(GameInfo *game, std::vector *outChanges = nullptr); 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 429d20cfec9..cba2ce2be5f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp @@ -699,8 +699,7 @@ 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")); + buttonRandomize = TheWindowManager->winGetWindowFromId( parentLanGameOptions, buttonRandomizeID ); // absent from older .wnd layouts if (buttonRandomize) buttonRandomize->winSetText(TheGameText->FETCH_OR_SUBSTITUTE("GUI:Randomize", L"Randomize")); // no .csf change needed buttonBack = TheWindowManager->winGetWindowFromId( parentLanGameOptions, buttonBackID); @@ -1296,15 +1295,22 @@ WindowMsgHandledType LanGameOptionsMenuSystem( GameWindow *window, UnsignedInt m // broadcast the new options and tell the room it happened. if (TheLAN->AmIHost()) { - std::vector lockedTemplates = buildLockedTemplates(); - performRandomAssign(TheLAN->GetMyGame(), lockedTemplates); - TheLAN->RequestGameOptions(GenerateGameOptionsString(), true); - lanUpdateSlotList(); - - ++s_randomizeCount; - UnicodeString strInform; - strInform.format(TheGameText->FETCH_OR_SUBSTITUTE("GUI:HostRandomizedSlots", L"Host randomized factions, colors and start positions (roll %d)"), s_randomizeCount); - TheLAN->RequestChat(strInform, LANAPIInterface::LANCHAT_SYSTEM); + std::vector changes; + performRandomAssign(TheLAN->GetMyGame(), &changes); + if (changes.empty()) + { + TheLAN->RequestChat(TheGameText->FETCH_OR_SUBSTITUTE("GUI:RandomizeNothingToDo", L"Randomize: every slot already has a faction, color and start position"), LANAPIInterface::LANCHAT_SYSTEM); + } + else + { + TheLAN->RequestGameOptions(GenerateGameOptionsString(), true); + lanUpdateSlotList(); + + ++s_randomizeCount; + UnicodeString strInform; + strInform.format(TheGameText->FETCH_OR_SUBSTITUTE("GUI:HostRandomizedSlots", L"Randomize: the host resolved the random factions, colors and start positions (roll %d)"), s_randomizeCount); + TheLAN->RequestChat(strInform, LANAPIInterface::LANCHAT_SYSTEM); + } } } else if ( controlID == checkboxLimitSuperweaponsID ) 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 d90321021cf..7f48e942657 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -69,7 +69,6 @@ #include "GameNetwork/GeneralsOnline/NGMP_interfaces.h" #include "GameNetwork/RandomAssign.h" -#include "GameClient/ChallengeGenerals.h" #include #include #include @@ -227,6 +226,9 @@ static GameWindow *buttonRandomize = NULL; // TheSuperHackers @feature bill-rich 15/09/2026 How many times the host pressed Randomize in this // lobby; announced with every roll so re-rolling is visible to everyone present. static Int s_randomizeCount = 0; +// TheSuperHackers @feature bill-rich 24/09/2026 True while the host's own faction was set by Randomize rather +// than chosen, so Back/Start does not persist a rolled faction as the preferred one. +static Bool s_hostFactionFromRandomize = FALSE; static GameWindow *buttonEmote = NULL; static GameWindow *textEntryChat = NULL; static GameWindow *textEntryMapDisplay = NULL; @@ -368,7 +370,8 @@ static void savePlayerInfo() // save off some prefs CustomMatchPreferences pref; pref.setPreferredColor(slot->getColor()); - pref.setPreferredFaction(slot->getPlayerTemplate()); + if (!s_hostFactionFromRandomize) + pref.setPreferredFaction(slot->getPlayerTemplate()); if (TheNGMPGame->amIHost()) { pref.setPreferredMap(TheNGMPGame->getMap()); @@ -754,6 +757,8 @@ static void handleColorSelection(int index) static void handlePlayerTemplateSelection(int index, bool bInitialSetup = false) { + if (!bInitialSetup && TheNGMPGame && index == TheNGMPGame->getLocalSlotNum()) + s_hostFactionFromRandomize = FALSE; // an explicit choice is worth remembering again GameWindow *combo = comboBoxPlayerTemplate[index]; Int playerTemplate, selIndex; GadgetComboBoxGetSelectedPos(combo, &selIndex); @@ -1641,8 +1646,7 @@ 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")); + buttonRandomize = TheWindowManager->winGetWindowFromId( parentWOLGameSetup, buttonRandomizeID ); // absent from older .wnd layouts if (buttonRandomize) buttonRandomize->winSetText(TheGameText->FETCH_OR_SUBSTITUTE("GUI:Randomize", L"Randomize")); // no .csf change needed checkBoxUseStats = TheWindowManager->winGetWindowFromId( parentWOLGameSetup, checkBoxUseStatsID ); @@ -1895,6 +1899,7 @@ Bool initialAcceptEnable = FALSE; void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) { s_randomizeCount = 0; + s_hostFactionFromRandomize = FALSE; NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); if (pLobbyInterface == nullptr) { @@ -4158,23 +4163,66 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg } else if ( controlID == buttonRandomizeID ) { - // TheSuperHackers @feature bill-rich 15/09/2026 host-only Randomize: resolve the random slots, - // push them to the lobby service and tell the room it happened. + // TheSuperHackers @feature bill-rich 15/09/2026 host-only Randomize. The lobby service owns the + // slots, so the roll is computed on a scratch copy, only the slots it changed are sent, and + // the room is told once the service has accepted them; the next lobby push shows the result. 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(); + GameSlot saved[MAX_SLOTS]; + Int i; + for (i = 0; i < MAX_SLOTS; ++i) + { + if (game->getSlot(i)) + saved[i] = *game->getSlot(i); + } + std::vector changes; + performRandomAssign(game, &changes); + for (i = 0; i < MAX_SLOTS; ++i) + { + if (game->getSlot(i)) + *game->getSlot(i) = saved[i]; + } - ++s_randomizeCount; - UnicodeString strInform; - strInform.format(TheGameText->FETCH_OR_SUBSTITUTE("GUI:HostRandomizedSlots", L"Host randomized factions, colors and start positions (roll %d)"), s_randomizeCount); - pLobbyInterface->SendAnnouncementMessageToCurrentLobby(strInform, true); + if (changes.empty()) + { + UnicodeString strNothing = TheGameText->FETCH_OR_SUBSTITUTE("GUI:RandomizeNothingToDo", L"Randomize: every slot already has a faction, color and start position"); + pLobbyInterface->SendAnnouncementMessageToCurrentLobby(strNothing, true); + } + else + { + const Int localSlot = game->getLocalSlotNum(); + Bool hostRolled = FALSE; + for (const RandomSlotAssignment& change : changes) + { + if (change.slotIndex == localSlot && change.side != -1) + hostRolled = TRUE; + } + if (buttonRandomize) + buttonRandomize->winEnable(FALSE); // one roll in flight at a time; re-enabled by the lobby push + pLobbyInterface->UpdateCurrentLobby_BulkSlotUpdate(changes, [pLobbyInterface, hostRolled](bool bSuccess) + { + if (bSuccess) + { + ++s_randomizeCount; + if (hostRolled) + s_hostFactionFromRandomize = TRUE; + UnicodeString strInform; + strInform.format(TheGameText->FETCH_OR_SUBSTITUTE("GUI:HostRandomizedSlots", L"Randomize: the host resolved the random factions, colors and start positions (roll %d)"), s_randomizeCount); + pLobbyInterface->SendAnnouncementMessageToCurrentLobby(strInform, true); + } + else if (TheNGMPGame != nullptr) + { + TheNGMPGame->UpdateSlotsFromCurrentLobby(); + WOLDisplaySlotList(); + if (buttonRandomize) + buttonRandomize->winEnable(TRUE); + } + }); + } } } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index 00ce8d51bc7..6dadff624cb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -494,8 +494,9 @@ void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_ForceReady() }); } -void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_BulkSlotUpdate(NGMPGame* game) +void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_BulkSlotUpdate(const std::vector& changes, std::function onDone) { + // TheSuperHackers @feature bill-rich 15/09/2026 Randomize: one host action for all rolled slots. // reset autostart if host changes anything (because ready flag will reset too) #if !defined(GENERALS_ONLINE_DISABLE_AUTO_ACCEPT) ClearAutoReadyCountdown(); @@ -510,29 +511,41 @@ void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_BulkSlotUpdate(NGMPG j["field"] = ELobbyUpdateField::HOST_ACTION_BULK_SLOT_UPDATE; nlohmann::json slotsArray = nlohmann::json::array(); - for (int i = 0; i < MAX_SLOTS; ++i) + for (const RandomSlotAssignment& change : changes) { - const GameSlot *slot = game->getConstSlot(i); - if (slot && slot->isOccupied()) + // AI slots are members too (with the service's placeholder user id), so match on + // the slot rather than on a real user id. + const LobbyMemberEntry* occupant = nullptr; + for (const LobbyMemberEntry& member : m_CurrentLobby.members) { - 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); + if (member.m_SlotIndex == change.slotIndex) + occupant = &member; } + if (occupant == nullptr) + continue; // nobody in that slot any more + nlohmann::json slotEntry; + slotEntry["slot_index"] = change.slotIndex; + slotEntry["user_id"] = occupant->user_id; + if (change.side != -1) + slotEntry["side"] = change.side; + if (change.color != -1) + slotEntry["color"] = change.color; + if (change.startPos != -1) + slotEntry["start_pos"] = change.startPos; + 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) + NGMP_OnlineServicesManager::GetInstance()->GetHTTPManager()->SendPOSTRequest(strURI.c_str(), EIPProtocolVersion::DONT_CARE, mapHeaders, strPostData.c_str(), [onDone](bool bSuccess, int statusCode, std::string strBody, HTTPRequest* pReq) { - if (!bSuccess || statusCode < 200 || statusCode >= 300) + const bool bOK = bSuccess && statusCode >= 200 && statusCode < 300; + if (!bOK) { - DEBUG_LOG(("UpdateCurrentLobby_BulkSlotUpdate failed: success=%d, status=%d", bSuccess, statusCode)); + NetworkLog(ELogVerbosity::LOG_RELEASE, "UpdateCurrentLobby_BulkSlotUpdate failed: success=%d, status=%d", bSuccess, statusCode); } + if (onDone) + onDone(bOK); }); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp index 5912a4a09b9..0f53ba2f8f7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp @@ -31,13 +31,21 @@ #include "GameClient/MapUtil.h" #include "GameClient/ChallengeGenerals.h" -#include #include +#include #include +// Private RNG for the roll: seeded from the clock, never the game-logic RNG, and +// not the process CRT RNG either so the roll has no side effect on anything else. +static Int rollBelow(Int n) +{ + static std::mt19937 s_rng((unsigned int)std::random_device{}() ^ (unsigned int)timeGetTime()); + return (n > 0) ? (Int)(s_rng() % (unsigned int)n) : 0; +} + // 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) +static void buildValidTemplates(const GameInfo *game, std::vector &out) { Int count = ThePlayerTemplateStore->getPlayerTemplateCount(); for (Int c = 0; c < count; ++c) @@ -54,17 +62,10 @@ static void buildValidTemplates(const GameInfo *game, const std::vector &lo 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) + // Skip generals that are not unlocked on this client, the same test the + // lobby faction combo applies (GUIUtil.cpp) and match start enforces. + const GeneralPersona *general = TheChallengeGenerals->getGeneralByTemplateName(fac->getName()); + if (general && !general->isStartingEnabled()) continue; out.push_back(c); @@ -79,14 +80,14 @@ static void assignRandomFactions(GameInfo *game, const std::vector &validTe for (Int i = 0; i < MAX_SLOTS; ++i) { GameSlot *slot = game->getSlot(i); - if (!slot || !slot->isOccupied()) + if (!slot || !slot->isOccupied() || slot->getPlayerTemplate() == PLAYERTEMPLATE_OBSERVER) continue; // Assign faction if random Int playerTemplateIdx = slot->getPlayerTemplate(); if (playerTemplateIdx == PLAYERTEMPLATE_RANDOM && !validTemplates.empty()) { - playerTemplateIdx = validTemplates[rand() % validTemplates.size()]; + playerTemplateIdx = validTemplates[rollBelow((Int)validTemplates.size())]; slot->setPlayerTemplate(playerTemplateIdx); } @@ -105,7 +106,7 @@ static void assignRandomFactions(GameInfo *game, const std::vector &validTe freeColors.push_back(c); } if (!freeColors.empty()) - slot->setColor(freeColors[rand() % freeColors.size()]); + slot->setColor(freeColors[rollBelow((Int)freeColors.size())]); } } } @@ -117,10 +118,10 @@ static void assignRandomFactions(GameInfo *game, const std::vector &validTe 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 (!md) + return; // unknown map: no start spots to hand out; match start will do it + Int numPlayers = md->m_numPlayers; if (numPlayers <= 0) return; @@ -129,8 +130,7 @@ static void assignRandomPositions(GameInfo *game) numPlayers = MAX_SLOTS; // Build distance matrix between all start positions using map waypoints - static const WaypointMap s_emptyWaypoints = {}; - const WaypointMap &waypoints = md ? md->m_waypoints : s_emptyWaypoints; + const WaypointMap &waypoints = md->m_waypoints; Real startSpotDistance[MAX_SLOTS][MAX_SLOTS]; for (i = 0; i < MAX_SLOTS; ++i) { @@ -220,7 +220,7 @@ static void assignRandomPositions(GameInfo *game) posIdx = -1; for (Int attempt = 0; attempt < numPlayers * 2 && posIdx == -1; ++attempt) { - Int candidate = rand() % numPlayers; + Int candidate = rollBelow(numPlayers); if (!taken[candidate]) posIdx = candidate; } @@ -284,73 +284,53 @@ static void assignRandomPositions(GameInfo *game) } } - // 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); - } + // Observer slots are left alone: they take a player's spot at match start. } -void performRandomAssign(GameInfo *game, const std::vector &lockedTemplates) +void performRandomAssign(GameInfo *game, std::vector *outChanges) { if (!game) return; - // Clock-seeded CRT RNG, independent of the game-logic RNG (see header). - srand((unsigned int)timeGetTime()); + Int before[MAX_SLOTS][3]; + Int i; + for (i = 0; i < MAX_SLOTS; ++i) + { + const GameSlot *slot = game->getConstSlot(i); + before[i][0] = slot ? slot->getPlayerTemplate() : -1; + before[i][1] = slot ? slot->getColor() : -1; + before[i][2] = slot ? slot->getStartPos() : -1; + } // Phase 1: Assign factions and colors for random slots std::vector validTemplates; - buildValidTemplates(game, lockedTemplates, validTemplates); + buildValidTemplates(game, 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) + if (outChanges) { - const PlayerTemplate *fac = ThePlayerTemplateStore->getNthPlayerTemplate(t); - if (fac) + outChanges->clear(); + for (i = 0; i < MAX_SLOTS; ++i) { - const GeneralPersona *general = TheChallengeGenerals->getGeneralByTemplateName(fac->getName()); - if (general && !general->isStartingEnabled()) - lockedTemplates.push_back(t); + const GameSlot *slot = game->getConstSlot(i); + if (!slot) + continue; + RandomSlotAssignment change; + change.slotIndex = i; + if (slot->getPlayerTemplate() != before[i][0]) + change.side = slot->getPlayerTemplate(); + if (slot->getColor() != before[i][1]) + change.color = slot->getColor(); + if (slot->getStartPos() != before[i][2]) + change.startPos = slot->getStartPos(); + if (change.side != -1 || change.color != -1 || change.startPos != -1) + outChanges->push_back(change); } } - return lockedTemplates; + + // Reset accepted state since we changed settings + game->resetAccepted(); } From 8d4c69e4f274a8cbf2bbdb9a4b3f7ae2b391c2c5 Mon Sep 17 00:00:00 2001 From: Bill Rich Date: Thu, 24 Sep 2026 15:15:26 -0700 Subject: [PATCH 5/8] fix(gui): Re-enable Randomize after a roll and show rolled colours on the host Lab run: the button was disabled for the duration of the request but only re-enabled on failure, so a lobby allowed exactly one roll. It now returns to whatever state it had when the roll started, which also keeps it disabled for a guest or a stats match. The host's lobby showed a remote player's rolled colour as ???, because a slot whose colour combo is disabled is never repopulated and the new colour was missing from the stale list; the selection now rebuilds the list once and looks again. A rolled colour is no longer persisted as the host's preferred colour either, which already applied to the faction. --- .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 30 +++++++++++-------- .../GameEngine/Source/GameNetwork/GUIUtil.cpp | 26 +++++++++++----- 2 files changed, 37 insertions(+), 19 deletions(-) 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 7f48e942657..50cb1cd4bd7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -226,9 +226,9 @@ static GameWindow *buttonRandomize = NULL; // TheSuperHackers @feature bill-rich 15/09/2026 How many times the host pressed Randomize in this // lobby; announced with every roll so re-rolling is visible to everyone present. static Int s_randomizeCount = 0; -// TheSuperHackers @feature bill-rich 24/09/2026 True while the host's own faction was set by Randomize rather -// than chosen, so Back/Start does not persist a rolled faction as the preferred one. -static Bool s_hostFactionFromRandomize = FALSE; +// TheSuperHackers @feature bill-rich 24/09/2026 True while the host's own faction and colour were set by +// Randomize rather than chosen, so Back/Start does not persist a rolled slot as the preferred one. +static Bool s_hostSlotFromRandomize = FALSE; static GameWindow *buttonEmote = NULL; static GameWindow *textEntryChat = NULL; static GameWindow *textEntryMapDisplay = NULL; @@ -369,9 +369,11 @@ static void savePlayerInfo() { // save off some prefs CustomMatchPreferences pref; - pref.setPreferredColor(slot->getColor()); - if (!s_hostFactionFromRandomize) + if (!s_hostSlotFromRandomize) + { + pref.setPreferredColor(slot->getColor()); pref.setPreferredFaction(slot->getPlayerTemplate()); + } if (TheNGMPGame->amIHost()) { pref.setPreferredMap(TheNGMPGame->getMap()); @@ -758,7 +760,7 @@ static void handleColorSelection(int index) static void handlePlayerTemplateSelection(int index, bool bInitialSetup = false) { if (!bInitialSetup && TheNGMPGame && index == TheNGMPGame->getLocalSlotNum()) - s_hostFactionFromRandomize = FALSE; // an explicit choice is worth remembering again + s_hostSlotFromRandomize = FALSE; // an explicit choice is worth remembering again GameWindow *combo = comboBoxPlayerTemplate[index]; Int playerTemplate, selIndex; GadgetComboBoxGetSelectedPos(combo, &selIndex); @@ -1899,7 +1901,7 @@ Bool initialAcceptEnable = FALSE; void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) { s_randomizeCount = 0; - s_hostFactionFromRandomize = FALSE; + s_hostSlotFromRandomize = FALSE; NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); if (pLobbyInterface == nullptr) { @@ -4201,15 +4203,19 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg if (change.slotIndex == localSlot && change.side != -1) hostRolled = TRUE; } + // The button is held down only while the request is in flight, and goes back to + // whatever it was when the roll started: a stats match or a guest must not end + // up with it enabled. + const Bool bWasEnabled = (buttonRandomize != NULL) && BitIsSet(buttonRandomize->winGetStatus(), WIN_STATUS_ENABLED); if (buttonRandomize) - buttonRandomize->winEnable(FALSE); // one roll in flight at a time; re-enabled by the lobby push - pLobbyInterface->UpdateCurrentLobby_BulkSlotUpdate(changes, [pLobbyInterface, hostRolled](bool bSuccess) + buttonRandomize->winEnable(FALSE); + pLobbyInterface->UpdateCurrentLobby_BulkSlotUpdate(changes, [pLobbyInterface, hostRolled, bWasEnabled](bool bSuccess) { if (bSuccess) { ++s_randomizeCount; if (hostRolled) - s_hostFactionFromRandomize = TRUE; + s_hostSlotFromRandomize = TRUE; UnicodeString strInform; strInform.format(TheGameText->FETCH_OR_SUBSTITUTE("GUI:HostRandomizedSlots", L"Randomize: the host resolved the random factions, colors and start positions (roll %d)"), s_randomizeCount); pLobbyInterface->SendAnnouncementMessageToCurrentLobby(strInform, true); @@ -4218,9 +4224,9 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg { TheNGMPGame->UpdateSlotsFromCurrentLobby(); WOLDisplaySlotList(); - if (buttonRandomize) - buttonRandomize->winEnable(TRUE); } + if (buttonRandomize) + buttonRandomize->winEnable(bWasEnabled); }); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp index 7e3231fcee1..503716b5337 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp @@ -507,17 +507,29 @@ void UpdateSlotList( GameInfo *myGame, GameWindow *comboPlayer[], //if( i == myGame->getLocalSlotNum()) if((comboColor[i] != nullptr) && BitIsSet(comboColor[i]->winGetStatus(), WIN_STATUS_ENABLED)) PopulateColorComboBox(i, comboColor, myGame, myGame->getConstSlot(i)->getPlayerTemplate() == PLAYERTEMPLATE_OBSERVER); - Int max, idx; + Int max, idx, attempt; if (comboColor[i] != nullptr) { - max = GadgetComboBoxGetLength(comboColor[i]); - for (idx=0; idxgetColor()) + max = GadgetComboBoxGetLength(comboColor[i]); + for (idx=0; idxgetColor()) + { + GadgetComboBoxSetSelectedPos(comboColor[i], idx, TRUE); + bFound = TRUE; + break; + } } + // TheSuperHackers @bugfix bill-rich 24/09/2026 A slot whose colour combo is disabled (every + // remote slot, on the host) is skipped by the populate above, so a colour it has not + // listed before is missing and the cell keeps showing whatever it showed last. Rebuild + // it once and look again, rather than leaving the host's lobby disagreeing with the + // service and with every other client. + if (!bFound && attempt == 0) + PopulateColorComboBox(i, comboColor, myGame, myGame->getConstSlot(i)->getPlayerTemplate() == PLAYERTEMPLATE_OBSERVER); } } From a8ab6a0fc9935d213328f418630914179dc50907 Mon Sep 17 00:00:00 2001 From: Bill Rich Date: Thu, 24 Sep 2026 15:41:36 -0700 Subject: [PATCH 6/8] fix(gui): Remember a faction and colour only when the player picked them The lobby stored whatever the local slot happened to hold when the player left or started, so a slot the host had rolled or moved became that player's remembered preference, on guests as well as the host. The earlier guard could not work: it was cleared from the combo callback, and the service push that delivers a roll fires those callbacks exactly like a click. The repaint driven by a push is now marked as such, and only a pick made here marks the slot as chosen; nothing else is persisted. --- .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) 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 50cb1cd4bd7..f815a1319d6 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -226,9 +226,13 @@ static GameWindow *buttonRandomize = NULL; // TheSuperHackers @feature bill-rich 15/09/2026 How many times the host pressed Randomize in this // lobby; announced with every roll so re-rolling is visible to everyone present. static Int s_randomizeCount = 0; -// TheSuperHackers @feature bill-rich 24/09/2026 True while the host's own faction and colour were set by -// Randomize rather than chosen, so Back/Start does not persist a rolled slot as the preferred one. -static Bool s_hostSlotFromRandomize = FALSE; +// TheSuperHackers @feature bill-rich 24/09/2026 The lobby remembers a faction and colour as the player's +// preference only when the player picked them here. A value that arrived from the service, because +// the host pressed Randomize or moved this slot, is not a choice and must not be stored. +// s_applyingLobbyUpdate marks the repaint that a service push drives: it fires the same combo +// callbacks as a real click, so without it the push that delivers a roll looks like a choice. +static Bool s_localSlotChosen = FALSE; +static Bool s_applyingLobbyUpdate = FALSE; static GameWindow *buttonEmote = NULL; static GameWindow *textEntryChat = NULL; static GameWindow *textEntryMapDisplay = NULL; @@ -369,7 +373,7 @@ static void savePlayerInfo() { // save off some prefs CustomMatchPreferences pref; - if (!s_hostSlotFromRandomize) + if (s_localSlotChosen) { pref.setPreferredColor(slot->getColor()); pref.setPreferredFaction(slot->getPlayerTemplate()); @@ -698,6 +702,8 @@ NameKeyType listboxGameSetupChatID = NAMEKEY_INVALID; static void handleColorSelection(int index) { + if (!s_applyingLobbyUpdate && TheNGMPGame && index == TheNGMPGame->getLocalSlotNum()) + s_localSlotChosen = TRUE; // picked here, so it is worth remembering GameWindow *combo = comboBoxColor[index]; Int color, selIndex; GadgetComboBoxGetSelectedPos(combo, &selIndex); @@ -759,8 +765,8 @@ static void handleColorSelection(int index) static void handlePlayerTemplateSelection(int index, bool bInitialSetup = false) { - if (!bInitialSetup && TheNGMPGame && index == TheNGMPGame->getLocalSlotNum()) - s_hostSlotFromRandomize = FALSE; // an explicit choice is worth remembering again + if (!bInitialSetup && !s_applyingLobbyUpdate && TheNGMPGame && index == TheNGMPGame->getLocalSlotNum()) + s_localSlotChosen = TRUE; // picked here, so it is worth remembering GameWindow *combo = comboBoxPlayerTemplate[index]; Int playerTemplate, selIndex; GadgetComboBoxGetSelectedPos(combo, &selIndex); @@ -1573,8 +1579,10 @@ void WOLDisplaySlotList(void) DEBUG_ASSERTCRASH(!game->getConstSlot(0)->isOpen(), ("Open host!")); + s_applyingLobbyUpdate = TRUE; UpdateSlotList(game, comboBoxPlayer, comboBoxColor, comboBoxPlayerTemplate, comboBoxTeam, buttonAccept, buttonStart, buttonMapStartPosition); + s_applyingLobbyUpdate = FALSE; WOLDisplayGameOptions(); @@ -1901,7 +1909,8 @@ Bool initialAcceptEnable = FALSE; void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) { s_randomizeCount = 0; - s_hostSlotFromRandomize = FALSE; + s_localSlotChosen = FALSE; + s_applyingLobbyUpdate = FALSE; NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); if (pLobbyInterface == nullptr) { @@ -4196,26 +4205,17 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg } else { - const Int localSlot = game->getLocalSlotNum(); - Bool hostRolled = FALSE; - for (const RandomSlotAssignment& change : changes) - { - if (change.slotIndex == localSlot && change.side != -1) - hostRolled = TRUE; - } // The button is held down only while the request is in flight, and goes back to // whatever it was when the roll started: a stats match or a guest must not end // up with it enabled. const Bool bWasEnabled = (buttonRandomize != NULL) && BitIsSet(buttonRandomize->winGetStatus(), WIN_STATUS_ENABLED); if (buttonRandomize) buttonRandomize->winEnable(FALSE); - pLobbyInterface->UpdateCurrentLobby_BulkSlotUpdate(changes, [pLobbyInterface, hostRolled, bWasEnabled](bool bSuccess) + pLobbyInterface->UpdateCurrentLobby_BulkSlotUpdate(changes, [pLobbyInterface, bWasEnabled](bool bSuccess) { if (bSuccess) { ++s_randomizeCount; - if (hostRolled) - s_hostSlotFromRandomize = TRUE; UnicodeString strInform; strInform.format(TheGameText->FETCH_OR_SUBSTITUTE("GUI:HostRandomizedSlots", L"Randomize: the host resolved the random factions, colors and start positions (roll %d)"), s_randomizeCount); pLobbyInterface->SendAnnouncementMessageToCurrentLobby(strInform, true); From 695c6a3c254287c919ed505a18a15e9cc0d5a942 Mon Sep 17 00:00:00 2001 From: Bill Rich Date: Thu, 5 Nov 2026 12:41:44 -0800 Subject: [PATCH 7/8] fix(gui): Tidy the Randomize roll and track colour and faction choices apart Pass the change list by reference, pick the first start spot directly since nothing is taken yet, reset the accepted flags in the LAN caller rather than inside the roll, and drop the re-pull on a failed online roll since the scratch copy was already restored. A chosen colour no longer makes a rolled faction count as a choice, and vice versa. --- .../Include/GameNetwork/RandomAssign.h | 8 +-- .../GUICallbacks/Menus/LanGameOptionsMenu.cpp | 3 +- .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 27 +++++----- .../Source/GameNetwork/RandomAssign.cpp | 50 +++++++------------ 4 files changed, 36 insertions(+), 52 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h index 64cbf35303e..b80d21416d8 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h @@ -42,7 +42,7 @@ struct RandomSlotAssignment // observer slots are left alone (observers are resolved at match start, as in // retail). Draws from a private RNG seeded from the clock, deliberately NOT from // the game-logic RNG, so the result says nothing about the draw the engine will -// make from the lobby seed at match start. outChanges, when given, receives the -// slots that changed and what changed, for callers that must push the roll -// through an authoritative service instead of applying it locally. -void performRandomAssign(GameInfo *game, std::vector *outChanges = nullptr); +// make from the lobby seed at match start. outChanges receives the slots that +// changed and what changed, for callers that must push the roll through an +// authoritative service instead of applying it locally. +void performRandomAssign(GameInfo *game, std::vector &outChanges); 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 cba2ce2be5f..34835296dbf 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp @@ -1296,13 +1296,14 @@ WindowMsgHandledType LanGameOptionsMenuSystem( GameWindow *window, UnsignedInt m if (TheLAN->AmIHost()) { std::vector changes; - performRandomAssign(TheLAN->GetMyGame(), &changes); + performRandomAssign(TheLAN->GetMyGame(), changes); if (changes.empty()) { TheLAN->RequestChat(TheGameText->FETCH_OR_SUBSTITUTE("GUI:RandomizeNothingToDo", L"Randomize: every slot already has a faction, color and start position"), LANAPIInterface::LANCHAT_SYSTEM); } else { + TheLAN->GetMyGame()->resetAccepted(); // settings changed, so everyone accepts again TheLAN->RequestGameOptions(GenerateGameOptionsString(), true); lanUpdateSlotList(); 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 f815a1319d6..d7aa1c4dddd 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -229,9 +229,11 @@ static Int s_randomizeCount = 0; // TheSuperHackers @feature bill-rich 24/09/2026 The lobby remembers a faction and colour as the player's // preference only when the player picked them here. A value that arrived from the service, because // the host pressed Randomize or moved this slot, is not a choice and must not be stored. +// Colour and faction are tracked apart, so picking one does not make a rolled value of the other a choice. // s_applyingLobbyUpdate marks the repaint that a service push drives: it fires the same combo // callbacks as a real click, so without it the push that delivers a roll looks like a choice. -static Bool s_localSlotChosen = FALSE; +static Bool s_localColorChosen = FALSE; +static Bool s_localFactionChosen = FALSE; static Bool s_applyingLobbyUpdate = FALSE; static GameWindow *buttonEmote = NULL; static GameWindow *textEntryChat = NULL; @@ -373,11 +375,10 @@ static void savePlayerInfo() { // save off some prefs CustomMatchPreferences pref; - if (s_localSlotChosen) - { + if (s_localColorChosen) pref.setPreferredColor(slot->getColor()); + if (s_localFactionChosen) pref.setPreferredFaction(slot->getPlayerTemplate()); - } if (TheNGMPGame->amIHost()) { pref.setPreferredMap(TheNGMPGame->getMap()); @@ -703,7 +704,7 @@ NameKeyType listboxGameSetupChatID = NAMEKEY_INVALID; static void handleColorSelection(int index) { if (!s_applyingLobbyUpdate && TheNGMPGame && index == TheNGMPGame->getLocalSlotNum()) - s_localSlotChosen = TRUE; // picked here, so it is worth remembering + s_localColorChosen = TRUE; // picked here, so it is worth remembering GameWindow *combo = comboBoxColor[index]; Int color, selIndex; GadgetComboBoxGetSelectedPos(combo, &selIndex); @@ -766,7 +767,7 @@ static void handleColorSelection(int index) static void handlePlayerTemplateSelection(int index, bool bInitialSetup = false) { if (!bInitialSetup && !s_applyingLobbyUpdate && TheNGMPGame && index == TheNGMPGame->getLocalSlotNum()) - s_localSlotChosen = TRUE; // picked here, so it is worth remembering + s_localFactionChosen = TRUE; // picked here, so it is worth remembering GameWindow *combo = comboBoxPlayerTemplate[index]; Int playerTemplate, selIndex; GadgetComboBoxGetSelectedPos(combo, &selIndex); @@ -1582,9 +1583,9 @@ void WOLDisplaySlotList(void) s_applyingLobbyUpdate = TRUE; UpdateSlotList(game, comboBoxPlayer, comboBoxColor, comboBoxPlayerTemplate, comboBoxTeam, buttonAccept, buttonStart, buttonMapStartPosition); - s_applyingLobbyUpdate = FALSE; - WOLDisplayGameOptions(); + WOLDisplayGameOptions(); // repopulates the army combos too, so still part of the push + s_applyingLobbyUpdate = FALSE; for (Int i = 0; i < MAX_SLOTS; ++i) { @@ -1909,7 +1910,8 @@ Bool initialAcceptEnable = FALSE; void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) { s_randomizeCount = 0; - s_localSlotChosen = FALSE; + s_localColorChosen = FALSE; + s_localFactionChosen = FALSE; s_applyingLobbyUpdate = FALSE; NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); if (pLobbyInterface == nullptr) @@ -4191,7 +4193,7 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg saved[i] = *game->getSlot(i); } std::vector changes; - performRandomAssign(game, &changes); + performRandomAssign(game, changes); for (i = 0; i < MAX_SLOTS; ++i) { if (game->getSlot(i)) @@ -4220,11 +4222,6 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg strInform.format(TheGameText->FETCH_OR_SUBSTITUTE("GUI:HostRandomizedSlots", L"Randomize: the host resolved the random factions, colors and start positions (roll %d)"), s_randomizeCount); pLobbyInterface->SendAnnouncementMessageToCurrentLobby(strInform, true); } - else if (TheNGMPGame != nullptr) - { - TheNGMPGame->UpdateSlotsFromCurrentLobby(); - WOLDisplaySlotList(); - } if (buttonRandomize) buttonRandomize->winEnable(bWasEnabled); }); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp index 0f53ba2f8f7..fac0df506e2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp @@ -216,16 +216,8 @@ static void assignRandomPositions(GameInfo *game) if (!hasStartSpotBeenPicked) { - // First player: pick randomly - posIdx = -1; - for (Int attempt = 0; attempt < numPlayers * 2 && posIdx == -1; ++attempt) - { - Int candidate = rollBelow(numPlayers); - if (!taken[candidate]) - posIdx = candidate; - } - if (posIdx < 0) - continue; + // First player: nothing is taken yet, so any spot is free + posIdx = rollBelow(numPlayers); hasStartSpotBeenPicked = TRUE; slot->setStartPos(posIdx); taken[posIdx] = TRUE; @@ -287,7 +279,7 @@ static void assignRandomPositions(GameInfo *game) // Observer slots are left alone: they take a player's spot at match start. } -void performRandomAssign(GameInfo *game, std::vector *outChanges) +void performRandomAssign(GameInfo *game, std::vector &outChanges) { if (!game) return; @@ -310,27 +302,21 @@ void performRandomAssign(GameInfo *game, std::vector *outC // Phase 2: Assign start positions using distance-based placement assignRandomPositions(game); - if (outChanges) + outChanges.clear(); + for (i = 0; i < MAX_SLOTS; ++i) { - outChanges->clear(); - for (i = 0; i < MAX_SLOTS; ++i) - { - const GameSlot *slot = game->getConstSlot(i); - if (!slot) - continue; - RandomSlotAssignment change; - change.slotIndex = i; - if (slot->getPlayerTemplate() != before[i][0]) - change.side = slot->getPlayerTemplate(); - if (slot->getColor() != before[i][1]) - change.color = slot->getColor(); - if (slot->getStartPos() != before[i][2]) - change.startPos = slot->getStartPos(); - if (change.side != -1 || change.color != -1 || change.startPos != -1) - outChanges->push_back(change); - } + const GameSlot *slot = game->getConstSlot(i); + if (!slot) + continue; + RandomSlotAssignment change; + change.slotIndex = i; + if (slot->getPlayerTemplate() != before[i][0]) + change.side = slot->getPlayerTemplate(); + if (slot->getColor() != before[i][1]) + change.color = slot->getColor(); + if (slot->getStartPos() != before[i][2]) + change.startPos = slot->getStartPos(); + if (change.side != -1 || change.color != -1 || change.startPos != -1) + outChanges.push_back(change); } - - // Reset accepted state since we changed settings - game->resetAccepted(); } From fd6dae06dece4e063da233f84f3a99fcbf0a4744 Mon Sep 17 00:00:00 2001 From: Bill Rich Date: Thu, 5 Nov 2026 15:37:10 -0800 Subject: [PATCH 8/8] chore(lobby): Drop the TheSuperHackers comment tags from the Randomize change --- .../GeneralsOnline/OnlineServices_LobbyInterface.h | 2 +- .../Code/GameEngine/Include/GameNetwork/RandomAssign.h | 2 +- .../GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp | 4 ++-- .../GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp | 6 +++--- GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp | 2 +- .../GeneralsOnline/OnlineServices_LobbyInterface.cpp | 2 +- .../Code/GameEngine/Source/GameNetwork/RandomAssign.cpp | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h index 3bee3441ec9..51a627bddad 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h @@ -228,7 +228,7 @@ class NGMP_OnlineServices_LobbyInterface void UpdateCurrentLobby_AITeam(int slot, int team); void UpdateCurrentLobby_AIStartPos(int slot, int startpos); - // TheSuperHackers @feature bill-rich 15/09/2026 Randomize: send the slots a roll changed (only the fields it + // Randomize: send the slots a roll changed (only the fields it // changed, with the occupant's user id so the service can skip a slot that changed hands) and report the outcome. void UpdateCurrentLobby_BulkSlotUpdate(const std::vector& changes, std::function onDone); diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h index b80d21416d8..b445a889db1 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h @@ -16,7 +16,7 @@ ** along with this program. If not, see . */ -// TheSuperHackers @feature bill-rich 15/09/2026 Lobby "Randomize" button: resolves every slot still set +// Lobby "Randomize" button: resolves every slot still set // to Random (faction, color, start position) in the lobby, so a group no longer has // to start and abandon a match just to learn the engine's random draw. 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 34835296dbf..c4f7d06147a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/LanGameOptionsMenu.cpp @@ -122,7 +122,7 @@ static GameWindow *parentLanGameOptions = nullptr; static GameWindow *buttonBack = nullptr; static GameWindow *buttonStart = nullptr; static GameWindow *buttonRandomize = nullptr; -// TheSuperHackers @feature bill-rich 15/09/2026 How many times the host pressed Randomize in this +// How many times the host pressed Randomize in this // lobby; announced with every roll so re-rolling is visible to everyone present. static Int s_randomizeCount = 0; static GameWindow *buttonSelectMap = nullptr; @@ -1291,7 +1291,7 @@ WindowMsgHandledType LanGameOptionsMenuSystem( GameWindow *window, UnsignedInt m } else if ( controlID == buttonRandomizeID ) { - // TheSuperHackers @feature bill-rich 15/09/2026 host-only Randomize: resolve the random slots, + // Host-only Randomize: resolve the random slots, // broadcast the new options and tell the room it happened. if (TheLAN->AmIHost()) { 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 d7aa1c4dddd..696167b20fe 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -223,10 +223,10 @@ static GameWindow *buttonBack = NULL; static GameWindow *buttonStart = NULL; static GameWindow *buttonSelectMap = NULL; static GameWindow *buttonRandomize = NULL; -// TheSuperHackers @feature bill-rich 15/09/2026 How many times the host pressed Randomize in this +// How many times the host pressed Randomize in this // lobby; announced with every roll so re-rolling is visible to everyone present. static Int s_randomizeCount = 0; -// TheSuperHackers @feature bill-rich 24/09/2026 The lobby remembers a faction and colour as the player's +// The lobby remembers a faction and colour as the player's // preference only when the player picked them here. A value that arrived from the service, because // the host pressed Randomize or moved this slot, is not a choice and must not be stored. // Colour and faction are tracked apart, so picking one does not make a rolled value of the other a choice. @@ -4176,7 +4176,7 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg } else if ( controlID == buttonRandomizeID ) { - // TheSuperHackers @feature bill-rich 15/09/2026 host-only Randomize. The lobby service owns the + // Host-only Randomize. The lobby service owns the // slots, so the roll is computed on a scratch copy, only the slots it changed are sent, and // the room is told once the service has accepted them; the next lobby push shows the result. NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp index 503716b5337..a04f23cdc75 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GUIUtil.cpp @@ -523,7 +523,7 @@ void UpdateSlotList( GameInfo *myGame, GameWindow *comboPlayer[], break; } } - // TheSuperHackers @bugfix bill-rich 24/09/2026 A slot whose colour combo is disabled (every + // A slot whose colour combo is disabled (every // remote slot, on the host) is skipped by the populate above, so a colour it has not // listed before is missing and the cell keeps showing whatever it showed last. Rebuild // it once and look again, rather than leaving the host's lobby disagreeing with the diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index 6dadff624cb..7bae5d54291 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -496,7 +496,7 @@ void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_ForceReady() void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_BulkSlotUpdate(const std::vector& changes, std::function onDone) { - // TheSuperHackers @feature bill-rich 15/09/2026 Randomize: one host action for all rolled slots. + // Randomize: one host action for all rolled slots. // reset autostart if host changes anything (because ready flag will reset too) #if !defined(GENERALS_ONLINE_DISABLE_AUTO_ACCEPT) ClearAutoReadyCountdown(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp index fac0df506e2..6c6d814667f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/RandomAssign.cpp @@ -16,7 +16,7 @@ ** along with this program. If not, see . */ -// TheSuperHackers @feature bill-rich 15/09/2026 Lobby "Randomize" button. The placement mirrors +// Lobby "Randomize" button. The placement mirrors // populateRandomSideAndColor / populateRandomStartPosition in GameLogic.cpp so the // lobby result looks like what a match start would have produced, but it is drawn // from an independent, clock-seeded RNG: nothing here consumes or predicts the