Skip to content

[ZH] Implement Headless Mode #651

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 51 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
e4b6f18
Make CRC-related commandline arguments work when compiling Release wi…
helmutbuhler Mar 22, 2025
fb4881f
Add comments to Xfer.h
helmutbuhler Mar 22, 2025
10991d6
Add some comments to crc command line arguments
helmutbuhler Mar 22, 2025
320cd7c
Resest Frame Counter earlier in GameLogic::prepareNewGame instead of …
helmutbuhler Mar 23, 2025
2e0a25c
Improved CRC Debugging: Optionally save crc data into a file per fram…
helmutbuhler Mar 29, 2025
adfd721
Improved logging for mismatch debugging.
helmutbuhler Mar 29, 2025
ba13902
Make condition for checking TheCRCFirstFrameToLog consistent.
helmutbuhler Mar 29, 2025
1b2f5a8
Add comment regarding g_keepCRCSaves
helmutbuhler Mar 29, 2025
fb5d977
Remove transform crclogging in Object::crc. This was likely copypaste…
helmutbuhler Mar 29, 2025
46a4ab4
Delete CRC Frame files properly
helmutbuhler Mar 29, 2025
9f7287e
Improve comments
helmutbuhler Apr 10, 2025
4a22c73
Reset framecounter and call CRCDebugStartNewGame in GameLogic::startN…
helmutbuhler Apr 10, 2025
e536826
Fix TEAM_ID_INVALID
helmutbuhler Apr 10, 2025
21b4851
Some code cleanup in CRCDebug
helmutbuhler Apr 10, 2025
88a5b8e
Merge branch 'hb_sh' into improve_crc_logging_ea
helmutbuhler Apr 10, 2025
2be0870
Add define NORMAL_LOG_IN_CRC_LOG to control normal logging in crc logs.
helmutbuhler Apr 10, 2025
3813f09
Add cmake options to control logging
helmutbuhler Apr 10, 2025
83e7ec1
Add m_headless global flag to optionally run the game in headless mode.
helmutbuhler Apr 10, 2025
16aa073
Add some comments where logic-client-separation is broken.
helmutbuhler Apr 10, 2025
4229f1d
Add some more checks for headless
helmutbuhler Apr 10, 2025
c7c8b1d
Add comment about logic-client architecture and headless mode
helmutbuhler Apr 10, 2025
f69e41f
Add -headless commandline option
helmutbuhler Apr 11, 2025
7498de0
Fix headless when shutting down the engine.
helmutbuhler Apr 13, 2025
bf0a2e5
Rename RadarHeadless to RadarDummy
helmutbuhler Apr 13, 2025
7df3c68
Simplify RadarDummy
helmutbuhler Apr 13, 2025
8d325d9
Change TheWindowManager Check in ControlBar::getStarImage
helmutbuhler Apr 13, 2025
741a9a3
Merge branch 'main' into improve_crc_logging_ea
helmutbuhler Apr 13, 2025
bb744e7
Readd logging configuration to cmake files
helmutbuhler Apr 13, 2025
bf19c8c
Merge branch 'improve_crc_logging_ea' into headless_sh
helmutbuhler Apr 13, 2025
dd16528
Merge branch 'main' of https://github.com/TheSuperHackers/GeneralsGam…
helmutbuhler Apr 14, 2025
adfbe8b
Remove config-logging.cmake
helmutbuhler Apr 14, 2025
6fdf9ed
Fix wrong cmake feature names
helmutbuhler Apr 14, 2025
c26a4fa
Add RTS_DEBUG_INCLUDE_DEBUG_LOG_IN_CRC_LOG option (again)
helmutbuhler Apr 14, 2025
4917362
Fix comments
helmutbuhler Apr 15, 2025
40a9407
Merge branch 'improve_crc_logging_ea' into headless_sh
helmutbuhler Apr 15, 2025
5fba809
Merge branch 'main' of https://github.com/TheSuperHackers/GeneralsGam…
helmutbuhler Apr 15, 2025
73adde5
Merge branch 'main' of https://github.com/TheSuperHackers/GeneralsGam…
helmutbuhler Apr 15, 2025
e50eec4
Add null checks for W3DDisplay::m_3DScene
helmutbuhler Apr 17, 2025
232da78
Merge branch 'main' into headless_sh
helmutbuhler Apr 19, 2025
016187a
Merge branch 'main' into headless_sh
helmutbuhler Apr 24, 2025
d6c8339
Move m_headless check for W3DView to GameClient
helmutbuhler Apr 21, 2025
c26971e
Some minor code moving around
helmutbuhler Apr 21, 2025
8eb9730
Add missing headless check for Sizzle Video
helmutbuhler Apr 22, 2025
5951bc8
Add missing headless checks for m_bibBuffer in BaseHeightMapRenderObj…
helmutbuhler Apr 22, 2025
39ae00c
Add DummyGameWindowManager to avoid null checks
helmutbuhler Apr 23, 2025
8759b91
Remove TheWindowManager NULL checks
helmutbuhler Apr 23, 2025
cf8963d
Remove more TheWindowManager NULL checks
helmutbuhler Apr 24, 2025
d371c76
Fix stupid dates in comments
helmutbuhler Apr 24, 2025
9513645
Remove one more TheWindowManager NULL check
helmutbuhler Apr 24, 2025
1cf0c69
Fix some white space stuff
helmutbuhler Apr 24, 2025
d8bcd57
Add comments for DummyGameWindowManager
helmutbuhler Apr 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ class GlobalData : public SubsystemInterface
Bool m_dumpAssetUsage;
Int m_framesPerSecondLimit;
Int m_chipSetType; ///<See W3DShaderManager::ChipsetType for options

