Skip to content

Commit c25825e

Browse files
authored
[ZH] Implement Headless Mode (#651)
1 parent 95980dc commit c25825e

36 files changed

+541
-217
lines changed

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

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

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

377428
#endif // __COMMON_GAMEAUDIO_H_

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ class GlobalData : public SubsystemInterface
100100
Bool m_dumpAssetUsage;
101101
Int m_framesPerSecondLimit;
102102
Int m_chipSetType; ///<See W3DShaderManager::ChipsetType for options
103+
104+
// TheSuperHackers @feature helmutbuhler 11/04/2025
105+
// Run game without graphics, input or audio.
106+
Bool m_headless;
107+
103108
Bool m_windowed;
104109
Int m_xResolution;
105110
Int m_yResolution;

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

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

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

298308

GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,47 @@ inline Drawable* GameClient::findDrawableByID( const DrawableID id )
259259
// the singleton
260260
extern GameClient *TheGameClient;
261261

262+
263+
// TheSuperHackers @logic-client-separation helmutbuhler 11/04/2025
264+
// Some information about the architecture and headless mode:
265+
// The game is structurally separated into GameLogic and GameClient.
266+
// The Logic is responsible for everything that affects the game mechanic and what is synchronized over
267+
// the network. The Client is responsible for rendering, input, audio and similar stuff.
268+
//
269+
// Unfortunately there are some places in the code that make the Logic depend on the Client.
270+
// (Search for @logic-client-separation)
271+
// That means if we want to run the game headless, we cannot just disable the Client. We need to disable
272+
// the parts in the Client that don't work in headless mode and need to keep the parts that are needed
273+
// to run the Logic.
274+
// The following describes which parts we disable in headless mode:
275+
//
276+
// GameEngine:
277+
// TheGameClient is partially disabled:
278+
// TheKeyboard = NULL
279+
// TheMouse = NULL
280+
// TheDisplay is partially disabled:
281+
// m_3DInterfaceScene = NULL
282+
// m_2DScene = NULL
283+
// m_3DScene = NULL
284+
// (m_assetManager remains!)
285+
// TheWindowManager = GameWindowManagerDummy
286+
// TheIMEManager = NULL
287+
// TheTerrainVisual is partially disabled:
288+
// TheTerrainTracksRenderObjClassSystem = NULL
289+
// TheW3DShadowManager = NULL
290+
// TheWaterRenderObj = NULL
291+
// TheSmudgeManager = NULL
292+
// TheTerrainRenderObject is partially disabled:
293+
// m_treeBuffer = NULL
294+
// m_propBuffer = NULL
295+
// m_bibBuffer = NULL
296+
// m_bridgeBuffer is partially disabled:
297+
// m_vertexBridge = NULL
298+
// m_indexBridge = NULL
299+
// m_vertexMaterial = NULL
300+
// m_waypointBuffer = NULL
301+
// m_roadBuffer = NULL
302+
// m_shroud = NULL
303+
// TheRadar = RadarDummy
304+
262305
#endif // _GAME_INTERFACE_H_

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

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

332332
// heirarchy methods
@@ -435,6 +435,16 @@ friend class GameWindowManager;
435435

436436
}; // end class GameWindow
437437

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

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

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

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

372372
}; // end class Mouse
373373

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

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

GeneralsMD/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 },

GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ void GameEngine::init( int argc, char *argv[] )
437437
startTime64 = endTime64;//Reset the clock ////////////////////////////////////////////////////////
438438
DEBUG_LOG(("%s", Buf));////////////////////////////////////////////////////////////////////////////
439439
#endif/////////////////////////////////////////////////////////////////////////////////////////////
440-
initSubsystem(TheAudio,"TheAudio", createAudioManager(), NULL);
440+
initSubsystem(TheAudio,"TheAudio", TheGlobalData->m_headless ? NEW AudioManagerDummy : createAudioManager(), NULL);
441441
if (!TheAudio->isMusicAlreadyLoaded())
442442
setQuitting(TRUE);
443443

@@ -513,7 +513,7 @@ void GameEngine::init( int argc, char *argv[] )
513513
initSubsystem(TheCrateSystem,"TheCrateSystem", MSGNEW("GameEngineSubsystem") CrateSystem(), &xferCRC, "Data\\INI\\Default\\Crate.ini", "Data\\INI\\Crate.ini");
514514
initSubsystem(ThePlayerList,"ThePlayerList", MSGNEW("GameEngineSubsystem") PlayerList(), NULL);
515515
initSubsystem(TheRecorder,"TheRecorder", createRecorder(), NULL);
516-
initSubsystem(TheRadar,"TheRadar", createRadar(), NULL);
516+
initSubsystem(TheRadar,"TheRadar", TheGlobalData->m_headless ? NEW RadarDummy : createRadar(), NULL);
517517
initSubsystem(TheVictoryConditions,"TheVictoryConditions", createVictoryConditions(), NULL);
518518

519519

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,7 @@ GlobalData::GlobalData()
613613
m_dumpAssetUsage = FALSE;
614614
m_framesPerSecondLimit = 0;
615615
m_chipSetType = 0;
616+
m_headless = FALSE;
616617
m_windowed = 0;
617618
m_xResolution = 800;
618619
m_yResolution = 600;

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

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

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

418418
{ "W3DGameWindow", 700, 256 },
419+
{ "GameWindowDummy", 700, 256 },
419420
{ "SuccessState", 32, 32 },
420421
{ "FailureState", 32, 32 },
421422
{ "ContinueState", 32, 32 },

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,8 @@ void ControlBar::reset( void )
13821382
//-------------------------------------------------------------------------------------------------
13831383
void ControlBar::update( void )
13841384
{
1385+
if (TheGlobalData->m_headless)
1386+
return;
13851387
getStarImage();
13861388
updateRadarAttackGlow();
13871389
if(m_controlBarSchemeManager)

GeneralsMD/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);

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4097,3 +4097,36 @@ 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+
}
4130+
4131+
4132+

0 commit comments

Comments
 (0)