Skip to content

Commit c9d1fd5

Browse files
authored
[GEN] Implement Headless Mode (#899)
1 parent c25825e commit c9d1fd5

35 files changed

+537
-211
lines changed

Generals/Code/GameEngine/Include/Common/GameAudio.h

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,57 @@ class AudioManager : public SubsystemInterface
371371
Bool m_disallowSpeech : 1;
372372
};
373373

374+
// TheSuperHackers @feature helmutbuhler 17/05/2025
375+
// AudioManager that does nothing. Used for Headless Mode.
376+
class AudioManagerDummy : public AudioManager
377+
{
378+
#if defined(RTS_DEBUG) || defined(RTS_INTERNAL)
379+
virtual void audioDebugDisplay(DebugDisplayInterface* dd, void* userData, FILE* fp) {}
380+
#endif
381+
virtual void stopAudio(AudioAffect which) {}
382+
virtual void pauseAudio(AudioAffect which) {}
383+
virtual void resumeAudio(AudioAffect which) {}
384+
virtual void pauseAmbient(Bool shouldPause) {}
385+
virtual void killAudioEventImmediately(AudioHandle audioEvent) {}
386+
virtual void nextMusicTrack() {}
387+
virtual void prevMusicTrack() {}
388+
virtual Bool isMusicPlaying() const { return false; }
389+
virtual Bool hasMusicTrackCompleted(const AsciiString& trackName, Int numberOfTimes) const { return false; }
390+
virtual AsciiString getMusicTrackName() const { return ""; }
391+
virtual void openDevice() {}
392+
virtual void closeDevice() {}
393+
virtual void* getDevice() { return NULL; }
394+
virtual void notifyOfAudioCompletion(UnsignedInt audioCompleted, UnsignedInt flags) {}
395+
virtual UnsignedInt getProviderCount(void) const { return 0; };
396+
virtual AsciiString getProviderName(UnsignedInt providerNum) const { return ""; }
397+
virtual UnsignedInt getProviderIndex(AsciiString providerName) const { return 0; }
398+
virtual void selectProvider(UnsignedInt providerNdx) {}
399+
virtual void unselectProvider(void) {}
400+
virtual UnsignedInt getSelectedProvider(void) const { return 0; }
401+
virtual void setSpeakerType(UnsignedInt speakerType) {}
402+
virtual UnsignedInt getSpeakerType(void) { return 0; }
403+
virtual UnsignedInt getNum2DSamples(void) const { return 0; }
404+
virtual UnsignedInt getNum3DSamples(void) const { return 0; }
405+
virtual UnsignedInt getNumStreams(void) const { return 0; }
406+
virtual Bool doesViolateLimit(AudioEventRTS* event) const { return false; }
407+
virtual Bool isPlayingLowerPriority(AudioEventRTS* event) const { return false; }
408+
virtual Bool isPlayingAlready(AudioEventRTS* event) const { return false; }
409+
virtual Bool isObjectPlayingVoice(UnsignedInt objID) const { return false; }
410+
virtual void adjustVolumeOfPlayingAudio(AsciiString eventName, Real newVolume) {}
411+
virtual void removePlayingAudio(AsciiString eventName) {}
412+
virtual void removeAllDisabledAudio() {}
413+
virtual Bool has3DSensitiveStreamsPlaying(void) const { return false; }
414+
virtual void* getHandleForBink(void) { return NULL; }
415+
virtual void releaseHandleForBink(void) {}
416+
virtual void friend_forcePlayAudioEventRTS(const AudioEventRTS* eventToPlay) {}
417+
virtual void setPreferredProvider(AsciiString providerNdx) {}
418+
virtual void setPreferredSpeaker(AsciiString speakerType) {}
419+
virtual Real getFileLengthMS(AsciiString strToLoad) const { return -1; }
420+
virtual void closeAnySamplesUsingFile(const void* fileToClose) {}
421+
virtual void setDeviceListenerPosition(void) {}
422+
};
423+
424+
374425
extern AudioManager *TheAudio;
375426