// TheSuperHackers @feature helmutbuhler 11/04/2025
// Run game without graphics, input or audio.
Bool m_headless;

Bool m_windowed;
Int m_xResolution;
Int m_yResolution;
Expand Down
10 changes: 10 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/Common/Radar.h
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,16 @@ class Radar : public Snapshot,
// EXTERNALS //////////////////////////////////////////////////////////////////////////////////////
extern Radar *TheRadar; ///< the radar singleton extern

// TheSuperHackers @feature helmutbuhler 10/04/2025
// Radar that does nothing. Used for Headless Mode.
class RadarDummy : public Radar
{
public:
virtual void draw(Int pixelX, Int pixelY, Int width, Int height) { }
virtual void clearShroud() { }
virtual void setShroudLevel(Int x, Int y, CellShroudStatus setting) { }
};

#endif // __RADAR_H_


Expand Down
40 changes: 40 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,44 @@ inline Drawable* GameClient::findDrawableByID( const DrawableID id )
// the singleton
extern GameClient *TheGameClient;


// TheSuperHackers @logic-client-separation helmutbuhler 11/04/2025
// Some information about the architecture and headless mode:
// The game is structurally separated into GameLogic and GameClient.
// The Logic is responsible for everything that affects the game mechanic and what is synchronized over
// the network. The Client is responsible for rendering, input, audio and similar stuff.
//
// Unfortunately there are some places in the code that make the Logic depend on the Client.
// (Search for @logic-client-separation)
// That means if we want to run the game headless, we cannot just disable the Client. We need to disable
// the parts in the Client that don't work in headless mode and need to keep the parts that are needed
// to run the Logic.
// The following describes which parts we disable in headless mode:
//
// GameEngine:
// TheGameClient is partially disabled:
// TheKeyboard = NULL
// TheMouse = NULL
// TheDisplay is partially disabled:
// m_3DInterfaceScene = NULL
// m_2DScene = NULL
// m_3DScene = NULL
// (m_assetManager remains!)
// TheWindowManager = DummyGameWindowManager
// TheIMEManager = NULL
// TheTerrainVisual is partially disabled:
// TheTerrainTracksRenderObjClassSystem = NULL
// TheW3DShadowManager = NULL
// TheWaterRenderObj = NULL
// TheSmudgeManager = NULL
// TheTerrainRenderObject is partially disabled:
// m_treeBuffer = NULL
// m_propBuffer = NULL
// m_bibBuffer = NULL
// m_bridgeBuffer = NULL
// m_waypointBuffer = NULL
// m_roadBuffer = NULL
// m_shroud = NULL
// TheRadar = RadarDummy
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RadarDummy, DummyGameWindow, DummyGameWindowManager are not consistently named.

I suggest to put "Dummy" at the end so that it groups properly with the normal class.

Radar
RadarDummy
GameWindow
GameWindowDummy
GameWindowManager
GameWindowManagerDummy

Alternatively can also use "Null" instead of "Dummy". Either name is fine.


#endif // _GAME_INTERFACE_H_
43 changes: 43 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/GameClient/GameWindowManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,49 @@ extern WindowMsgHandledType PassMessagesToParentSystem( GameWindow *window,
WindowMsgData mData2 );


