Skip to content
Open
1 change: 1 addition & 0 deletions GeneralsMD/Code/GameEngine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include "GameNetwork/RandomAssign.h"

#include "NGMP_include.h"
#include "OnlineServices_RoomsInterface.h"
#include "GameNetwork/GameInfo.h"
Expand Down Expand Up @@ -226,6 +228,10 @@ class NGMP_OnlineServices_LobbyInterface
void UpdateCurrentLobby_AITeam(int slot, int team);
void UpdateCurrentLobby_AIStartPos(int slot, int startpos);

// 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<RandomSlotAssignment>& changes, std::function<void(bool bSuccess)> onDone);

void UpdateCurrentLobbyMaxCameraHeight(uint16_t maxCameraHeight);

void SetJoinability(ELobbyJoinability joinabilityFlag);
Expand Down
48 changes: 48 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/GameNetwork/RandomAssign.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
** 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 <http://www.gnu.org/licenses/>.
*/

// 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

#include "Lib/BaseType.h"

#include <vector>

class GameInfo;

// 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;
};

// 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 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<RandomSlotAssignment> &outChanges);
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
#include "Common/MultiplayerSettings.h"
#include "GameClient/GameText.h"
#include "GameNetwork/GUIUtil.h"
#include "GameNetwork/RandomAssign.h"