376427
#endif // __COMMON_GAMEAUDIO_H_

Generals/Code/GameEngine/Include/Common/GlobalData.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ class GlobalData : public SubsystemInterface
9696
Bool m_dumpAssetUsage;
9797
Int m_framesPerSecondLimit;
9898
Int m_chipSetType; ///<See W3DShaderManager::ChipsetType for options
99+
100+
// TheSuperHackers @feature helmutbuhler 11/04/2025
101+
// Run game without graphics, input or audio.
102+
Bool m_headless;
103+
99104
Bool m_windowed;
100105
Int m_xResolution;
101106
Int m_yResolution;

Generals/Code/GameEngine/Include/Common/Radar.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,16 @@ class Radar : public Snapshot,
291291
// EXTERNALS //////////////////////////////////////////////////////////////////////////////////////
292292
extern Radar *TheRadar; ///< the radar singleton extern
293293

294+
// TheSuperHackers @feature helmutbuhler 10/04/2025
295+
// Radar that does nothing. Used for Headless Mode.
296+
class RadarDummy : public Radar
297+
{
298+
public:
299+
virtual void draw(Int pixelX, Int pixelY, Int width, Int height) { }
300+
virtual void clearShroud() { }
301+
virtual void setShroudLevel(Int x, Int y, CellShroudStatus setting) { }
302+
};
303+
294304
#endif // __RADAR_H_
295305

296306

Generals/Code/GameEngine/Include/GameClient/GameClient.h

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,4 +227,47 @@ class GameClient : public SubsystemInterface,
227227
// the singleton
228228
extern GameClient *TheGameClient;
229229

230+
231+
// TheSuperHackers @logic-client-separation helmutbuhler 11/04/2025
232+
// Some information about the architecture and headless mode:
233+
// The game is structurally separated into GameLogic and GameClient.
234+
// The Logic is responsible for everything that affects the game mechanic and what is synchronized over
235+
// the network. The Client is responsible for rendering, input, audio and similar stuff.
236+
//
237+
// Unfortunately there are some places in the code that make the Logic depend on the Client.
238+
// (Search for @logic-client-separation)
239+
// That means if we want to run the game headless, we cannot just disable the Client. We need to disable
240+
// the parts in the Client that don't work in headless mode and need to keep the parts that are needed
241+
// to run the Logic.
242+
// The following describes which parts we disable in headless mode:
243+
//
244+
// GameEngine:
245+
// TheGameClient is partially disabled:
246+
// TheKeyboard = NULL
247+
// TheMouse = NULL
248+
// TheDisplay is partially disabled:
249+
// m_3DInterfaceScene = NULL
250+
// m_2DScene = NULL
251+
// m_3DScene = NULL
252+
// (m_assetManager remains!)
253+
// TheWindowManager = GameWindowManagerDummy
254+
// TheIMEManager = NULL
255+
// TheTerrainVisual is partially disabled:
256+
// TheTerrainTracksRenderObjClassSystem = NULL
257+
// TheW3DShadowManager = NULL
258+
// TheWaterRenderObj = NULL
259+
// TheSmudgeManager = NULL
260+
// TheTerrainRenderObject is partially disabled:
261+
// m_treeBuffer = NULL
262+
// m_propBuffer = NULL
263+
// m_bibBuffer = NULL
264+
// m_bridgeBuffer is partially disabled:
265+
// m_vertexBridge = NULL
266+
// m_indexBridge = NULL
267+
// m_vertexMaterial = NULL
268+
// m_waypointBuffer = NULL
269+
// m_roadBuffer = NULL
270+
// m_shroud = NULL
271+
// TheRadar = RadarDummy
272+
230273
#endif // _GAME_INTERFACE_H_

Generals/Code/GameEngine/Include/GameClient/GameWindow.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ friend class GameWindowManager;
324324
// window instance data
325325
Int winSetInstanceData( WinInstanceData *data ); ///< copy over instance data
326326
WinInstanceData *winGetInstanceData( void ); ///< get instance data
327-
void *winGetUserData( void ); ///< get the window user data
327+
virtual void *winGetUserData( void ); ///< get the window user data
328328
void winSetUserData( void *userData ); ///< set the user data
329329

