-
Notifications
You must be signed in to change notification settings - Fork 167
Description
Prerequisites
- I have searched for similar issues and confirmed this is not a duplicate
Game Version
- Command & Conquer Generals
- Command & Conquer Generals: Zero Hour
- Other (please specify below)
Bug Description
Summary
The game's random number generator (RNG) seed for multiplayer matches is fully deterministic. Once the seed is known, all "random" outcomes — most notably faction assignment when players select "Random" — can be predicted. Depending on the multiplayer backend, the seed is either directly controlled by the host (LAN) or visible to all clients before the game starts (GeneralsOnline). This undermines fair play in competitive and casual multiplayer.
How the seed flows
LAN
- Host sets the seed — When a LAN game lobby is created, the host calls
setSeed(GetTickCount())inLANAPI::RequestGameCreate. - Seed is broadcast in plaintext — The seed is serialized into the game options string as
SD=<value>viaGameInfoToAsciiString()and sent over unencrypted UDP to all clients. The host periodically re-broadcasts this viaRequestGameOptions/RequestGameAnnounce. - All clients apply the seed — At game start, every client calls
InitGameLogicRandom(m_currentGame->getSeed()).
On LAN the host has full unilateral control over the seed.
GeneralsOnline
- Server generates the seed — The host client's
CreateLobbyrequest does not include a seed. The server assignsRNGSeedand returns it in the lobby JSON response (OnlineServices_LobbyInterface.cpp). - Seed is distributed to every client before game start — All lobby members receive
lobby.rng_seedas part of theLobbyEntrystruct when they sync with the lobby.NGMPGame::SyncWithLobbycallssetSeed(lobby.rng_seed).
On GeneralsOnline the host cannot choose the seed, but every client can observe it before the match begins and predict the outcome.
Faction resolution (both backends)
populateRandomSideAndColor() in GameLogic.cpp resolves every PLAYERTEMPLATE_RANDOM slot using GameLogicRandomValue(), which is seeded by the host-/server-provided seed. The existing MORE_RANDOM mitigation attempts to discard early values, but the discard count itself is derived from the seed (GetGameLogicRandomSeed() % 7), so it is equally predictable.
How the seed can be obtained or abused
| Vector | Applies to | Description |
|---|---|---|
| Host manipulation | LAN | The host can restart lobbies until GetTickCount() yields a favourable seed, or a modified client can set an arbitrary seed directly. |
| Memory scraping | LAN, GeneralsOnline | Any player can read the seed from the game process memory (theGameLogicBaseSeed / m_seed in GameInfo) before the match begins and compute the faction table offline. |
| LAN traffic sniffing | LAN | Game options are broadcast as unencrypted UDP containing SD=<seed>. Any device on the network can capture and decode it. |
| Lobby API observation | GeneralsOnline | The RNGSeed field is part of the lobby response JSON visible to every client. Even though the server generates it, any client (or third-party tool querying the API) can read the seed and predict assignments before the game launches. |
Impact on fair play
- On LAN, a host who wants a specific faction matchup can trivially achieve it.
- On any backend, any player with basic tooling can predict what "Random" will resolve to for every slot, removing the strategic uncertainty that "Random" is supposed to provide.
- In ranked/competitive play this directly compromises match integrity.
Proposed solution
Short term (server-side, GeneralsOnline):
- Do not distribute the RNG seed to clients until the moment of game launch (i.e. after all players have locked in and the countdown completes). This closes the window for pre-game prediction.
- Ensure the seed is generated with a cryptographically secure RNG on the server.
Long term (all backends including LAN):
Implement a commit-reveal multi-party entropy protocol:
- Each player generates a local secret random value and broadcasts a hash (commitment) during the pre-game phase.
- Once all commitments are collected, each player reveals their secret.
- The final game seed is computed as a deterministic combination (e.g. XOR or cryptographic hash) of all revealed values.
- Any player who fails to reveal, or whose reveal doesn't match the commitment, triggers a lobby abort or re-roll.
This ensures no single client — including the host — can determine or predict the seed, and passive observation of network traffic or memory is insufficient to derive the final seed before all parties have committed.
Relevant code
- Seed origin (LAN):
LANAPI::RequestGameCreate—myGame->setSeed(GetTickCount()) - Seed from server (GeneralsOnline):
OnlineServices_LobbyInterface.cpp—lobbyEntryIter["RNGSeed"].get_to(lobbyEntry.rng_seed) - Seed consumed (GeneralsOnline):
NGMPGame::SyncWithLobby—setSeed(lobby.rng_seed) - Seed serialized to LAN:
GameInfoToAsciiString—SD=%din options string - Seed applied at game start (LAN):
InitGameLogicRandom(getSeed()) - Faction resolution:
populateRandomSideAndColor— usesGameLogicRandomValueseeded by the provided seed - RNG implementation:
RandomValue.cpp— deterministic PRNG; GeneralsOnline has experimentalGENERALS_ONLINE_USE_NEW_RNG_LOGIC(currently disabled) that swaps tostd::mt19937but does not change the seeding trust model
Reproduction Steps
- Host a multiplayer game (LAN or GeneralsOnline).
- Have one or more players select "Random" faction.
- Start the game and note the assigned factions.
- Re-host with the same seed (on LAN: set
m_seedin memory or replay the sameGetTickCount()value; on GeneralsOnline: readRNGSeedfrom the lobby API response) and the same lobby configuration. - Observe that the "random" faction assignments are identical — they are fully determined by the seed.
- On LAN, alternatively capture the UDP broadcast to obtain the
SD=value and compute the expected assignments offline before the game begins.
Additional Context
No response