extern char *LANnextScreen;
Expand Down Expand Up @@ -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;
Expand All @@ -119,6 +121,10 @@ static NameKeyType windowMapID = NAMEKEY_INVALID;
static GameWindow *parentLanGameOptions = nullptr;
static GameWindow *buttonBack = nullptr;
static GameWindow *buttonStart = nullptr;
static GameWindow *buttonRandomize = nullptr;
// 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;
Expand Down Expand Up @@ -674,6 +680,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" );
Expand All @@ -692,6 +699,9 @@ 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 ); // 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);
DEBUG_ASSERTCRASH(buttonBack, ("Could not find the buttonBack"));
listboxChatWindowLanGame = TheWindowManager->winGetWindowFromId( parentLanGameOptions, listboxChatWindowLanGameID );
Expand Down Expand Up @@ -795,6 +805,7 @@ void DeinitLanGameGadgets()
buttonSelectMap = nullptr;
buttonStart = nullptr;
buttonBack = nullptr;
buttonRandomize = nullptr;
listboxChatWindowLanGame = nullptr;
textEntryChat = nullptr;
textEntryMapDisplay = nullptr;
Expand Down Expand Up @@ -822,6 +833,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
Expand Down Expand Up @@ -885,6 +897,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 );
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
Expand Down Expand Up @@ -1275,6 +1289,31 @@ WindowMsgHandledType LanGameOptionsMenuSystem( GameWindow *window, UnsignedInt m

}
}
else if ( controlID == buttonRandomizeID )
{
// Host-only Randomize: resolve the random slots,
// broadcast the new options and tell the room it happened.
if (TheLAN->AmIHost())
{
std::vector<RandomSlotAssignment> 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();

++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 )
{
handleLimitSuperweaponsClick();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
#include "GameNetwork/GameSpy/GSConfig.h"

#include "GameNetwork/GeneralsOnline/NGMP_interfaces.h"
#include "GameNetwork/RandomAssign.h"
#include <ws2ipdef.h>
#include <format>
#include <cmath>
Expand Down Expand Up @@ -203,6 +204,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)
Expand All @@ -220,6 +222,19 @@ static GameWindow *parentWOLGameSetup = NULL;
static GameWindow *buttonBack = NULL;
static GameWindow *buttonStart = NULL;
static GameWindow *buttonSelectMap = NULL;
static GameWindow *buttonRandomize = NULL;
// 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;
// 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_localColorChosen = FALSE;
static Bool s_localFactionChosen = FALSE;
static Bool s_applyingLobbyUpdate = FALSE;
static GameWindow *buttonEmote = NULL;
static GameWindow *textEntryChat = NULL;
static GameWindow *textEntryMapDisplay = NULL;
Expand Down Expand Up @@ -360,8 +375,10 @@ static void savePlayerInfo()
{
// save off some prefs
CustomMatchPreferences pref;
pref.setPreferredColor(slot->getColor());
pref.setPreferredFaction(slot->getPlayerTemplate());
if (s_localColorChosen)
pref.setPreferredColor(slot->getColor());
if (s_localFactionChosen)
pref.setPreferredFaction(slot->getPlayerTemplate());
if (TheNGMPGame->amIHost())
{
pref.setPreferredMap(TheNGMPGame->getMap());
Expand Down Expand Up @@ -686,6 +703,8 @@ NameKeyType listboxGameSetupChatID = NAMEKEY_INVALID;

static void handleColorSelection(int index)
{
if (!s_applyingLobbyUpdate && TheNGMPGame && index == TheNGMPGame->getLocalSlotNum())
s_localColorChosen = TRUE; // picked here, so it is worth remembering
GameWindow *combo = comboBoxColor[index];
Int color, selIndex;
GadgetComboBoxGetSelectedPos(combo, &selIndex);
Expand Down Expand Up @@ -747,6 +766,8 @@ static void handleColorSelection(int index)

static void handlePlayerTemplateSelection(int index, bool bInitialSetup = false)
{
if (!bInitialSetup && !s_applyingLobbyUpdate && TheNGMPGame && index == TheNGMPGame->getLocalSlotNum())
s_localFactionChosen = TRUE; // picked here, so it is worth remembering
GameWindow *combo = comboBoxPlayerTemplate[index];
Int playerTemplate, selIndex;
GadgetComboBoxGetSelectedPos(combo, &selIndex);
Expand Down Expand Up @@ -1559,10 +1580,12 @@ void WOLDisplaySlotList(void)

DEBUG_ASSERTCRASH(!game->getConstSlot(0)->isOpen(), ("Open host!"));

s_applyingLobbyUpdate = TRUE;
UpdateSlotList(game, comboBoxPlayer, comboBoxColor,
comboBoxPlayerTemplate, comboBoxTeam, buttonAccept, buttonStart, buttonMapStartPosition);

WOLDisplayGameOptions();
WOLDisplayGameOptions(); // repopulates the army combos too, so still part of the push
s_applyingLobbyUpdate = FALSE;

for (Int i = 0; i < MAX_SLOTS; ++i)
{
Expand Down Expand Up @@ -1620,6 +1643,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");
Expand All @@ -1633,6 +1657,9 @@ void InitWOLGameGadgets()
parentWOLGameSetup = TheWindowManager->winGetWindowFromId( NULL, parentWOLGameSetupID );
buttonEmote = TheWindowManager->winGetWindowFromId( parentWOLGameSetup,buttonEmoteID );
buttonSelectMap = TheWindowManager->winGetWindowFromId( parentWOLGameSetup,buttonSelectMapID );
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 );
buttonStart = TheWindowManager->winGetWindowFromId( parentWOLGameSetup,buttonStartID );
buttonBack = TheWindowManager->winGetWindowFromId( parentWOLGameSetup, buttonBackID);
Expand Down Expand Up @@ -1679,6 +1706,7 @@ void InitWOLGameGadgets()
{
checkBoxLimitSuperweapons->winEnable( false );
comboBoxStartingCash->winEnable( false );
if (buttonRandomize) buttonRandomize->winEnable( false );
NameKeyType labelID = TheNameKeyGenerator->nameToKey("GameSpyGameOptionsMenu.wnd:StartingCashLabel");
TheWindowManager->winGetWindowFromId(parentWOLGameSetup, labelID)->winEnable( FALSE );
}
Expand All @@ -1687,6 +1715,7 @@ void InitWOLGameGadgets()
{
checkBoxLimitSuperweapons->winEnable(true);
comboBoxStartingCash->winEnable(true);
if (buttonRandomize) buttonRandomize->winEnable(true);
}
#endif

Expand All @@ -1699,6 +1728,7 @@ void InitWOLGameGadgets()
checkBoxLimitSuperweapons->winEnable( FALSE );
comboBoxStartingCash->winEnable( FALSE );
checkBoxLimitArmies->winEnable( FALSE );
if (buttonRandomize) buttonRandomize->winEnable( FALSE );
NameKeyType labelID = TheNameKeyGenerator->nameToKey("GameSpyGameOptionsMenu.wnd:StartingCashLabel");
TheWindowManager->winGetWindowFromId(parentWOLGameSetup, labelID)->winEnable( FALSE );
}
Expand Down Expand Up @@ -1840,6 +1870,7 @@ void DeinitWOLGameGadgets()
parentWOLGameSetup = NULL;
buttonEmote = NULL;
buttonSelectMap = NULL;
buttonRandomize = NULL;
buttonStart = NULL;
buttonBack = NULL;
listboxGameSetupChat = NULL;
Expand Down Expand Up @@ -1878,6 +1909,10 @@ Bool initialAcceptEnable = FALSE;
//-------------------------------------------------------------------------------------------------
void WOLGameSetupMenuInit( WindowLayout *layout, void *userData )
{
s_randomizeCount = 0;
s_localColorChosen = FALSE;
s_localFactionChosen = FALSE;
s_applyingLobbyUpdate = FALSE;
NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface<NGMP_OnlineServices_LobbyInterface>();
if (pLobbyInterface == nullptr)
{
Expand Down Expand Up @@ -2240,6 +2275,7 @@ void WOLGameSetupMenuInit( WindowLayout *layout, void *userData )
buttonStart->winSetText(TheGameText->fetch("GUI:Accept"));
buttonStart->winEnable( FALSE );
buttonSelectMap->winEnable( FALSE );
if (buttonRandomize) buttonRandomize->winEnable( FALSE );
initialAcceptEnable = FALSE;

WOLDisplaySlotList();
Expand Down Expand Up @@ -2531,6 +2567,7 @@ void WOLGameSetupMenuUpdate( WindowLayout * layout, void *userData)
buttonStart->winSetText(TheGameText->fetch("GUI:Start"));
buttonStart->winEnable(TRUE);
buttonSelectMap->winEnable(TRUE);
if (buttonRandomize) buttonRandomize->winEnable(TRUE);
initialAcceptEnable = TRUE;

comboBoxStartingCash->winEnable(TRUE);
Expand Down Expand Up @@ -4137,6 +4174,61 @@ WindowMsgHandledType WOLGameSetupMenuSystem( GameWindow *window, UnsignedInt msg
WOLMapSelectLayout->hide( FALSE );
WOLMapSelectLayout->bringForward();
}
else if ( controlID == buttonRandomizeID )
{
// 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<NGMP_OnlineServices_LobbyInterface>();
if (pLobbyInterface != nullptr && pLobbyInterface->IsHost())
{
NGMPGame* game = pLobbyInterface->GetCurrentGame();
if (game)
{
GameSlot saved[MAX_SLOTS];
Int i;
for (i = 0; i < MAX_SLOTS; ++i)
{
if (game->getSlot(i))
saved[i] = *game->getSlot(i);
}
std::vector<RandomSlotAssignment> changes;
performRandomAssign(game, changes);
for (i = 0; i < MAX_SLOTS; ++i)
{
if (game->getSlot(i))
*game->getSlot(i) = saved[i];
}

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
{
// 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, bWasEnabled](bool bSuccess)
{
if (bSuccess)
{
++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);
pLobbyInterface->SendAnnouncementMessageToCurrentLobby(strInform, true);
}
if (buttonRandomize)
buttonRandomize->winEnable(bWasEnabled);
});
}
}
}
}
else if ( controlID == buttonStartID )
{
savePlayerInfo();
Expand Down
Loading
Loading