330330
// heirarchy methods
@@ -433,6 +433,16 @@ friend class GameWindowManager;
433433

434434
}; // end class GameWindow
435435

436+
// TheSuperHackers @feature helmutbuhler 24/04/2025
437+
// GameWindow that does nothing. Used for Headless Mode.
438+
class GameWindowDummy : public GameWindow
439+
{
440+
MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE(GameWindowDummy, "GameWindowDummy")
441+
public:
442+
virtual void winDrawBorder() {}
443+
virtual void* winGetUserData(void) { return NULL; }
444+
};
445+
436446
// ModalWindow ----------------------------------------------------------------
437447
//-----------------------------------------------------------------------------
438448
class ModalWindow : public MemoryPoolObject

Generals/Code/GameEngine/Include/GameClient/GameWindowManager.h

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,39 @@ extern WindowMsgHandledType PassMessagesToParentSystem( GameWindow *window,
379379
WindowMsgData mData1,
380380
WindowMsgData mData2 );
381381

382-
382+
// TheSuperHackers @feature helmutbuhler 24/04/2025
383+
// GameWindowManager that does nothing. Used for Headless Mode.
384+
class GameWindowManagerDummy : public GameWindowManager
385+
{
386+
public:
387+
virtual GameWindow *winGetWindowFromId(GameWindow *window, Int id);
388+
virtual GameWindow *winCreateFromScript(AsciiString filenameString, WindowLayoutInfo *info);
389+
390+
virtual GameWindow *allocateNewWindow() { return newInstance(GameWindowDummy); }
391+
392+
virtual GameWinDrawFunc getPushButtonImageDrawFunc() { return NULL; }
393+
virtual GameWinDrawFunc getPushButtonDrawFunc() { return NULL; }
394+
virtual GameWinDrawFunc getCheckBoxImageDrawFunc() { return NULL; }
395+
virtual GameWinDrawFunc getCheckBoxDrawFunc() { return NULL; }
396+
virtual GameWinDrawFunc getRadioButtonImageDrawFunc() { return NULL; }
397+
virtual GameWinDrawFunc getRadioButtonDrawFunc() { return NULL; }
398+
virtual GameWinDrawFunc getTabControlImageDrawFunc() { return NULL; }
399+
virtual GameWinDrawFunc getTabControlDrawFunc() { return NULL; }
400+
virtual GameWinDrawFunc getListBoxImageDrawFunc() { return NULL; }
401+
virtual GameWinDrawFunc getListBoxDrawFunc() { return NULL; }
402+
virtual GameWinDrawFunc getComboBoxImageDrawFunc() { return NULL; }
403+
virtual GameWinDrawFunc getComboBoxDrawFunc() { return NULL; }
404+
virtual GameWinDrawFunc getHorizontalSliderImageDrawFunc() { return NULL; }
405+
virtual GameWinDrawFunc getHorizontalSliderDrawFunc() { return NULL; }
406+
virtual GameWinDrawFunc getVerticalSliderImageDrawFunc() { return NULL; }
407+
virtual GameWinDrawFunc getVerticalSliderDrawFunc() { return NULL; }
408+
virtual GameWinDrawFunc getProgressBarImageDrawFunc() { return NULL; }
409+
virtual GameWinDrawFunc getProgressBarDrawFunc() { return NULL; }
410+
virtual GameWinDrawFunc getStaticTextImageDrawFunc() { return NULL; }
411+
virtual GameWinDrawFunc getStaticTextDrawFunc() { return NULL; }
412+
virtual GameWinDrawFunc getTextEntryImageDrawFunc() { return NULL; }
413+
virtual GameWinDrawFunc getTextEntryDrawFunc() { return NULL; }
414+
};
383415

384416
#endif // __GAMEWINDOWMANAGER_H_
385417

Generals/Code/GameEngine/Include/GameClient/Mouse.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,20 @@ class Mouse : public SubsystemInterface
370370