// TheSuperHackers @feature helmutbuhler 24/04/2025
// GameWindow that does nothing. Used for Headless Mode.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to move these dummies into new header + source files because they are a bit on the larger side.

class DummyGameWindow : public GameWindow
{
MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE(DummyGameWindow, "DummyGameWindow")
public:
//virtual ~DummyGameWindow();
virtual void winDrawBorder() { }
};

// TheSuperHackers @feature helmutbuhler 24/04/2025
// GameWindowManager that does nothing. Used for Headless Mode.
class DummyGameWindowManager : public GameWindowManager
{
public:
virtual GameWindow *winGetWindowFromId(GameWindow *window, Int id);
virtual GameWindow *winCreateFromScript(AsciiString filenameString, WindowLayoutInfo *info);

virtual GameWindow *allocateNewWindow() { return newInstance(DummyGameWindow); }

virtual GameWinDrawFunc getPushButtonImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getPushButtonDrawFunc() { return NULL; }
virtual GameWinDrawFunc getCheckBoxImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getCheckBoxDrawFunc() { return NULL; }
virtual GameWinDrawFunc getRadioButtonImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getRadioButtonDrawFunc() { return NULL; }
virtual GameWinDrawFunc getTabControlImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getTabControlDrawFunc() { return NULL; }
virtual GameWinDrawFunc getListBoxImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getListBoxDrawFunc() { return NULL; }
virtual GameWinDrawFunc getComboBoxImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getComboBoxDrawFunc() { return NULL; }
virtual GameWinDrawFunc getHorizontalSliderImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getHorizontalSliderDrawFunc() { return NULL; }
virtual GameWinDrawFunc getVerticalSliderImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getVerticalSliderDrawFunc() { return NULL; }
virtual GameWinDrawFunc getProgressBarImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getProgressBarDrawFunc() { return NULL; }
virtual GameWinDrawFunc getStaticTextImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getStaticTextDrawFunc() { return NULL; }
virtual GameWinDrawFunc getTextEntryImageDrawFunc() { return NULL; }
virtual GameWinDrawFunc getTextEntryDrawFunc() { return NULL; }
};

#endif // __GAMEWINDOWMANAGER_H_

13 changes: 13 additions & 0 deletions GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,15 @@ Int parseQuickStart( char *args[], int num )
return 1;
}

Int parseHeadless( char *args[], int num )
{
if (TheWritableGlobalData)
{
TheWritableGlobalData->m_headless = TRUE;
}
return 1;
}

Int parseConstantDebug( char *args[], int num )
{
if (TheWritableGlobalData)
Expand Down Expand Up @@ -1205,6 +1214,10 @@ static CommandLineParam params[] =
{ "-noshaders", parseNoShaders },
{ "-quickstart", parseQuickStart },

// TheSuperHackers @feature helmutbuhler 11/04/2025
// This runs the game without a window, graphics, input and audio. Used for testing.
{ "-headless", parseHeadless },

#if (defined(_DEBUG) || defined(_INTERNAL))
{ "-noaudio", parseNoAudio },
{ "-map", parseMapName },
Expand Down
2 changes: 1 addition & 1 deletion GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ void GameEngine::init( int argc, char *argv[] )
initSubsystem(TheCrateSystem,"TheCrateSystem", MSGNEW("GameEngineSubsystem") CrateSystem(), &xferCRC, "Data\\INI\\Default\\Crate.ini", "Data\\INI\\Crate.ini");
initSubsystem(ThePlayerList,"ThePlayerList", MSGNEW("GameEngineSubsystem") PlayerList(), NULL);
initSubsystem(TheRecorder,"TheRecorder", createRecorder(), NULL);
initSubsystem(TheRadar,"TheRadar", createRadar(), NULL);
initSubsystem(TheRadar,"TheRadar", TheGlobalData->m_headless ? NEW RadarDummy : createRadar(), NULL);
initSubsystem(TheVictoryConditions,"TheVictoryConditions", createVictoryConditions(), NULL);


Expand Down
1 change: 1 addition & 0 deletions GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ GlobalData::GlobalData()
m_dumpAssetUsage = FALSE;
m_framesPerSecondLimit = 0;
m_chipSetType = 0;
m_headless = FALSE;
m_windowed = 0;
m_xResolution = 800;
m_yResolution = 600;
Expand Down
2 changes: 1 addition & 1 deletion GeneralsMD/Code/GameEngine/Source/Common/RandomValue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ Int GetGameClientRandomValue( int lo, int hi, const char *file, int line )
/**/
#ifdef DEBUG_RANDOM_CLIENT
DEBUG_LOG(( "%d: GetGameClientRandomValue = %d (%d - %d), %s line %d\n",
TheGameLogic->getFrame(), rval, lo, hi, file, line ));
TheGameLogic ? TheGameLogic->getFrame() : -1, rval, lo, hi, file, line ));
#endif
/**/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ static PoolSizeRec sizes[] =
{ "Overridable", 32, 32 },