371371
}; // end class Mouse
372372

373-
// INLINING ///////////////////////////////////////////////////////////////////
373+
// TheSuperHackers @feature helmutbuhler 17/05/2025
374+
// Mouse that does nothing. Used for Headless Mode.
375+
class MouseDummy : public Mouse
376+
{
377+
virtual void parseIni() {}
378+
virtual void update() {}
379+
virtual void initCursorResources() {}
380+
virtual void createStreamMessages() {}
381+
virtual void setCursor(MouseCursor cursor) {}
382+
virtual void capture() {}
383+
virtual void releaseCapture() {}
384+
virtual UnsignedByte getMouseEvent(MouseIO *result, Bool flush) { return MOUSE_NONE; }
385+
};
386+
374387

375388
// EXTERNALS //////////////////////////////////////////////////////////////////
376389
extern Mouse *TheMouse; ///< extern mouse singleton definition

Generals/Code/GameEngine/Source/Common/CommandLine.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,15 @@ Int parseQuickStart( char *args[], int num )
862862
return 1;
863863
}
864864

865+
Int parseHeadless( char *args[], int num )
866+
{
867+
if (TheWritableGlobalData)
868+
{
869+
TheWritableGlobalData->m_headless = TRUE;
870+
}
871+
return 1;
872+
}
873+
865874
Int parseConstantDebug( char *args[], int num )
866875
{
867876
if (TheWritableGlobalData)
@@ -1206,6 +1215,10 @@ static CommandLineParam params[] =
12061215
{ "-quickstart", parseQuickStart },
12071216
{ "-useWaveEditor", parseUseWaveEditor },
12081217

1218+
// TheSuperHackers @feature helmutbuhler 11/04/2025
1219+
// This runs the game without a window, graphics, input and audio. Used for testing.
1220+
{ "-headless", parseHeadless },
1221+
12091222
#if (defined(RTS_DEBUG) || defined(RTS_INTERNAL))
12101223
{ "-noaudio", parseNoAudio },
12111224
{ "-map", parseMapName },

Generals/Code/GameEngine/Source/Common/GameEngine.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ void GameEngine::init( int argc, char *argv[] )
347347
initSubsystem(TheTerrainRoads,"TheTerrainRoads", MSGNEW("GameEngineSubsystem") TerrainRoadCollection(), &xferCRC, "Data\\INI\\Default\\Roads.ini", "Data\\INI\\Roads.ini");
348348
initSubsystem(TheGlobalLanguageData,"TheGlobalLanguageData",MSGNEW("GameEngineSubsystem") GlobalLanguage, NULL); // must be before the game text
349349
initSubsystem(TheCDManager,"TheCDManager", CreateCDManager(), NULL);
350-
initSubsystem(TheAudio,"TheAudio", createAudioManager(), NULL);
350+
initSubsystem(TheAudio,"TheAudio", TheGlobalData->m_headless ? NEW AudioManagerDummy : createAudioManager(), NULL);
351351
if (!TheAudio->isMusicAlreadyLoaded())
352352
setQuitting(TRUE);
353353
initSubsystem(TheFunctionLexicon,"TheFunctionLexicon", createFunctionLexicon(), NULL);
@@ -375,7 +375,7 @@ void GameEngine::init( int argc, char *argv[] )
375375
initSubsystem(TheCrateSystem,"TheCrateSystem", MSGNEW("GameEngineSubsystem") CrateSystem(), &xferCRC, "Data\\INI\\Default\\Crate.ini", "Data\\INI\\Crate.ini");
376376
initSubsystem(ThePlayerList,"ThePlayerList", MSGNEW("GameEngineSubsystem") PlayerList(), NULL);
377377
initSubsystem(TheRecorder,"TheRecorder", createRecorder(), NULL);
378-
initSubsystem(TheRadar,"TheRadar", createRadar(), NULL);
378+
initSubsystem(TheRadar,"TheRadar", TheGlobalData->m_headless ? NEW RadarDummy : createRadar(), NULL);
379379
initSubsystem(TheVictoryConditions,"TheVictoryConditions", createVictoryConditions(), NULL);
380380

381381
AsciiString fname;

Generals/Code/GameEngine/Source/Common/GlobalData.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ GlobalData::GlobalData()
608608
m_dumpAssetUsage = FALSE;
609609
m_framesPerSecondLimit = 0;
610610
m_chipSetType = 0;
611+
m_headless = FALSE;
611612
m_windowed = 0;
612613
m_xResolution = 800;
613614
m_yResolution = 600;

Generals/Code/GameEngine/Source/Common/RandomValue.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ Int GetGameClientRandomValue( int lo, int hi, const char *file, int line )
249249
/**/
250250
#ifdef DEBUG_RANDOM_CLIENT
251251
DEBUG_LOG(( "%d: GetGameClientRandomValue = %d (%d - %d), %s line %d\n",
252-
TheGameLogic->getFrame(), rval, lo, hi, file, line ));
252+
TheGameLogic ? TheGameLogic->getFrame() : -1, rval, lo, hi, file, line ));
253253
#endif
254254
/**/
255255

Generals/Code/GameEngine/Source/Common/System/MemoryInit.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ static PoolSizeRec sizes[] =
378378
{ "Overridable", 32, 32 },
379379

380380
{ "W3DGameWindow", 1024, 32 },
381+
{ "GameWindowDummy", 1024, 32 },
381382
{ "SuccessState", 32, 32 },
382383
{ "FailureState", 32, 32 },
383384
{ "ContinueState", 32, 32 },

Generals/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,6 +1359,8 @@ void ControlBar::reset( void )
13591359
//-------------------------------------------------------------------------------------------------
13601360
void ControlBar::update( void )
13611361
{
1362+
if (TheGlobalData->m_headless)
1363+
return;
13621364
getStarImage();
13631365
updateRadarAttackGlow();
13641366
if(m_controlBarSchemeManager)

Generals/Code/GameEngine/Source/GameClient/GUI/Gadget/GadgetListBox.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,6 +2221,8 @@ Int GadgetListBoxAddEntryText( GameWindow *listbox,
22212221
addInfo.width = -1;
22222222

22232223
ListboxData *listData = (ListboxData *)listbox->winGetUserData();
2224+
if (listData == NULL)
2225+
return -1;
22242226
Bool wasFull = (listData->listLength <= listData->endPos);
22252227
Int newEntryOffset = (wasFull)?0:1;
22262228
Int oldBottomIndex = GadgetListBoxGetBottomVisibleEntry(listbox);

Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4097,3 +4097,33 @@ void GameWindowManager::clearTabList( void )
40974097
{
40984098
m_tabList.clear();
40994099
}
4100+
4101+
4102+
GameWindow *GameWindowManagerDummy::winGetWindowFromId(GameWindow *window, Int id)
4103+
{
4104+
window = GameWindowManager::winGetWindowFromId(window, id);
4105+
if (window != NULL)
4106+
return window;
4107+
4108+
// Just return any window, callers expect this to be non-null
4109+
return m_windowList;
4110+
}
4111+
4112+
WindowMsgHandledType DummyWindowSystem(GameWindow *window, UnsignedInt msg, WindowMsgData mData1, WindowMsgData mData2)
4113+
{
4114+
return MSG_IGNORED;
4115+
}
4116+
4117+
GameWindow *GameWindowManagerDummy::winCreateFromScript(AsciiString filenameString, WindowLayoutInfo *info)
4118+
{
4119+
WindowLayoutInfo scriptInfo;
4120+
GameWindow* dummyWindow = winCreate(NULL, 0, 0, 0, 100, 100, DummyWindowSystem, NULL);
4121+
scriptInfo.windows.push_back(dummyWindow);
4122+
if (info)
4123+
*info = scriptInfo;
4124+
return dummyWindow;
4125+
}
4126+
4127+
GameWindowDummy::~GameWindowDummy()
4128+
{
4129+
}

0 commit comments

Comments
 (0)