{ "W3DGameWindow", 700, 256 },
{ "DummyGameWindow", 700, 256 },
{ "SuccessState", 32, 32 },
{ "FailureState", 32, 32 },
{ "ContinueState", 32, 32 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,8 @@ void ControlBar::reset( void )
//-------------------------------------------------------------------------------------------------
void ControlBar::update( void )
{
if (TheGlobalData->m_headless)
return;
getStarImage();
updateRadarAttackGlow();
if(m_controlBarSchemeManager)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4097,3 +4097,36 @@ void GameWindowManager::clearTabList( void )
{
m_tabList.clear();
}


GameWindow *DummyGameWindowManager::winGetWindowFromId(GameWindow *window, Int id)
{
window = GameWindowManager::winGetWindowFromId(window, id);
if (window != NULL)
return window;

// Just return any window, callers expect this to be non-null
return m_windowList;
}

WindowMsgHandledType DummyWindowSystem(GameWindow *window, UnsignedInt msg, WindowMsgData mData1, WindowMsgData mData2)
{
return MSG_IGNORED;
}

GameWindow *DummyGameWindowManager::winCreateFromScript(AsciiString filenameString, WindowLayoutInfo *info)
{
WindowLayoutInfo scriptInfo;
GameWindow* dummyWindow = winCreate(NULL, 0, 0, 0, 100, 100, DummyWindowSystem, NULL);
scriptInfo.windows.push_back(dummyWindow);
if (info)
*info = scriptInfo;
return dummyWindow;
}

DummyGameWindow::~DummyGameWindow()
{
}



36 changes: 21 additions & 15 deletions GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,13 @@ void GameClient::init( void )
TheDisplayStringManager->setName("TheDisplayStringManager");
}

// create the keyboard
TheKeyboard = createKeyboard();
TheKeyboard->init();
TheKeyboard->setName("TheKeyboard");
if (!TheGlobalData->m_headless)
{
// create the keyboard
TheKeyboard = createKeyboard();
TheKeyboard->init();
TheKeyboard->setName("TheKeyboard");
}

// allocate and load image collection for the GUI and just load the 256x256 ones for now
TheMappedImageCollection = MSGNEW("GameClientSubsystem") ImageCollection;
Expand Down Expand Up @@ -321,11 +324,14 @@ void GameClient::init( void )
if( TheFontLibrary )
TheFontLibrary->init();

// create the mouse
TheMouse = createMouse();
TheMouse->parseIni();
TheMouse->initCursorResources();
TheMouse->setName("TheMouse");
if (!TheGlobalData->m_headless)
{
// create the mouse
TheMouse = createMouse();
TheMouse->parseIni();
TheMouse->initCursorResources();
TheMouse->setName("TheMouse");
}

// instantiate the display
TheDisplay = createGameDisplay();
Expand All @@ -340,7 +346,7 @@ void GameClient::init( void )
}

// create the window manager
TheWindowManager = createWindowManager();
TheWindowManager = TheGlobalData->m_headless ? NEW DummyGameWindowManager : createWindowManager();
if( TheWindowManager )
{

Expand Down Expand Up @@ -397,11 +403,10 @@ void GameClient::init( void )
TheRayEffects->setName("TheRayEffects");
}

TheMouse->init(); //finish initializing the mouse.

// set the limits of the mouse now that we've created the display and such
if( TheMouse )
{
TheMouse->init(); //finish initializing the mouse.
TheMouse->setPosition( 0, 0 );
TheMouse->setMouseLimits();
TheMouse->setName("TheMouse");
Expand Down Expand Up @@ -532,7 +537,7 @@ void GameClient::update( void )
//Initial Game Codition. We must show the movie first and then we can display the shell
if(TheGlobalData->m_afterIntro && !TheDisplay->isMoviePlaying())
{
if( playSizzle && TheGlobalData->m_playSizzle )
if( playSizzle && TheGlobalData->m_playSizzle && !TheGlobalData->m_headless )
{
TheWritableGlobalData->m_allowExitOutOfMovies = TRUE;
if(TheGameLODManager && TheGameLODManager->didMemPass())
Expand Down Expand Up @@ -623,10 +628,9 @@ void GameClient::update( void )
if(TheGlobalData->m_playIntro || TheGlobalData->m_afterIntro)
{
// redraw all views, update the GUI
if (!TheGlobalData->m_headless)
{
TheDisplay->DRAW();
}
{
TheDisplay->UPDATE();
}
return;
Expand Down Expand Up @@ -746,10 +750,12 @@ void GameClient::update( void )
}

// update display
if (!TheGlobalData->m_headless)
{
TheDisplay->UPDATE();
}

if (!TheGlobalData->m_headless)
{
USE_PERF_TIMER(GameClient_draw)

Expand Down
9 changes: 6 additions & 3 deletions GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1298,7 +1298,7 @@ void InGameUI::setRadiusCursor(RadiusCursorType cursorType, const SpecialPowerTe
//-------------------------------------------------------------------------------------------------
void InGameUI::handleRadiusCursor()
{
if (!m_curRadiusCursor.isEmpty())
if (!m_curRadiusCursor.isEmpty() && TheMouse != NULL)
{
const MouseIO* mouseIO = TheMouse->getMouseStatus();
Coord3D pos;
Expand Down Expand Up @@ -1337,6 +1337,8 @@ void InGameUI::handleRadiusCursor()

void InGameUI::triggerDoubleClickAttackMoveGuardHint( void )
{
if (TheMouse == NULL)
return;
m_duringDoubleClickAttackMoveGuardHintTimer = 11;
const MouseIO* mouseIO = TheMouse->getMouseStatus();
TheTacticalView->screenToTerrain( &mouseIO->pos, &m_duringDoubleClickAttackMoveGuardHintStashedPosition );
Expand Down Expand Up @@ -2936,8 +2938,9 @@ void InGameUI::setGUICommand( const CommandButton *command )
}
setRadiusCursorNone();
}

m_mouseModeCursor = TheMouse->getMouseCursor();

if (TheMouse != NULL)
m_mouseModeCursor = TheMouse->getMouseCursor();

} // end setGUICommand

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2218,6 +2218,8 @@ const WaterHandle* TerrainLogic::getWaterHandle( Real x, Real y )

/**@todo: Remove this after we have all water types included
in water triggers. For now do special check for water grid mesh. */
// TheSuperHackers @logic-client-separation helmutbuhler 11/04/2025
// We shouldn't depend on TerrainVisual here.
Real meshZ;
if( TheTerrainVisual->getWaterGridHeight( x, y, &meshZ ) )
{
Expand Down
3 changes: 3 additions & 0 deletions GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6096,6 +6096,9 @@ const AsciiString& Object::getCommandSetString() const
//=============================================================================
Bool Object::canProduceUpgrade( const UpgradeTemplate *upgrade )
{
// TheSuperHackers @logic-client-separation helmutbuhler 11/04/2025
// TheControlBar belongs to the client, we shouldn't depend on that to check this.

// We need to have the button to make the upgrade. CommandSets are a weird Logic/Client hybrid.
const CommandSet *set = TheControlBar->findCommandSet(getCommandSetString());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,10 @@ void DockUpdate::loadDockPositions()
if( m_numberApproachPositions != DYNAMIC_APPROACH_VECTOR_FLAG )
{
// Dynamic means no bones

// TheSuperHackers @logic-client-separation helmutbuhler 11/04/2025
// We shouldn't depend on bones of a drawable here!

Coord3D approachBones[DEFAULT_APPROACH_VECTOR_SIZE];
m_numberApproachPositionBones = myDrawable->getPristineBonePositions( "DockWaiting", 1, approachBones, NULL, m_numberApproachPositions);
if( m_numberApproachPositions == m_approachPositions.size() )//safeguard: will always be true
Expand Down
2 changes: 2 additions & 0 deletions GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2598,6 +2598,8 @@ Bool Weapon::privateFireWeapon(
Bool reloaded = false;
if (m_ammoInClip > 0)
{
// TheSuperHackers @logic-client-separation helmutbuhler 11/04/2025
// barrelCount shouln't depend on Drawable, which belongs to client.
Int barrelCount = sourceObj->getDrawable()->getBarrelCount(m_wslot);
if (m_curBarrel >= barrelCount)
{
Expand Down
Loading
Loading