From d53b29c9f201d77f01eccd26f6b10dc7e951e280 Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:37:34 +0200 Subject: [PATCH 01/40] bugfix(w3d): Skip rivers that overflow the dynamic index buffer (#3489) --- .../Source/W3DDevice/GameClient/Water/W3DWater.cpp | 11 +++++++++-- Core/Libraries/Source/WWVegas/WW3D2/dx8indexbuffer.h | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWater.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWater.cpp index 4d4946c2b3a..130e2e3d82e 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWater.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Water/W3DWater.cpp @@ -2726,10 +2726,17 @@ void WaterRenderObjClass::drawRiverWater(PolygonTrigger *pTrig) static Bool doWobble = true; if (m_disableRiver) return; - m_drawingRiver = true; //allocate 2 triangles per side with 3 indices per triangle - DynamicIBAccessClass ib_access(BUFFER_TYPE_DYNAMIC_DX8,(rectangleCount+1)*2*3); + const Int indexCount = (rectangleCount+1)*2*3; + // TheSuperHackers @bugfix arcticdolphin 12/10/2026 Skip rivers that do not fit the 16 bit dynamic buffers. + DEBUG_ASSERTCRASH(indexCount <= DynamicIBAccessClass::MAX_INDEX_COUNT, ("River has too many points to draw")); + if (indexCount > DynamicIBAccessClass::MAX_INDEX_COUNT) + return; + + m_drawingRiver = true; + + DynamicIBAccessClass ib_access(BUFFER_TYPE_DYNAMIC_DX8,indexCount); { DynamicIBAccessClass::WriteLockClass lockib(&ib_access); UnsignedShort *curIb = lockib.Get_Index_Array(); diff --git a/Core/Libraries/Source/WWVegas/WW3D2/dx8indexbuffer.h b/Core/Libraries/Source/WWVegas/WW3D2/dx8indexbuffer.h index 3ae18aabe10..f69e6997e27 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/dx8indexbuffer.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/dx8indexbuffer.h @@ -119,6 +119,8 @@ class DynamicIBAccessClass void Allocate_DX8_Dynamic_Buffer(); public: + enum { MAX_INDEX_COUNT = 0xffff }; ///< index_count is an unsigned short + DynamicIBAccessClass(unsigned short type, unsigned short index_count); ~DynamicIBAccessClass(); From cfa0c8cdd354cacadb1bc6ff28501d8ade70c474 Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:55:48 +0200 Subject: [PATCH 02/40] fix(worldheightmap): Free the legacy cliff state buffer when loading blend tile data (#3482) --- .../Source/W3DDevice/GameClient/WorldHeightMap.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp index 1893a5376d4..12939a3dc62 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp @@ -1074,6 +1074,7 @@ Bool WorldHeightMap::ParseBlendTileData(DataChunkInput &file, DataChunkInfo *inf m_cellCliffState[j*m_flipStateWidth + i] = data[j*byteWidth + i]; } } + delete[] data; } else { file.readArrayOfBytes((char*)m_cellCliffState, m_height*m_flipStateWidth); } From ba0eb5d67e6de78a2d3225e98d1f48514bb6ddc5 Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:01:51 +0200 Subject: [PATCH 03/40] bugfix(worldheightmap): Reject map sizes that overflow the cell array allocations (#3486) --- .../W3DDevice/GameClient/WorldHeightMap.h | 1 + .../W3DDevice/GameClient/WorldHeightMap.cpp | 36 ++++++++++++++++--- .../Include/Common/MapReaderWriterInfo.h | 5 +++ .../Include/Common/MapReaderWriterInfo.h | 5 +++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h index 846c11bbd42..8532f6b2eab 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h @@ -238,6 +238,7 @@ class WorldHeightMap : public RefCountClass, public: // height map info. static Int getMinHeightValue() {return K_MIN_HEIGHT;} static Int getMaxHeightValue() {return K_MAX_HEIGHT;} + static Bool isValidMapSize(Int width, Int height, Int borderSize); ///< Within MAX_MAP_DIMENSION and MAX_MAP_AREA, width and height include the border UnsignedByte *getDataPtr() {return m_data;} diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp index 12939a3dc62..0f0b19a3cb6 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp @@ -827,6 +827,24 @@ Bool WorldHeightMap::ParseObjectsDataChunk(DataChunkInput &file, DataChunkInfo * return (file.parse(userData)); } +Bool WorldHeightMap::isValidMapSize(Int width, Int height, Int borderSize) +{ + if (width <= 0 || width > MAX_MAP_DIMENSION) + return false; + if (height <= 0 || height > MAX_MAP_DIMENSION) + return false; + if (width * height > MAX_MAP_AREA) + return false; + if (borderSize < 0 || borderSize > (width - 1) / 2 || borderSize > (height - 1) / 2) + return false; + return true; +} + +static Bool isValidHeightDataSize(DataChunkInput &file, Int dataSize, Int width, Int height) +{ + return dataSize > 0 && dataSize == width * height && (UnsignedInt)dataSize <= file.getChunkDataSizeLeft(); +} + /** * WorldHeightMap::ParseHeightMapData - read a height map chunk. * Format is the newer CHUNKY format. @@ -857,6 +875,10 @@ Bool WorldHeightMap::ParseHeightMapData(DataChunkInput &file, DataChunkInfo *inf m_borderSize = 0; } + if (!isValidMapSize(m_width, m_height, m_borderSize)) { + throw ERROR_CORRUPT_FILE_FORMAT; + } + if (info->version >= K_HEIGHT_MAP_VERSION_4) { Int numBorders = file.readInt(); m_boundaries.resize(numBorders); @@ -871,11 +893,12 @@ Bool WorldHeightMap::ParseHeightMapData(DataChunkInput &file, DataChunkInfo *inf } m_dataSize = file.readInt(); - m_data = MSGNEW("WorldHeightMap_ParseHeightMapData") UnsignedByte[m_dataSize]; - if (m_dataSize <= 0 || (m_dataSize != (m_width*m_height))) { + if (!isValidHeightDataSize(file, m_dataSize, m_width, m_height)) { throw ERROR_CORRUPT_FILE_FORMAT ; } + m_data = MSGNEW("WorldHeightMap_ParseHeightMapData") UnsignedByte[m_dataSize]; + Int numBytesX = (m_width+7)/8; //how many bytes to fit all bitflags Int numBytesY = m_height; m_seismicUpdateWidth=numBytesX; @@ -931,6 +954,10 @@ Bool WorldHeightMap::ParseSizeOnly(DataChunkInput &file, DataChunkInfo *info, vo m_borderSize = 0; } + if (!isValidMapSize(m_width, m_height, m_borderSize)) { + throw ERROR_CORRUPT_FILE_FORMAT; + } + if (info->version >= K_HEIGHT_MAP_VERSION_4) { Int numBorders = file.readInt(); m_boundaries.resize(numBorders); @@ -945,10 +972,11 @@ Bool WorldHeightMap::ParseSizeOnly(DataChunkInput &file, DataChunkInfo *info, vo } m_dataSize = file.readInt(); - m_data = MSGNEW("WorldHeightMap_ParseSizeOnly") UnsignedByte[m_dataSize]; - if (m_dataSize <= 0 || (m_dataSize != (m_width*m_height))) { + if (!isValidHeightDataSize(file, m_dataSize, m_width, m_height)) { throw ERROR_CORRUPT_FILE_FORMAT ; } + + m_data = MSGNEW("WorldHeightMap_ParseSizeOnly") UnsignedByte[m_dataSize]; file.readArrayOfBytes((char *)m_data, m_dataSize); // Resize me. if (info->version == K_HEIGHT_MAP_VERSION_1) { diff --git a/Generals/Code/GameEngine/Include/Common/MapReaderWriterInfo.h b/Generals/Code/GameEngine/Include/Common/MapReaderWriterInfo.h index c370ac5b3c0..0a3b698bcdc 100644 --- a/Generals/Code/GameEngine/Include/Common/MapReaderWriterInfo.h +++ b/Generals/Code/GameEngine/Include/Common/MapReaderWriterInfo.h @@ -53,6 +53,11 @@ #define K_LIGHTING_VERSION_3 3 // Added 2 additional global lights for terrain. #define K_WORLDDICT_VERSION_1 1 #define K_MAPPREVIEW_VERSION_1 1 + +// TheSuperHackers @bugfix arcticdolphin 12/10/2026 Map size limits. +#define MAX_MAP_DIMENSION 8192 // The shroud texture spans the map and shoreline tiles pack each coordinate into 16 bits +#define MAX_MAP_AREA (4096*4096) // Terrain, pathfinding and partition arrays allocate per cell in a 32 bit process + /** Virtual helper class, so that we can write map data using FILE* or CFile. */ class OutputStream { public: diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MapReaderWriterInfo.h b/GeneralsMD/Code/GameEngine/Include/Common/MapReaderWriterInfo.h index c114ad29109..02eb3a762be 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MapReaderWriterInfo.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MapReaderWriterInfo.h @@ -55,6 +55,11 @@ #define K_LIGHTING_VERSION_3 3 // Added 2 additional global lights for terrain. #define K_WORLDDICT_VERSION_1 1 #define K_MAPPREVIEW_VERSION_1 1 + +// TheSuperHackers @bugfix arcticdolphin 12/10/2026 Map size limits. +#define MAX_MAP_DIMENSION 8192 // The shroud texture spans the map and shoreline tiles pack each coordinate into 16 bits +#define MAX_MAP_AREA (4096*4096) // Terrain, pathfinding and partition arrays allocate per cell in a 32 bit process + /** Virtual helper class, so that we can write map data using FILE* or CFile. */ class OutputStream { public: From 85280dce7afe76513a51e3a3b9f09d5e940d6b94 Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:59:06 +0200 Subject: [PATCH 04/40] fix(lanapi): Fix GameInfo memory leaks in LANAPI::OnGameStart(), LANAPI::RequestGameLeave() (#3462) --- Core/GameEngine/Include/GameNetwork/GameInfo.h | 1 + .../GameEngine/Include/GameNetwork/GameSpy/StagingRoomGameInfo.h | 1 + Core/GameEngine/Include/GameNetwork/LANGameInfo.h | 1 + Core/GameEngine/Source/GameNetwork/LANAPI.cpp | 1 + Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp | 1 + Generals/Code/GameEngine/Include/Common/Recorder.h | 1 + Generals/Code/GameEngine/Include/GameNetwork/GameSpyGameInfo.h | 1 + GeneralsMD/Code/GameEngine/Include/Common/Recorder.h | 1 + GeneralsMD/Code/GameEngine/Include/GameNetwork/GameSpyGameInfo.h | 1 + 9 files changed, 9 insertions(+) diff --git a/Core/GameEngine/Include/GameNetwork/GameInfo.h b/Core/GameEngine/Include/GameNetwork/GameInfo.h index 5483bbeba58..06a44239451 100644 --- a/Core/GameEngine/Include/GameNetwork/GameInfo.h +++ b/Core/GameEngine/Include/GameNetwork/GameInfo.h @@ -302,6 +302,7 @@ class SkirmishGameInfo : public GameInfo, public Snapshot for (Int i = 0; i< MAX_SLOTS; ++i) setSlotPointer(i, &m_skirmishSlot[i]); } + virtual ~SkirmishGameInfo() override {} }; extern SkirmishGameInfo *TheSkirmishGameInfo; diff --git a/Core/GameEngine/Include/GameNetwork/GameSpy/StagingRoomGameInfo.h b/Core/GameEngine/Include/GameNetwork/GameSpy/StagingRoomGameInfo.h index d17569bc807..5ed92588a8b 100644 --- a/Core/GameEngine/Include/GameNetwork/GameSpy/StagingRoomGameInfo.h +++ b/Core/GameEngine/Include/GameNetwork/GameSpy/StagingRoomGameInfo.h @@ -96,6 +96,7 @@ class GameSpyStagingRoom : public GameInfo public: GameSpyStagingRoom(); + virtual ~GameSpyStagingRoom() override {} virtual void reset() override; void cleanUpSlotPointers(); diff --git a/Core/GameEngine/Include/GameNetwork/LANGameInfo.h b/Core/GameEngine/Include/GameNetwork/LANGameInfo.h index 2d0cdbd202a..1296d47dcb4 100644 --- a/Core/GameEngine/Include/GameNetwork/LANGameInfo.h +++ b/Core/GameEngine/Include/GameNetwork/LANGameInfo.h @@ -79,6 +79,7 @@ class LANGameInfo : public GameInfo public: LANGameInfo(); + virtual ~LANGameInfo() override {} void setSlot( Int slotNum, LANGameSlot slotInfo ); ///< Set the slot state (human, open, AI, etc) LANGameSlot* getLANSlot( Int slotNum ); ///< Get the slot const LANGameSlot* getConstLANSlot( Int slotNum ) const; ///< Get the slot diff --git a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp index ef244dd8675..e5d5accb78f 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPI.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPI.cpp @@ -693,6 +693,7 @@ void LANAPI::RequestGameLeave() // Exit out immediately if we're hosting OnPlayerLeave(m_name); removeGame(m_currentGame); + delete m_currentGame; m_currentGame = nullptr; m_inLobby = true; } diff --git a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp index 701ef3e987e..5075b7cfba0 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp @@ -233,6 +233,7 @@ void LANAPI::OnGameStart() DEBUG_LOG(("After transfer, we didn't really have the map. Bailing...")); OnPlayerLeave(m_name); removeGame(m_currentGame); + delete m_currentGame; m_currentGame = nullptr; m_inLobby = TRUE; diff --git a/Generals/Code/GameEngine/Include/Common/Recorder.h b/Generals/Code/GameEngine/Include/Common/Recorder.h index bd7b030e146..110e58bbc3f 100644 --- a/Generals/Code/GameEngine/Include/Common/Recorder.h +++ b/Generals/Code/GameEngine/Include/Common/Recorder.h @@ -42,6 +42,7 @@ class ReplayGameInfo : public GameInfo for (Int i = 0; i< MAX_SLOTS; ++i) setSlotPointer(i, &m_ReplaySlot[i]); } + virtual ~ReplayGameInfo() override {} }; enum RecorderModeType CPP_11(: Int) { diff --git a/Generals/Code/GameEngine/Include/GameNetwork/GameSpyGameInfo.h b/Generals/Code/GameEngine/Include/GameNetwork/GameSpyGameInfo.h index 5d136972f72..9664361f0c4 100644 --- a/Generals/Code/GameEngine/Include/GameNetwork/GameSpyGameInfo.h +++ b/Generals/Code/GameEngine/Include/GameNetwork/GameSpyGameInfo.h @@ -67,6 +67,7 @@ class GameSpyGameInfo : public GameInfo public: GameSpyGameInfo(); + virtual ~GameSpyGameInfo() override {} inline void setServer(SBServer server) { m_server = server; } inline SBServer getServer() { return m_server; } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h index fba73ad5ec3..1a82d84adfc 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/Recorder.h @@ -44,6 +44,7 @@ class ReplayGameInfo : public GameInfo for (Int i = 0; i< MAX_SLOTS; ++i) setSlotPointer(i, &m_ReplaySlot[i]); } + virtual ~ReplayGameInfo() override {} }; enum RecorderModeType CPP_11(: Int) { diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GameSpyGameInfo.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GameSpyGameInfo.h index a1acedac598..4b1e8ee7100 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GameSpyGameInfo.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GameSpyGameInfo.h @@ -67,6 +67,7 @@ class GameSpyGameInfo : public GameInfo public: GameSpyGameInfo(); + virtual ~GameSpyGameInfo() override {} inline void setServer(SBServer server) { m_server = server; } inline SBServer getServer() { return m_server; } From f42400624700f962e173ee8352cedb42eed5f8b4 Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:23:15 +0200 Subject: [PATCH 05/40] bugfix(network): Close sockets that fail to bind (#3488) --- Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp | 2 ++ Core/GameEngine/Source/GameNetwork/udp.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp b/Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp index 04e5f7166b0..0cb350227bc 100644 --- a/Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp +++ b/Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp @@ -1529,6 +1529,8 @@ Bool FirewallHelperClass::openSpareSocket(UnsignedShort port) { if (m_spareSockets[i].udp->Bind((UnsignedInt)0, port) != 0) { DEBUG_CRASH(("FirewallHelperClass::openSpareSocket - Failed to init spare socket")); + delete m_spareSockets[i].udp; + m_spareSockets[i].udp = nullptr; return FALSE; } diff --git a/Core/GameEngine/Source/GameNetwork/udp.cpp b/Core/GameEngine/Source/GameNetwork/udp.cpp index e5f30ccb795..e3ab907928b 100644 --- a/Core/GameEngine/Source/GameNetwork/udp.cpp +++ b/Core/GameEngine/Source/GameNetwork/udp.cpp @@ -174,6 +174,8 @@ Int UDP::Bind(UnsignedInt IP,UnsignedShort Port) { status=GetStatus(); //CERR("Bind failure (" << status << ") IP " << IP << " PORT " << Port ) + closesocket(fd); + fd=0; return(status); } From 12d0f88a450d418ad0f82cbb49d14962876848ca Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:02:43 +0200 Subject: [PATCH 06/40] fix(network): Release the disconnect chat message after sending it (#3498) --- Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp index f347db5a83f..6f08930bd3c 100644 --- a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp +++ b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp @@ -2509,6 +2509,8 @@ void ConnectionManager::sendDisconnectChat(UnicodeString text) { sendLocalCommandDirect(msg, 0xff ^ (1 << m_localSlot)); processDisconnectChat(msg); + + msg->detach(); } UnsignedShort ConnectionManager::sendFileAnnounce(AsciiString path, UnsignedByte playerMask) From 1ce1722d7e3c6956f9183ee2062b8a1e5a89095a Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:03:59 +0200 Subject: [PATCH 07/40] fix(lanapi): Delete the lobby player when they leave the lobby (#3499) --- Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp index 7f88b835f86..6aa2956df0d 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPIhandlers.cpp @@ -567,6 +567,7 @@ void LANAPI::handleRequestLobbyLeave( LANMessage *msg, UnsignedInt senderIP ) if (player->getIP() == senderIP) { removePlayer(player); + delete player; OnPlayerList(m_lobbyPlayers); break; } From bde9e9c166e7211c7be7a50707c1a3adfe0ce6fb Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:50:36 +0200 Subject: [PATCH 08/40] fix(addoncompat): Close fullviewport.dat file handle after reading it (#3476) --- Core/GameEngine/Source/Common/AddonCompat.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/GameEngine/Source/Common/AddonCompat.cpp b/Core/GameEngine/Source/Common/AddonCompat.cpp index 6d88feb47d6..a877ec1edda 100644 --- a/Core/GameEngine/Source/Common/AddonCompat.cpp +++ b/Core/GameEngine/Source/Common/AddonCompat.cpp @@ -29,6 +29,7 @@ Bool HasFullviewportDat() if (File* file = TheFileSystem->openFile("GenTool/fullviewport.dat", File::READ | File::BINARY)) { file->read(&value, 1); + file->close(); } return value != '0'; } From 0f6f0e48df2990c713cb69ca6de08b44b7efea85 Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:08:04 +0200 Subject: [PATCH 09/40] fix: Close leaked system handles in GameWindowManager, ImageCollection, MainMenuUtils (#3497) --- Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp | 2 ++ .../Source/GameClient/GUI/GameWindowManagerScript.cpp | 4 +++- .../Source/GameClient/GUI/GameWindowManagerScript.cpp | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp index 551071fd104..0db1233b077 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp @@ -776,6 +776,7 @@ int asyncGethostbyname(char * szName) /* Thread finished */ stat = 0; s_asyncDNSLookupInProgress = FALSE; + CloseHandle(s_asyncDNSThreadHandle); s_asyncDNSThreadHandle = nullptr; return( (s_asyncDNSThreadSucceeded)?LOOKUP_SUCCEEDED:LOOKUP_FAILED ); } @@ -833,6 +834,7 @@ void StopAsyncDNSCheck() MAYBE_UNUSED Int res = TerminateThread(s_asyncDNSThreadHandle, 0); (void)res; DEBUG_ASSERTCRASH(res, ("Could not terminate the Async DNS Lookup thread!")); // Thread still not killed! + CloseHandle(s_asyncDNSThreadHandle); } s_asyncDNSThreadHandle = nullptr; s_asyncDNSLookupInProgress = FALSE; diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp index e234b654278..9fc3840da7d 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp @@ -2747,7 +2747,9 @@ GameWindow *GameWindowManager::winCreateFromScript( AsciiString filenameString, { DEBUG_LOG(( "WinCreateFromScript: Error parsing layout block" )); - return FALSE; + inFile->close(); + inFile = nullptr; + return nullptr; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp index d588e90cc31..ce7723ac6d8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp @@ -2772,7 +2772,9 @@ GameWindow *GameWindowManager::winCreateFromScript( AsciiString filenameString, { DEBUG_LOG(( "WinCreateFromScript: Error parsing layout block" )); - return FALSE; + inFile->close(); + inFile = nullptr; + return nullptr; } From 7833a75c5e9765d104f39cd8e6c7de39f0ae8969 Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:00:41 +0200 Subject: [PATCH 10/40] fix(maputil): Close the file and free the buffer when a map preview copy fails (#3485) --- Core/GameEngine/Source/GameClient/MapUtil.cpp | 58 ++++++------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/MapUtil.cpp b/Core/GameEngine/Source/GameClient/MapUtil.cpp index ef776c2e1f7..5ea1a305944 100644 --- a/Core/GameEngine/Source/GameClient/MapUtil.cpp +++ b/Core/GameEngine/Source/GameClient/MapUtil.cpp @@ -1142,53 +1142,38 @@ const MapMetaData *MapCache::findMap(AsciiString mapName) // ------------------------------------------------------------------------------------------------ /** Embed the pristine map into the xfer stream */ // ------------------------------------------------------------------------------------------------ -static void copyFromBigToDir( const AsciiString& infile, const AsciiString& outfile ) +static Bool copyFromBigToDir( const AsciiString& infile, const AsciiString& outfile ) { - // open the map file - File *file = TheFileSystem->openFile( infile.str(), File::READ | File::BINARY ); if( file == nullptr ) { DEBUG_CRASH(( "copyFromBigToDir - Error opening source file '%s'", infile.str() )); - throw SC_INVALID_DATA; + return FALSE; } - // how big is the map file - Int fileSize = file->seek( 0, File::END ); - - - // rewind to beginning of file + const Int fileSize = file->seek( 0, File::END ); file->seek( 0, File::START ); - // allocate buffer big enough to hold the entire map file char *buffer = NEW char[ fileSize ]; - if( buffer == nullptr ) - { - DEBUG_CRASH(( "copyFromBigToDir - Unable to allocate buffer for file '%s'", infile.str() )); - throw SC_INVALID_DATA; - } - - // copy the file to the buffer - if( file->read( buffer, fileSize ) < fileSize ) - { - DEBUG_CRASH(( "copyFromBigToDir - Error reading from file '%s'", infile.str() )); - throw SC_INVALID_DATA; - } - // close the BIG file + const Bool readOk = file->read( buffer, fileSize ) == fileSize; file->close(); + DEBUG_ASSERTCRASH( readOk, ( "copyFromBigToDir - Error reading from file '%s'", infile.str() ) ); - File *filenew = TheFileSystem->openFile( outfile.str(), File::WRITE | File::CREATE | File::BINARY ); - - if( !filenew || filenew->write(buffer, fileSize) < fileSize) + Bool writeOk = FALSE; + if( readOk ) { - DEBUG_CRASH(( "copyFromBigToDir - Error writing to file '%s'", outfile.str() )); - throw SC_INVALID_DATA; + File *filenew = TheFileSystem->openFile( outfile.str(), File::WRITE | File::CREATE | File::BINARY ); + if( filenew != nullptr ) + { + writeOk = filenew->write( buffer, fileSize ) == fileSize; + filenew->close(); + } + DEBUG_ASSERTCRASH( writeOk, ( "copyFromBigToDir - Error writing to file '%s'", outfile.str() ) ); } - filenew->close(); - - // delete the buffer delete [] buffer; + + return writeOk; } Image *getMapPreviewImage( AsciiString mapName ) @@ -1233,16 +1218,7 @@ Image *getMapPreviewImage( AsciiString mapName ) mapPreviewDir.concat(name); - Bool success = false; - try - { - copyFromBigToDir(tgaName, mapPreviewDir); - success = true; - } - catch (...) - { - success = false; // no rethrow - } + const Bool success = copyFromBigToDir(tgaName, mapPreviewDir); if (success) { From 35a00704a7c53d264954f77ccb44b356b095ee86 Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:31:48 +0200 Subject: [PATCH 11/40] perf(displaystring): Rebuild static text only when its hotkey setting changes (#3443) --- .../Source/WWVegas/WW3D2/render2dsentence.cpp | 3 --- .../Source/WWVegas/WW3D2/render2dsentence.h | 4 +--- .../W3DDevice/GameClient/W3DDisplayString.h | 1 + .../W3DDevice/GameClient/W3DDisplayString.cpp | 20 +++++++++++++------ .../W3DDevice/GameClient/W3DDisplayString.h | 1 + .../W3DDevice/GameClient/W3DDisplayString.cpp | 20 +++++++++++++------ 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp index 64001c0bc07..11567667e60 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp @@ -61,7 +61,6 @@ Render2DSentenceClass::Render2DSentenceClass () : TextureStartX (0), CurSurface (nullptr), CurrTextureSize (0), - MonoSpaced (false), IsClippedEnabled (false), ClipRect (0, 0, 0, 0), BaseLocation (0, 0), @@ -147,8 +146,6 @@ Render2DSentenceClass::Reset () } Cursor.Set (0, 0); - MonoSpaced = false; - ParseHotKey = false; Release_Pending_Surfaces (); Reset_Sentence_Data (); diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h index 15426c1e950..c41867c198f 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h @@ -189,6 +189,7 @@ class Render2DSentenceClass { // void Build_Sentence (const WCHAR *text, int *hkX, int *hkY); void Draw_Sentence (uint32 color = 0xFFFFFFFF); + bool Has_Sentence_Data () const { return SentenceData.Count () > 0; } // // Texture hint @@ -196,8 +197,6 @@ class Render2DSentenceClass { void Set_Texture_Size_Hint( int hint ) { TextureSizeHint = hint; } int Get_Texture_Size_Hint() const { return TextureSizeHint; } - void Set_Mono_Spaced( bool onoff ) { MonoSpaced = onoff; } - private: // @@ -253,7 +252,6 @@ class Render2DSentenceClass { int CurrTextureSize; int TextureSizeHint; SurfaceClass * CurSurface; - bool MonoSpaced; float WrapWidth; bool Centered; // Determines whether or not to center each line RectClass ClipRect; diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h index 80123dd0919..e57784c8f70 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h @@ -97,6 +97,7 @@ class W3DDisplayString : public DisplayString Render2DSentenceClass m_textRendererHotKey; ///< for drawing text Bool m_textChanged; ///< when contents of string change this is TRUE Bool m_fontChanged; ///< when font has chagned this is TRUE + Bool m_clipChanged; ///< when clip region has changed this is TRUE UnicodeString m_hotkey; ///< holds the current hotkey marker. Bool m_useHotKey; ICoord2D m_hotKeyPos; diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp index eafcf5fe6d6..48ede071d5e 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp @@ -86,6 +86,7 @@ W3DDisplayString::W3DDisplayString() m_size.x = 0; m_size.y = 0; m_fontChanged = FALSE; + m_clipChanged = FALSE; m_clipRegion.lo.x = 0; m_clipRegion.lo.y = 0; m_clipRegion.hi.x = 0; @@ -170,19 +171,18 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr { if(m_useHotKey) { - m_textRenderer.Set_Hot_Key_Parse(TRUE); m_textRenderer.Build_Sentence( getText().str(), &m_hotKeyPos.x, &m_hotKeyPos.y ); m_hotkey.translate(TheHotKeyManager->searchHotKey(getText())); if(!m_hotkey.isEmpty()) m_textRendererHotKey.Build_Sentence(m_hotkey.str(), nullptr, nullptr); else - { - m_useHotKey = FALSE; m_textRendererHotKey.Reset(); - } } else + { m_textRenderer.Build_Sentence( getText().str(), nullptr, nullptr ); + m_textRendererHotKey.Reset(); + } m_fontChanged = FALSE; m_textChanged = FALSE; needNewPolys = TRUE; @@ -193,7 +193,9 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr // if our position has changed, or our colors have changed, or our // text data has changed, we need to redo the texture quads // + // TheSuperHackers @bugfix arcticdolphin 08/10/2026 Redo the quads when the clip region changes. if( needNewPolys || + m_clipChanged || x != m_textPos.x || y != m_textPos.y || color != m_currTextColor || @@ -205,6 +207,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr m_textPos.y = y; m_currTextColor = color; m_currDropColor = dropColor; + m_clipChanged = FALSE; // reset the quads m_textRenderer.Reset_Polys(); @@ -217,7 +220,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr m_textRenderer.Set_Location( Vector2( m_textPos.x, m_textPos.y ) ); m_textRenderer.Draw_Sentence( m_currTextColor ); - if (m_useHotKey) + if (m_textRendererHotKey.Has_Sentence_Data()) { m_textRendererHotKey.Reset_Polys(); m_textRendererHotKey.Set_Location( Vector2( m_textPos.x + m_hotKeyPos.x , m_textPos.y +m_hotKeyPos.y) ); @@ -228,7 +231,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr TheDisplay->flush(); - if (m_useHotKey) + if (m_textRendererHotKey.Has_Sentence_Data()) { m_textRendererHotKey.Render(); } @@ -336,6 +339,7 @@ void W3DDisplayString::setClipRegion( IRegion2D *region ) // assign new region m_clipRegion = *region; + m_clipChanged = TRUE; // set new region in renderer m_textRenderer.Set_Clipping_Rect( RectClass( m_clipRegion.lo.x, @@ -388,6 +392,10 @@ void W3DDisplayString::setWordWrap( Int wordWrap ) void W3DDisplayString::setUseHotkey( Bool useHotkey, Color hotKeyColor ) { + // TheSuperHackers @performance arcticdolphin 08/10/2026 Skip the rebuild when the request is unchanged. + if( m_useHotKey == useHotkey && m_hotKeyColor == hotKeyColor ) + return; + m_useHotKey = useHotkey; m_hotKeyColor = hotKeyColor; m_textRenderer.Set_Hot_Key_Parse(useHotkey); diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h index 0d49e002dab..8e0c6a39ef5 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h @@ -97,6 +97,7 @@ class W3DDisplayString : public DisplayString Render2DSentenceClass m_textRendererHotKey; ///< for drawing text Bool m_textChanged; ///< when contents of string change this is TRUE Bool m_fontChanged; ///< when font has changed this is TRUE + Bool m_clipChanged; ///< when clip region has changed this is TRUE UnicodeString m_hotkey; ///< holds the current hotkey marker. Bool m_useHotKey; ICoord2D m_hotKeyPos; diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp index c8a4b78ea59..10f36a95cea 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp @@ -86,6 +86,7 @@ W3DDisplayString::W3DDisplayString() m_size.x = 0; m_size.y = 0; m_fontChanged = FALSE; + m_clipChanged = FALSE; m_clipRegion.lo.x = 0; m_clipRegion.lo.y = 0; m_clipRegion.hi.x = 0; @@ -170,19 +171,18 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr { if(m_useHotKey) { - m_textRenderer.Set_Hot_Key_Parse(TRUE); m_textRenderer.Build_Sentence( getText().str(), &m_hotKeyPos.x, &m_hotKeyPos.y ); m_hotkey.translate(TheHotKeyManager->searchHotKey(getText())); if(!m_hotkey.isEmpty()) m_textRendererHotKey.Build_Sentence(m_hotkey.str(), nullptr, nullptr); else - { - m_useHotKey = FALSE; m_textRendererHotKey.Reset(); - } } else + { m_textRenderer.Build_Sentence( getText().str(), nullptr, nullptr ); + m_textRendererHotKey.Reset(); + } m_fontChanged = FALSE; m_textChanged = FALSE; needNewPolys = TRUE; @@ -193,7 +193,9 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr // if our position has changed, or our colors have changed, or our // text data has changed, we need to redo the texture quads // + // TheSuperHackers @bugfix arcticdolphin 08/10/2026 Redo the quads when the clip region changes. if( needNewPolys || + m_clipChanged || x != m_textPos.x || y != m_textPos.y || color != m_currTextColor || @@ -205,6 +207,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr m_textPos.y = y; m_currTextColor = color; m_currDropColor = dropColor; + m_clipChanged = FALSE; // reset the quads m_textRenderer.Reset_Polys(); @@ -217,7 +220,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr m_textRenderer.Set_Location( Vector2( m_textPos.x, m_textPos.y ) ); m_textRenderer.Draw_Sentence( m_currTextColor ); - if (m_useHotKey) + if (m_textRendererHotKey.Has_Sentence_Data()) { m_textRendererHotKey.Reset_Polys(); m_textRendererHotKey.Set_Location( Vector2( m_textPos.x + m_hotKeyPos.x , m_textPos.y +m_hotKeyPos.y) ); @@ -228,7 +231,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr TheDisplay->flush(); - if (m_useHotKey) + if (m_textRendererHotKey.Has_Sentence_Data()) { m_textRendererHotKey.Render(); } @@ -336,6 +339,7 @@ void W3DDisplayString::setClipRegion( IRegion2D *region ) // assign new region m_clipRegion = *region; + m_clipChanged = TRUE; // set new region in renderer m_textRenderer.Set_Clipping_Rect( RectClass( m_clipRegion.lo.x, @@ -388,6 +392,10 @@ void W3DDisplayString::setWordWrap( Int wordWrap ) void W3DDisplayString::setUseHotkey( Bool useHotkey, Color hotKeyColor ) { + // TheSuperHackers @performance arcticdolphin 08/10/2026 Skip the rebuild when the request is unchanged. + if( m_useHotKey == useHotkey && m_hotKeyColor == hotKeyColor ) + return; + m_useHotKey = useHotkey; m_hotKeyColor = hotKeyColor; m_textRenderer.Set_Hot_Key_Parse(useHotkey); From ba92aba0bff86db2572e4fbfbbcd58172020c218 Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:08:58 +0200 Subject: [PATCH 12/40] bugfix(w3d): Fix leaked surface copy in screenshot and movie capture (#3477) --- .../Include/W3DDevice/GameClient/W3DScreenshot.h | 2 +- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 4 ++-- .../Source/W3DDevice/GameClient/stb_image_write_impl.cpp | 4 ++-- Generals/Code/Libraries/Source/WWVegas/WW3D2/ww3d.cpp | 4 ++-- GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/ww3d.cpp | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index e6edec10cd8..184a6573839 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 TheSuperHackers +** Copyright 2026 TheSuperHackers ** ** 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 diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 0d0d851eb63..7492aaa7c5c 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 TheSuperHackers +** Copyright 2026 TheSuperHackers ** ** 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 @@ -185,7 +185,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality) return; } - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)); DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); surface->Release_Ref(); diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp index 2264ba2c403..cf30d9ba0cd 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp @@ -1,6 +1,6 @@ /* -** Command & Conquer Generals(tm) -** Copyright 2025 TheSuperHackers +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers ** ** 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 diff --git a/Generals/Code/Libraries/Source/WWVegas/WW3D2/ww3d.cpp b/Generals/Code/Libraries/Source/WWVegas/WW3D2/ww3d.cpp index c12469332d5..2e2e8974ad2 100644 --- a/Generals/Code/Libraries/Source/WWVegas/WW3D2/ww3d.cpp +++ b/Generals/Code/Libraries/Source/WWVegas/WW3D2/ww3d.cpp @@ -1368,7 +1368,7 @@ void WW3D::Make_Screen_Shot( const char * filename_base , const float gamma, con SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)); DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); surface->Release_Ref(); @@ -1718,7 +1718,7 @@ void WW3D::Update_Movie_Capture() SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)); DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); surface->Release_Ref(); diff --git a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/ww3d.cpp b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/ww3d.cpp index e9ba5f5f652..25a30a04496 100644 --- a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/ww3d.cpp +++ b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/ww3d.cpp @@ -1373,7 +1373,7 @@ void WW3D::Make_Screen_Shot( const char * filename_base , const float gamma, con SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)); DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); surface->Release_Ref(); @@ -1723,7 +1723,7 @@ void WW3D::Update_Movie_Capture() SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)); DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); surface->Release_Ref(); From 5bf92ebf5db5621f70e72a166434c786a885829c Mon Sep 17 00:00:00 2001 From: ArcticDolphin <5984296+tintinhamans@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:14:45 +0200 Subject: [PATCH 13/40] bugfix(gamewindow): Remove destroyed windows from the modal stack and prevent duplicate modals for the same window (#3224) --- .../Include/GameClient/GameWindowManager.h | 4 +- .../GameClient/GUI/GameWindowManager.cpp | 91 ++++++++--------- .../Include/GameClient/GameWindowManager.h | 4 +- .../GameClient/GUI/GameWindowManager.cpp | 99 +++++++++---------- 4 files changed, 87 insertions(+), 111 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameClient/GameWindowManager.h b/Generals/Code/GameEngine/Include/GameClient/GameWindowManager.h index 4ce12aa8964..6b7989099e2 100644 --- a/Generals/Code/GameEngine/Include/GameClient/GameWindowManager.h +++ b/Generals/Code/GameEngine/Include/GameClient/GameWindowManager.h @@ -274,8 +274,7 @@ friend class GameWindow; virtual GameWindow *winGetCapture(); ///< current mouse capture settings virtual Int winSetModal( GameWindow *window ); ///< put at top of modal stack - virtual Int winUnsetModal( GameWindow *window ); /**< take window off modal stack, if window is - not at top of stack and error will occur */ + virtual Int winUnsetModal( GameWindow *window ); ///< take window off the modal stack from anywhere in the stack //--------------------------------------------------------------------------- ///////////////////////////////////////////////////////////////////////////// @@ -331,7 +330,6 @@ friend class GameWindow; protected: void processDestroyList(); ///< process windows waiting to be killed - void removeWindowFromModalStack( GameWindow *window ); Int drawWindow( GameWindow *window ); ///< draw this window diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp index 9fbf531b166..f836424db2c 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp @@ -96,22 +96,13 @@ void GameWindowManager::processDestroyList() next = doDestroy->m_next; - // Check to see if this window is "special" - if( m_mouseCaptor == doDestroy ) - winRelease( doDestroy ); - - if( m_keyboardFocus == doDestroy ) - winSetFocus( nullptr ); - - if( m_currMouseRgn == doDestroy ) - m_currMouseRgn = nullptr; - - if( m_grabWindow == doDestroy ) - m_grabWindow = nullptr; - // send the destroy message to the window we're about to kill winSendSystemMsg( doDestroy, GWM_DESTROY, 0, 0 ); + DEBUG_ASSERTCRASH( m_mouseCaptor != doDestroy && m_keyboardFocus != doDestroy + && m_currMouseRgn != doDestroy && m_grabWindow != doDestroy, + ("processDestroyList: manager still points at a window being destroyed") ); + DEBUG_ASSERTCRASH(doDestroy->winGetUserData() == nullptr, ("Win user data is expected to be deleted now")); // free the memory @@ -1516,7 +1507,7 @@ Int GameWindowManager::winDestroy( GameWindow *window ) if( m_keyboardFocus == window ) winSetFocus( nullptr ); - removeWindowFromModalStack( window ); + winUnsetModal( window ); if( m_currMouseRgn == window ) m_currMouseRgn = nullptr; @@ -1600,6 +1591,24 @@ Int GameWindowManager::winSetModal( GameWindow *window ) DEBUG_LOG(( "WinSetModal: Non Root window attempted to go modal." )); return WIN_ERR_INVALID_PARAMETER; // return error if not } + + // TheSuperHackers @bugfix arcticdolphin 08/09/2026 If already modal, move to the top instead of duplicating. + ModalWindow *previous = nullptr; + for( ModalWindow *existing = m_modalHead; existing != nullptr; previous = existing, existing = existing->next ) + { + if( existing->window != window ) + continue; + + if( previous != nullptr ) + { + previous->next = existing->next; + existing->next = m_modalHead; + m_modalHead = existing; + } + + return WIN_ERR_OK; + } + // Allocate new Modal Window Entry modal = newInstance(ModalWindow); if( modal == nullptr ) @@ -1618,54 +1627,36 @@ Int GameWindowManager::winSetModal( GameWindow *window ) } //------------------------------------------------------------------------------------------------- -/** pops window off of the modal stack. If this window is not the top - * of the modal stack an error will occur. */ +/** takes the window off the modal stack from anywhere in the stack */ //------------------------------------------------------------------------------------------------- +// TheSuperHackers @bugfix arcticdolphin 07/09/2026 Remove the window from anywhere in the modal stack, not just the top, so a destroyed window cannot leave a dangling entry behind. Int GameWindowManager::winUnsetModal( GameWindow *window ) { - ModalWindow *next; - if( window == nullptr ) return WIN_ERR_INVALID_WINDOW; - // verify entry is at top of list - if( (m_modalHead == nullptr) || (m_modalHead->window != window) ) - { - - // return error if not - DEBUG_LOG(( "WinUnsetModal: Invalid window attempting to unset modal (%d)", - window->winGetWindowId() )); - return WIN_ERR_GENERAL_FAILURE; - - } - - // remove from top of list - next = m_modalHead->next; - deleteInstance(m_modalHead); - m_modalHead = next; - - return WIN_ERR_OK; - -} + ModalWindow *previous = nullptr; + ModalWindow *modal = m_modalHead; -// TheSuperHackers @bugfix arcticdolphin 27/08/2026 Remove destroyed windows from the entire modal stack. -void GameWindowManager::removeWindowFromModalStack( GameWindow *window ) -{ - ModalWindow **link = &m_modalHead; - - while( *link ) + while( modal != nullptr ) { - ModalWindow *modal = *link; - - if( modal->window != window ) + if( modal->window == window ) { - link = &modal->next; - continue; + if( previous != nullptr ) + previous->next = modal->next; + else + m_modalHead = modal->next; + + deleteInstance(modal); + return WIN_ERR_OK; } - *link = modal->next; - deleteInstance(modal); + previous = modal; + modal = modal->next; } + + return WIN_ERR_GENERAL_FAILURE; + } //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/GameWindowManager.h b/GeneralsMD/Code/GameEngine/Include/GameClient/GameWindowManager.h index 8d0bfb6686d..0ae8cf8d198 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/GameWindowManager.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/GameWindowManager.h @@ -278,8 +278,7 @@ class GameWindowManager : public SubsystemInterface virtual GameWindow *winGetCapture(); ///< current mouse capture settings virtual Int winSetModal(GameWindow* window); ///< put at top of modal stack - virtual Int winUnsetModal(GameWindow* window); /**< take window off modal stack, if window is - not at top of stack and error will occur */ + virtual Int winUnsetModal( GameWindow *window ); ///< take window off the modal stack from anywhere in the stack //--------------------------------------------------------------------------- ///////////////////////////////////////////////////////////////////////////// @@ -335,7 +334,6 @@ class GameWindowManager : public SubsystemInterface protected: void processDestroyList(); ///< process windows waiting to be killed - void removeWindowFromModalStack( GameWindow *window ); Int drawWindow(GameWindow* window); ///< draw this window diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp index a80e368fc86..33ad638e060 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp @@ -96,24 +96,13 @@ void GameWindowManager::processDestroyList() next = doDestroy->m_next; - // Check to see if this window is "special" - if (m_mouseCaptor == doDestroy) - winRelease(doDestroy); - - if (m_keyboardFocus == doDestroy) - winSetFocus(NULL); - - removeWindowFromModalStack(doDestroy); - - if( m_currMouseRgn == doDestroy ) - m_currMouseRgn = nullptr; - - if (m_grabWindow == doDestroy) - m_grabWindow = NULL; - // send the destroy message to the window we're about to kill winSendSystemMsg(doDestroy, GWM_DESTROY, 0, 0); + DEBUG_ASSERTCRASH( m_mouseCaptor != doDestroy && m_keyboardFocus != doDestroy + && m_currMouseRgn != doDestroy && m_grabWindow != doDestroy, + ("processDestroyList: manager still points at a window being destroyed") ); + DEBUG_ASSERTCRASH(doDestroy->winGetUserData() == NULL, ("Win user data is expected to be deleted now")); // free the memory @@ -1422,7 +1411,7 @@ Int GameWindowManager::winDestroy(GameWindow* window) if (m_keyboardFocus == window) winSetFocus(NULL); - removeWindowFromModalStack( window ); + winUnsetModal( window ); if (m_currMouseRgn == window) m_currMouseRgn = NULL; @@ -1506,6 +1495,24 @@ Int GameWindowManager::winSetModal(GameWindow* window) DEBUG_LOG(("WinSetModal: Non Root window attempted to go modal.")); return WIN_ERR_INVALID_PARAMETER; // return error if not } + + // TheSuperHackers @bugfix arcticdolphin 08/09/2026 If already modal, move to the top instead of duplicating. + ModalWindow *previous = nullptr; + for( ModalWindow *existing = m_modalHead; existing != nullptr; previous = existing, existing = existing->next ) + { + if( existing->window != window ) + continue; + + if( previous != nullptr ) + { + previous->next = existing->next; + existing->next = m_modalHead; + m_modalHead = existing; + } + + return WIN_ERR_OK; + } + // Allocate new Modal Window Entry modal = newInstance(ModalWindow); if (modal == NULL) @@ -1524,54 +1531,36 @@ Int GameWindowManager::winSetModal(GameWindow* window) } //------------------------------------------------------------------------------------------------- -/** pops window off of the modal stack. If this window is not the top - * of the modal stack an error will occur. */ - //------------------------------------------------------------------------------------------------- -Int GameWindowManager::winUnsetModal(GameWindow* window) +/** takes the window off the modal stack from anywhere in the stack */ +//------------------------------------------------------------------------------------------------- +// TheSuperHackers @bugfix arcticdolphin 07/09/2026 Remove the window from anywhere in the modal stack, not just the top, so a destroyed window cannot leave a dangling entry behind. +Int GameWindowManager::winUnsetModal( GameWindow *window ) { - ModalWindow* next; - - if (window == NULL) + if( window == nullptr ) return WIN_ERR_INVALID_WINDOW; - // verify entry is at top of list - if ((m_modalHead == NULL) || (m_modalHead->window != window)) - { - - // return error if not - DEBUG_LOG(("WinUnsetModal: Invalid window attempting to unset modal (%d)", - window->winGetWindowId())); - return WIN_ERR_GENERAL_FAILURE; - - } - - // remove from top of list - next = m_modalHead->next; - deleteInstance(m_modalHead); - m_modalHead = next; + ModalWindow *previous = nullptr; + ModalWindow *modal = m_modalHead; - return WIN_ERR_OK; - -} - -// TheSuperHackers @bugfix arcticdolphin 27/08/2026 Remove destroyed windows from the entire modal stack. -void GameWindowManager::removeWindowFromModalStack( GameWindow *window ) -{ - ModalWindow **link = &m_modalHead; - - while( *link ) + while( modal != nullptr ) { - ModalWindow *modal = *link; - - if( modal->window != window ) + if( modal->window == window ) { - link = &modal->next; - continue; + if( previous != nullptr ) + previous->next = modal->next; + else + m_modalHead = modal->next; + + deleteInstance(modal); + return WIN_ERR_OK; } - *link = modal->next; - deleteInstance(modal); + previous = modal; + modal = modal->next; } + + return WIN_ERR_GENERAL_FAILURE; + } //------------------------------------------------------------------------------------------------- From f1ae9e2153998c56e83f76bea9367ad8fe738ab7 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:30:26 +0100 Subject: [PATCH 14/40] perf(script): Reduce cost of ScriptEngine::findScript() by around 85% (#3096) --- Generals/Code/GameEngine/Include/GameLogic/Scripts.h | 6 +++--- Generals/Code/GameEngine/Include/GameLogic/SidesList.h | 2 +- GeneralsMD/Code/GameEngine/Include/GameLogic/Scripts.h | 6 +++--- GeneralsMD/Code/GameEngine/Include/GameLogic/SidesList.h | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameLogic/Scripts.h b/Generals/Code/GameEngine/Include/GameLogic/Scripts.h index c2a85ecfd0f..4f061b065b5 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/Scripts.h +++ b/Generals/Code/GameEngine/Include/GameLogic/Scripts.h @@ -142,7 +142,7 @@ class ScriptGroup : public MemoryPoolObject, public Snapshot void setWarnings(Bool warnings) { m_hasWarnings = warnings;} void setNextGroup(ScriptGroup *pGr) {m_nextGroup = pGr;} - AsciiString getName() const { return m_groupName;} + const AsciiString& getName() const { return m_groupName;} Bool isActive() const { return m_isGroupActive;} Bool isSubroutine() const { return m_isGroupSubroutine;} Bool hasWarnings() const { return m_hasWarnings;} @@ -661,7 +661,7 @@ class Script : public MemoryPoolObject, public Snapshot Real getCurTime() {return m_curTime;} Int getDelayEvalSeconds() {return m_delayEvaluationSeconds;} - AsciiString getName() const { return m_scriptName;} + const AsciiString& getName() const { return m_scriptName;} AsciiString getComment() const {return m_comment;} AsciiString getActionComment() const {return m_actionComment;} AsciiString getConditionComment() const {return m_conditionComment;} @@ -691,7 +691,7 @@ class Script : public MemoryPoolObject, public Snapshot OrCondition *findPreviousOrCondition( OrCondition *curOr ); // Support routines for ScriptEngine - - AsciiString getConditionTeamName() {return m_conditionTeamName;} + const AsciiString& getConditionTeamName() const {return m_conditionTeamName;} void setConditionTeamName(AsciiString teamName) {m_conditionTeamName = teamName;} }; diff --git a/Generals/Code/GameEngine/Include/GameLogic/SidesList.h b/Generals/Code/GameEngine/Include/GameLogic/SidesList.h index c91c2aee3ce..5ba8d205448 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/SidesList.h +++ b/Generals/Code/GameEngine/Include/GameLogic/SidesList.h @@ -330,7 +330,7 @@ class BuildListInfo : public MemoryPoolObject, public Snapshot const Coord2D *getRallyOffset() const {return &m_rallyPointOffset;} Real getAngle() const {return m_angle;} Bool isInitiallyBuilt() {return m_isInitiallyBuilt;} - AsciiString getScript() {return m_script;} + const AsciiString& getScript() const {return m_script;} Int getHealth() {return m_health;} Bool getWhiner() {return m_whiner;} Bool getUnsellable() {return m_unsellable;} diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Scripts.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Scripts.h index 561807e7360..fb4bcded355 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Scripts.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Scripts.h @@ -135,7 +135,7 @@ class ScriptGroup : public MemoryPoolObject, public Snapshot void setWarnings(Bool warnings) { m_hasWarnings = warnings; } void setNextGroup(ScriptGroup* pGr) { m_nextGroup = pGr; } - AsciiString getName() const { return m_groupName;} + const AsciiString& getName() const { return m_groupName;} Bool isActive() const { return m_isGroupActive;} Bool isSubroutine() const { return m_isGroupSubroutine;} Bool hasWarnings() const { return m_hasWarnings;} @@ -669,7 +669,7 @@ class Script : public MemoryPoolObject, public Snapshot Real getCurTime() {return m_curTime;} Int getDelayEvalSeconds() {return m_delayEvaluationSeconds;} - AsciiString getName() const { return m_scriptName;} + const AsciiString& getName() const { return m_scriptName;} AsciiString getComment() const {return m_comment;} AsciiString getActionComment() const {return m_actionComment;} AsciiString getConditionComment() const {return m_conditionComment;} @@ -699,7 +699,7 @@ class Script : public MemoryPoolObject, public Snapshot OrCondition* findPreviousOrCondition(OrCondition* curOr); // Support routines for ScriptEngine - - AsciiString getConditionTeamName() {return m_conditionTeamName;} + const AsciiString& getConditionTeamName() const {return m_conditionTeamName;} void setConditionTeamName(AsciiString teamName) {m_conditionTeamName = teamName;} }; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/SidesList.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/SidesList.h index 55cbcd800e5..3713fc1cc35 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/SidesList.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/SidesList.h @@ -330,7 +330,7 @@ class BuildListInfo : public MemoryPoolObject, public Snapshot const Coord2D *getRallyOffset() const {return &m_rallyPointOffset;} Real getAngle() const {return m_angle;} Bool isInitiallyBuilt() {return m_isInitiallyBuilt;} - AsciiString getScript() {return m_script;} + const AsciiString& getScript() const {return m_script;} Int getHealth() {return m_health;} Bool getWhiner() {return m_whiner;} Bool getUnsellable() {return m_unsellable;} From 97a2f3b4625cb6952b9913b5fe74081d19a1b82b Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:33:04 +0100 Subject: [PATCH 15/40] bugfix(pathfinder): Prevent a retail crash where the head of the closed list has no associated m_info (#3113) --- Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index e4773dc4cc8..d2f9da3f721 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -1976,7 +1976,19 @@ void PathfindCell::putOnClosedList( PathfindCellList &list ) m_info->m_prevOpen = nullptr; m_info->m_nextOpen = list.m_head ? list.m_head->m_info : nullptr; if (list.m_head) +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info This is only here to catch a crash point in the retail compatible pathfinding + // This crash mode occurs due to the closed list head not having an m_info associated with it + // A node cannot be put onto the closed list without an m_info under normal conditions + { + if (list.m_head->m_info) + { + list.m_head->m_info->m_prevOpen = this->m_info; + } + } +#else list.m_head->m_info->m_prevOpen = this->m_info; +#endif list.m_head = this; } From fcc79fbe23dceb67b1c7026707779e9a0bb83b57 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:34:55 +0100 Subject: [PATCH 16/40] bugfix(pathfinder): Prevent a retail crash where a pathfindCell has a dangling linked list pointer (#3113) --- Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index d2f9da3f721..3bb73b71a9f 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -1125,9 +1125,11 @@ void PathfindCellInfo::forceCleanPathFindCellInfos() void Pathfinder::forceCleanCells() { - UnicodeString pathfinderFailoverMessage = TheGameText->FETCH_OR_SUBSTITUTE("GUI:PathfindingCrashPrevented", L"A pathfinding crash was prevented, now switching to the crash fixed pathfinding."); + UnicodeString pathfinderFailoverMessage = TheGameText->FETCH_OR_SUBSTITUTE_FORMAT("GUI:PathfindingCrashPrevented", L"A pathfinding crash was prevented at frame %u, now switching to the crash fixed pathfinding.", TheGameLogic->getFrame()); TheInGameUI->message(pathfinderFailoverMessage); + printf("%ls\n", pathfinderFailoverMessage.str()); + TheAudio->addAudioEvent(&TheAudio->getMiscAudio()->m_allCheerSound); PathfindCellInfo::forceCleanPathFindCellInfos(); @@ -1727,6 +1729,13 @@ void PathfindCell::forwardInsertionSortRetailCompatible(PathfindCellList& list) UnsignedInt cellCount = 0; while (currentCell && cellCount < PATHFIND_CELLS_PER_FRAME && currentCell->m_info->m_totalCost <= m_info->m_totalCost) { + // Prevent a retail crash where a pathfindCell has an m_info with a dangling nextOpen pointer + if (currentCell->m_info->m_nextOpen && !currentCell->m_info->m_nextOpen->m_cell->m_info) + { + currentCell->m_info->m_nextOpen->m_cell = nullptr; + currentCell->m_info->m_nextOpen = nullptr; + } + cellCount++; previousCell = currentCell; currentCell = currentCell->getNextOpen(); From c4952d44583047fdb8e4fce5322bdf280b4f992a Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Sun, 25 Oct 2026 06:34:28 +0100 Subject: [PATCH 17/40] bugfix: Prevent the game from freezing during scripted camera movements in multiplayer (#3390) --- .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 6 +++--- .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 38595af3fcf..4f20d1b77c9 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1801,7 +1801,7 @@ void W3DDisplay::draw() // //PredictiveLODOptimizerClass::Optimize_LODs( 5000 ); - Bool freezeTime = TheFramePacer->isTimeFrozen() || TheFramePacer->isGameHalted(); + Bool freezeTime = TheFramePacer->isTimeFrozen(); // hack to let client spin fast in network games but still do effects at the same pace. -MDC static UnsignedInt lastFrame = ~0; @@ -1810,7 +1810,7 @@ void W3DDisplay::draw() /// @todo: I'm assuming the first view is our main 3D view. W3DView *primaryW3DView=(W3DView *)getFirstView(); - if (!freezeTime && TheScriptEngine->isTimeFast()) + if ((!freezeTime || TheFramePacer->isGameHalted()) && TheScriptEngine->isTimeFast()) { primaryW3DView->updateCameraMovements(); // Update camera motion effects. syncTime += TheW3DFrameLengthInMsec; @@ -2025,7 +2025,7 @@ void W3DDisplay::draw() if (TheScriptEngine->isTimeFrozenDebug() || TheScriptEngine->isTimeFrozenScript() || TheGameLogic->isGamePaused()) { - freezeTime = false; // We're frozen for debug or for pause, and need to continue out of the loop. + break; // We're frozen for debug or for pause, and need to break out of the loop. } } while (freezeTime && !TheTacticalView->isCameraMovementFinished()); diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 5eda9635d85..a1293592d53 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1936,12 +1936,12 @@ void W3DDisplay::draw() // //PredictiveLODOptimizerClass::Optimize_LODs( 5000 ); - Bool freezeTime = TheFramePacer->isTimeFrozen() || TheFramePacer->isGameHalted(); + Bool freezeTime = TheFramePacer->isTimeFrozen(); /// @todo: I'm assuming the first view is our main 3D view. W3DView *primaryW3DView=(W3DView *)getFirstView(); - if (!freezeTime && TheScriptEngine->isTimeFast()) + if ((!freezeTime || TheFramePacer->isGameHalted()) && TheScriptEngine->isTimeFast()) { primaryW3DView->updateCameraMovements(); // Update camera motion effects. return; @@ -2145,7 +2145,7 @@ void W3DDisplay::draw() if (TheScriptEngine->isTimeFrozenDebug() || TheScriptEngine->isTimeFrozenScript() || TheGameLogic->isGamePaused()) { - freezeTime = false; // We're frozen for debug or for pause, and need to continue out of the loop. + break; // We're frozen for debug or for pause, and need to break out of the loop. } } while (freezeTime && !TheTacticalView->isCameraMovementFinished()); From dd163b950c1aed557d6e6476151617c20256d6b8 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:15:54 +0100 Subject: [PATCH 18/40] refactor(particlesys): Cleanup retail smudge particle type identification (#3162) --- Core/GameEngine/Include/Common/GameDefines.h | 4 ++ .../Source/GameClient/System/ParticleSys.cpp | 13 ++++++- .../W3DDevice/GameClient/W3DParticleSys.cpp | 38 +++++++++---------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/Core/GameEngine/Include/Common/GameDefines.h b/Core/GameEngine/Include/Common/GameDefines.h index 67c123c0d4b..262d24be078 100644 --- a/Core/GameEngine/Include/Common/GameDefines.h +++ b/Core/GameEngine/Include/Common/GameDefines.h @@ -105,6 +105,10 @@ #define PRESERVE_RETAIL_SCRIPTED_CAMERA (1) // Retain scripted camera behavior present in retail Generals 1.08 and Zero Hour 1.04 #endif +#ifndef PRESERVE_RETAIL_PARTICLES +#define PRESERVE_RETAIL_PARTICLES (1) // Preserve original look of particles present in retail Generals 1.08 and Zero Hour 1.04 +#endif + #ifndef RETAIL_COMPATIBLE_CRC #if defined(GENERALS_ONLINE) #define RETAIL_COMPATIBLE_CRC (0) diff --git a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp index 6d5c15380c5..1ba9fb7652b 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -1225,6 +1225,15 @@ ParticleSystem::ParticleSystem( const ParticleSystemTemplate *sysTemplate, m_particleType = sysTemplate->m_particleType; m_particleTypeName = sysTemplate->m_particleTypeName; +#if PRESERVE_RETAIL_PARTICLES + // TheSuperHackers @info Hack to allow isUsingSmudge() functionality with retail smudge particles + // The retail data template for smudge particles is not correctly configured with the smudge particle type + if (m_particleType != ParticleType::SMUDGE && m_particleTypeName.startsWithNoCase("SMUDGE.")) + { + m_particleType = ParticleType::SMUDGE; + } +#endif + m_isStopped = false; // set up slave particle system, if any @@ -3082,8 +3091,8 @@ void ParticleSystemManager::update() if (sys->isUsingDrawables()) continue; - // temporary hack that checks if texture name starts with "SMUD" - if so, we can assume it's a smudge type - if (/*sys->isUsingSmudge()*/ *((DWORD *)sys->getParticleTypeName().str()) == 0x44554D53) + // Handle smudge type particles + if (sys->isUsingSmudge()) { for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext) { diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index b1b792b3ac9..c2a1b9c6fc4 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -156,31 +156,31 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) if (sys->isUsingDrawables()) continue; - //temporary hack that checks if texture name starts with "SMUD" - if so, we can assume it's a smudge type - if (/*sys->isUsingSmudge()*/ *((DWORD *)sys->getParticleTypeName().str()) == 0x44554D53) + // Handle smudge type particles + if (sys->isUsingSmudge()) { - if (drawSmudge) + if (!drawSmudge) + continue; + + for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext) { - for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext) - { - const Coord3D *pos = p->getPosition(); - Real psize = p->getSize(); + const Coord3D *pos = p->getPosition(); + Real psize = p->getSize(); - //Cull particle to edges of screen and terrain. - if (WWMath::Fabs( pos->x - bcX ) > ( beX + psize ) ) - continue; + //Cull particle to edges of screen and terrain. + if (WWMath::Fabs( pos->x - bcX ) > ( beX + psize ) ) + continue; - if (WWMath::Fabs( pos->y - bcY ) > ( beY + psize ) ) - continue; + if (WWMath::Fabs( pos->y - bcY ) > ( beY + psize ) ) + continue; - if (WWMath::Fabs( pos->z - bcZ ) > ( beZ + psize ) ) - continue; + if (WWMath::Fabs( pos->z - bcZ ) > ( beZ + psize ) ) + continue; - if (Smudge *smudge = TheSmudgeManager->findSmudge(p)) - { - // The particle is in view. Draw the smudge! - smudge->m_draw = true; - } + if (Smudge *smudge = TheSmudgeManager->findSmudge(p)) + { + // The particle is in view. Draw the smudge! + smudge->m_draw = true; } } continue; From 2e824b7a68cf74392a30c769b9549acf82b44e84 Mon Sep 17 00:00:00 2001 From: stm <14291421+stephanmeesters@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:36:19 +0200 Subject: [PATCH 19/40] unify(particlesys): Move W3DParticleSys.h to Core (#3172) --- Core/GameEngineDevice/CMakeLists.txt | 2 +- .../W3DDevice/GameClient/W3DParticleSys.h | 0 Generals/Code/GameEngineDevice/CMakeLists.txt | 2 +- .../W3DDevice/GameClient/W3DParticleSys.h | 62 ------------------- .../Code/GameEngineDevice/CMakeLists.txt | 2 +- scripts/cpp/unify_move_files.py | 1 + 6 files changed, 4 insertions(+), 65 deletions(-) rename {GeneralsMD/Code => Core}/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h (100%) delete mode 100644 Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 6added5403b..54721693913 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -54,7 +54,7 @@ set(GAMEENGINEDEVICE_SRC # Include/W3DDevice/GameClient/W3DInGameUI.h # Include/W3DDevice/GameClient/W3DMirror.h Include/W3DDevice/GameClient/W3DMouse.h -# Include/W3DDevice/GameClient/W3DParticleSys.h + Include/W3DDevice/GameClient/W3DParticleSys.h # Include/W3DDevice/GameClient/W3DPoly.h Include/W3DDevice/GameClient/W3DProfilerFrameCapture.h # Include/W3DDevice/GameClient/W3DProjectedShadow.h diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h similarity index 100% rename from GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h rename to Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index dea3ff6c9ae..41464eb4443 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -47,7 +47,7 @@ set(GAMEENGINEDEVICE_SRC Include/W3DDevice/GameClient/W3DInGameUI.h Include/W3DDevice/GameClient/W3DMirror.h # Include/W3DDevice/GameClient/W3DMouse.h - Include/W3DDevice/GameClient/W3DParticleSys.h +# Include/W3DDevice/GameClient/W3DParticleSys.h Include/W3DDevice/GameClient/W3DPoly.h Include/W3DDevice/GameClient/W3DProjectedShadow.h Include/W3DDevice/GameClient/W3DRoadBuffer.h diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h deleted file mode 100644 index 32cb43582f6..00000000000 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ /dev/null @@ -1,62 +0,0 @@ -/* -** Command & Conquer Generals(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 . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -// FILE: W3DParticleSys.h ///////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#include "GameClient/ParticleSys.h" -#include "WW3D2/pointgr.h" -#include "WW3D2/streak.h" -#include "WW3D2/rinfo.h" -#include "WWLib/bittype.h" - -//============================================================================= -/** W3D implementation of the game display which is responsible for creating - * all interaction with the screen and updating the display - */ -class W3DParticleSystemManager : public ParticleSystemManager -{ - -public: - W3DParticleSystemManager(); - virtual ~W3DParticleSystemManager() override; - - virtual void doParticles(RenderInfoClass &rinfo) override; - virtual void queueParticleRender() override; - ///< returns the number of particles shown on screen per frame - virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } - -private: - enum { MAX_POINTS_PER_GROUP = 512 }; - - PointGroupClass *m_pointGroup; ///< the point group that contains all of the particles - StreakLineClass *m_streakLine; ///< the streak class that contains all of the streaks - ShareBufferClass *m_posBuffer; ///< array of particle positions - ShareBufferClass *m_RGBABuffer; ///< array of particle color and alpha - ShareBufferClass *m_sizeBuffer; ///< array of particle sizes - ShareBufferClass *m_angleBuffer; ///< array of particle orientations - Bool m_readyToRender; ///< if true, it is OK to render -}; diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 56408e821bd..8b824957cab 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -54,7 +54,7 @@ set(GAMEENGINEDEVICE_SRC Include/W3DDevice/GameClient/W3DInGameUI.h Include/W3DDevice/GameClient/W3DMirror.h # Include/W3DDevice/GameClient/W3DMouse.h - Include/W3DDevice/GameClient/W3DParticleSys.h +# Include/W3DDevice/GameClient/W3DParticleSys.h Include/W3DDevice/GameClient/W3DPoly.h Include/W3DDevice/GameClient/W3DProjectedShadow.h # Include/W3DDevice/GameClient/W3DPropBuffer.h diff --git a/scripts/cpp/unify_move_files.py b/scripts/cpp/unify_move_files.py index 7047ccc6d6d..9fa88f72c60 100644 --- a/scripts/cpp/unify_move_files.py +++ b/scripts/cpp/unify_move_files.py @@ -606,6 +606,7 @@ def main(): #unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/INI/INIWeapon.cpp", Game.CORE, "GameEngine/Source/Common/INI/INIWeapon.cpp") #unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/INI/INIWebpageURL.cpp", Game.CORE, "GameEngine/Source/Common/INI/INIWebpageURL.cpp") + #unify_file(Game.ZEROHOUR, "GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h", Game.CORE, "GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h") #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp") return From 51cdf13a76196420f523cd12dc22db1dac0ea270 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:17:14 +0100 Subject: [PATCH 20/40] tweak(particlesys): Cleanup volume particle depth handling and add INI field to control it (#3188) Set VolParticleDepth=1..16 in ParticleSystem INI definitions to configure the depth for volume particles --- .../Include/GameClient/ParticleSys.h | 21 ++++++---- .../Source/Common/INI/INIParticleSys.cpp | 1 + .../Source/GameClient/System/ParticleSys.cpp | 41 ++++++++++++++----- .../W3DDevice/GameClient/W3DParticleSys.cpp | 5 ++- .../Source/WWVegas/WW3D2/pointgr.cpp | 3 +- .../GameLogic/ScriptEngine/ScriptEngine.cpp | 4 ++ .../GameLogic/ScriptEngine/ScriptEngine.cpp | 4 ++ 7 files changed, 57 insertions(+), 22 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index acd504f6838..677e9611abe 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -57,9 +57,11 @@ enum ParticleSystemID CPP_11(: Int) INVALID_PARTICLE_SYSTEM_ID = 0 }; -#define MAX_VOLUME_PARTICLE_DEPTH ( 16 ) -#define DEFAULT_VOLUME_PARTICLE_DEPTH ( 0 )//The Default is not to do the volume thing! -#define OPTIMUM_VOLUME_PARTICLE_DEPTH ( 6 ) +constexpr const UnsignedInt INVALID_VOLUME_PARTICLE_DEPTH = 0; //The volume depth is not initialized +constexpr const UnsignedInt DEFAULT_VOLUME_PARTICLE_DEPTH = 1; //The Default is not to do the volume thing! +constexpr const UnsignedInt MIN_VOLUME_PARTICLE_DEPTH = 2; +constexpr const UnsignedInt OPTIMUM_VOLUME_PARTICLE_DEPTH = 6; +constexpr const UnsignedInt MAX_VOLUME_PARTICLE_DEPTH = 16; // TheSuperHackers @info The X and Y angles are not necessary for particles because there are only 2 placement modes: // Billboard (always facing camera) and Ground Aligned, which overwrite any rotations on the X and Y axis by design. @@ -511,6 +513,8 @@ class ParticleSystemTemplate : public MemoryPoolObject, protected ParticleSystem public: ParticleSystemTemplate( const AsciiString &name ); + void validate(); + AsciiString getName() const { return m_name; } // This function was made const because of update modules' module data being all const. @@ -602,11 +606,12 @@ class ParticleSystem : public MemoryPoolObject, void setInitialDelay( UnsignedInt delay ) { m_delayLeft = delay; } - AsciiString getParticleTypeName() { return m_particleTypeName; } ///< return the name of the particles - Bool isUsingDrawables() { return (m_particleType == DRAWABLE) ? true : false; } - Bool isUsingStreak() { return (m_particleType == STREAK) ? true : false; } - Bool isUsingSmudge() { return (m_particleType == SMUDGE) ? true : false; } - UnsignedInt getVolumeParticleDepth() { return ( m_particleType == VOLUME_PARTICLE ) ? OPTIMUM_VOLUME_PARTICLE_DEPTH : 0; } + const AsciiString& getParticleTypeName() const { return m_particleTypeName; } ///< return the name of the particles + const Bool isUsingDrawables() const { return m_particleType == DRAWABLE; } + const Bool isUsingStreak() const { return m_particleType == STREAK; } + const Bool isUsingSmudge() const { return m_particleType == SMUDGE; } + const Bool isUsingVolumeParticles() const { return m_particleType == VOLUME_PARTICLE; } + const UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } Bool shouldBillboard() { return !m_isGroundAligned; } diff --git a/Core/GameEngine/Source/Common/INI/INIParticleSys.cpp b/Core/GameEngine/Source/Common/INI/INIParticleSys.cpp index fd0b84d5182..e72ae819664 100644 --- a/Core/GameEngine/Source/Common/INI/INIParticleSys.cpp +++ b/Core/GameEngine/Source/Common/INI/INIParticleSys.cpp @@ -54,4 +54,5 @@ void INI::parseParticleSystemDefinition( INI* ini ) // parse the ini definition ini->initFromINI( sysTemplate, sysTemplate->getFieldParse() ); + sysTemplate->validate(); } diff --git a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp index 1ba9fb7652b..7091f10e018 100644 --- a/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp +++ b/Core/GameEngine/Source/GameClient/System/ParticleSys.cpp @@ -803,7 +803,7 @@ ParticleSystemInfo::ParticleSystemInfo() m_windMotionEndAngleMax = TWO_PI; m_windMotionEndAngle = m_windMotionEndAngleMin; m_windMotionMovingToEndAngle = TRUE; - m_volumeParticleDepth = DEFAULT_VOLUME_PARTICLE_DEPTH; + m_volumeParticleDepth = INVALID_VOLUME_PARTICLE_DEPTH; } @@ -1122,7 +1122,7 @@ ParticleSystem::ParticleSystem( const ParticleSystemTemplate *sysTemplate, ///@todo: further formalize this parameter with an UnsignedInt field in the editor - m_volumeParticleDepth = DEFAULT_VOLUME_PARTICLE_DEPTH; + m_volumeParticleDepth = sysTemplate->m_volumeParticleDepth; m_driftVelocity = sysTemplate->m_driftVelocity; @@ -1225,15 +1225,6 @@ ParticleSystem::ParticleSystem( const ParticleSystemTemplate *sysTemplate, m_particleType = sysTemplate->m_particleType; m_particleTypeName = sysTemplate->m_particleTypeName; -#if PRESERVE_RETAIL_PARTICLES - // TheSuperHackers @info Hack to allow isUsingSmudge() functionality with retail smudge particles - // The retail data template for smudge particles is not correctly configured with the smudge particle type - if (m_particleType != ParticleType::SMUDGE && m_particleTypeName.startsWithNoCase("SMUDGE.")) - { - m_particleType = ParticleType::SMUDGE; - } -#endif - m_isStopped = false; // set up slave particle system, if any @@ -2750,6 +2741,9 @@ const FieldParse ParticleSystemTemplate::m_fieldParseTable[] = { "SizeRate", INI::parseGameClientRandomVariable, nullptr, offsetof( ParticleSystemTemplate, m_sizeRate ) }, { "SizeRateDamping", INI::parseGameClientRandomVariable, nullptr, offsetof( ParticleSystemTemplate, m_sizeRateDamping ) }, + // TheSuperHackers @feature Volume particle depth is now exposed for configuration + { "VolParticleDepth", INI::parseUnsignedInt, nullptr, offsetof(ParticleSystemTemplate, m_volumeParticleDepth) }, + { "Alpha1", ParticleSystemTemplate::parseRandomKeyframe, nullptr, offsetof( ParticleSystemTemplate, m_alphaKey[0] ) }, { "Alpha2", ParticleSystemTemplate::parseRandomKeyframe, nullptr, offsetof( ParticleSystemTemplate, m_alphaKey[1] ) }, { "Alpha3", ParticleSystemTemplate::parseRandomKeyframe, nullptr, offsetof( ParticleSystemTemplate, m_alphaKey[2] ) }, @@ -2919,6 +2913,31 @@ ParticleSystemTemplate::~ParticleSystemTemplate() } // ------------------------------------------------------------------------------------------------ +void ParticleSystemTemplate::validate() +{ + // TheSuperHackers @info Initialise all volume particles that lack ini configuration to the optimum depth of 6 + // In retail, volume particle depth was not configurable through ini and was hard coded to a particle depth of 6 + if (m_particleType == ParticleSystemInfo::VOLUME_PARTICLE) + { + if (m_volumeParticleDepth == INVALID_VOLUME_PARTICLE_DEPTH) + m_volumeParticleDepth = OPTIMUM_VOLUME_PARTICLE_DEPTH; + } + else + { + m_volumeParticleDepth = DEFAULT_VOLUME_PARTICLE_DEPTH; + } + +#if PRESERVE_RETAIL_PARTICLES + // TheSuperHackers @info Hack to allow isUsingSmudge() functionality with retail smudge particles + // The retail data template for smudge particles is not correctly configured with the smudge particle type + if (m_particleType != ParticleSystemInfo::SMUDGE && m_particleTypeName.startsWithNoCase("SMUDGE.")) + { + m_particleType = ParticleSystemInfo::SMUDGE; + } +#endif +} + +// ------------------------------------------------------------------------------------------------ /** If returns non-null, it is a slave system for use ... the create slaves parameter * tells *this* slave system whether or not it should create any slaves itself * automatically during its own constructor */ diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index c2a1b9c6fc4..c0a901ec7e6 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -326,9 +326,10 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) m_pointGroup->Set_Point_Frame( 0 ); //RENDER IT! - if( sys->getVolumeParticleDepth() > 1 ) + const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); + if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) { - m_pointGroup->RenderVolumeParticle( rinfo, sys->getVolumeParticleDepth() ); + m_pointGroup->RenderVolumeParticle( rinfo, volumeParticleDepth); } else m_pointGroup->Render( rinfo ); diff --git a/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp b/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp index 4bbbd6f0534..ab586c86187 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp @@ -1644,11 +1644,12 @@ void PointGroupClass::_Shutdown() * 12/03/2002 Mark Lorenzen Created. * * * *========================================================================*/ +#define DEFAULT_VOLUME_PARTICLE_DEPTH ( 1 ) #define MAX_VOLUME_PARTICLE_DEPTH ( 16 ) void PointGroupClass::RenderVolumeParticle(RenderInfoClass &rinfo, unsigned int depth ) { - if ( depth <= 1 ) //oops,wrong number + if ( depth <= DEFAULT_VOLUME_PARTICLE_DEPTH) //oops,wrong number { Render( rinfo ); return; diff --git a/Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp b/Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp index 41fa741a6c7..4e4b8f94689 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp @@ -9023,6 +9023,7 @@ static const std::string F_SIZE = "Size"; static const std::string F_STARTSIZERATE ="StartSizeRate"; static const std::string F_SIZERATE = "SizeRate"; static const std::string F_SIZERATEDAMP = "SizeRateDamping"; +static const std::string F_VOLPARTICLEDEPTH = "VolParticleDepth"; static const std::string F_ALPHA1 = "Alpha1"; static const std::string F_ALPHA2 = "Alpha2"; @@ -9185,6 +9186,9 @@ void _writeSingleParticleSystem( File *out, ParticleSystemTemplate *templ ) sprintf(buff2, FORMAT_STRING, templ->m_sizeRateDamping.getMaximumValue()); thisEntry.append(SEP_HEAD).append(F_SIZERATEDAMP).append(EQ_WITH_SPACES).append(buff1).append(SEP_SPACE).append(buff2).append(SEP_EOL); + sprintf(buff1, "%d", templ->m_volumeParticleDepth); + thisEntry.append(SEP_HEAD).append(F_VOLPARTICLEDEPTH).append(EQ_WITH_SPACES).append(buff1).append(SEP_EOL); + sprintf(buff1, FORMAT_STRING, templ->m_alphaKey[0].var.getMinimumValue()); sprintf(buff2, FORMAT_STRING, templ->m_alphaKey[0].var.getMaximumValue()); sprintf(buff3, "%d", templ->m_alphaKey[0].frame); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp index 5b2fef0caa5..e6dacec43b0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp @@ -9809,6 +9809,7 @@ static const std::string F_SIZE = "Size"; static const std::string F_STARTSIZERATE = "StartSizeRate"; static const std::string F_SIZERATE = "SizeRate"; static const std::string F_SIZERATEDAMP = "SizeRateDamping"; +static const std::string F_VOLPARTICLEDEPTH = "VolParticleDepth"; static const std::string F_ALPHA1 = "Alpha1"; static const std::string F_ALPHA2 = "Alpha2"; @@ -9971,6 +9972,9 @@ void _writeSingleParticleSystem(File* out, ParticleSystemTemplate* templ) sprintf(buff2, FORMAT_STRING, templ->m_sizeRateDamping.getMaximumValue()); thisEntry.append(SEP_HEAD).append(F_SIZERATEDAMP).append(EQ_WITH_SPACES).append(buff1).append(SEP_SPACE).append(buff2).append(SEP_EOL); + sprintf(buff1, "%d", templ->m_volumeParticleDepth); + thisEntry.append(SEP_HEAD).append(F_VOLPARTICLEDEPTH).append(EQ_WITH_SPACES).append(buff1).append(SEP_EOL); + sprintf(buff1, FORMAT_STRING, templ->m_alphaKey[0].var.getMinimumValue()); sprintf(buff2, FORMAT_STRING, templ->m_alphaKey[0].var.getMaximumValue()); sprintf(buff3, "%d", templ->m_alphaKey[0].frame); From b1be9fae0c35f0383b04d3999bc2da8278ae916d Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Sun, 6 Sep 2026 16:33:01 +0100 Subject: [PATCH 21/40] perf(particlesys): Batch same type particles to reduce cost of particle rendering by 15 to 30% (#3155) --- .../Include/GameClient/ParticleSys.h | 17 +- .../W3DDevice/GameClient/W3DParticleSys.h | 8 + .../W3DDevice/GameClient/W3DParticleSys.cpp | 261 ++++++++++++------ 3 files changed, 191 insertions(+), 95 deletions(-) diff --git a/Core/GameEngine/Include/GameClient/ParticleSys.h b/Core/GameEngine/Include/GameClient/ParticleSys.h index 677e9611abe..f11ec05cd83 100644 --- a/Core/GameEngine/Include/GameClient/ParticleSys.h +++ b/Core/GameEngine/Include/GameClient/ParticleSys.h @@ -515,7 +515,7 @@ class ParticleSystemTemplate : public MemoryPoolObject, protected ParticleSystem void validate(); - AsciiString getName() const { return m_name; } + const AsciiString& getName() const { return m_name; } // This function was made const because of update modules' module data being all const. ParticleSystem *createSlaveSystem( Bool createSlaves = TRUE ) const ; ///< if returns non-null, it is a slave system for use @@ -607,15 +607,16 @@ class ParticleSystem : public MemoryPoolObject, void setInitialDelay( UnsignedInt delay ) { m_delayLeft = delay; } const AsciiString& getParticleTypeName() const { return m_particleTypeName; } ///< return the name of the particles - const Bool isUsingDrawables() const { return m_particleType == DRAWABLE; } - const Bool isUsingStreak() const { return m_particleType == STREAK; } - const Bool isUsingSmudge() const { return m_particleType == SMUDGE; } - const Bool isUsingVolumeParticles() const { return m_particleType == VOLUME_PARTICLE; } - const UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } + Bool isUsingParticles() const { return m_particleType == PARTICLE; } + Bool isUsingDrawables() const { return m_particleType == DRAWABLE; } + Bool isUsingStreak() const { return m_particleType == STREAK; } + Bool isUsingSmudge() const { return m_particleType == SMUDGE; } + Bool isUsingVolumeParticles() const { return m_particleType == VOLUME_PARTICLE; } + UnsignedInt getVolumeParticleDepth() const { return m_volumeParticleDepth; } - Bool shouldBillboard() { return !m_isGroundAligned; } + Bool shouldBillboard() const { return !m_isGroundAligned; } - ParticleShaderType getShaderType() { return m_shaderType; } + ParticleShaderType getShaderType() const { return m_shaderType; } void setSlave( ParticleSystem *slave ); ///< set a slave system for us ParticleSystem *getSlave() { return m_slaveSystem; } diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h index 41481701474..cf17783902e 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DParticleSys.h @@ -50,13 +50,21 @@ class W3DParticleSystemManager : public ParticleSystemManager virtual Int getOnScreenParticleCount() override { return m_onScreenParticleCount; } private: + Bool finishedBatch(const ParticleSystem& system, const RefCountPtr& texture); + void initializeBatch(const ParticleSystem& system, const RefCountPtr& texture); + void flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount); + enum { MAX_POINTS_PER_GROUP = 512 }; + RefCountPtr m_batchTexture; ///< the texture used as the drawing surface for batched particle draws PointGroupClass *m_pointGroup; ///< the point group that contains all of the particles StreakLineClass *m_streakLine; ///< the streak class that contains all of the streaks ShareBufferClass *m_posBuffer; ///< array of particle positions ShareBufferClass *m_RGBABuffer; ///< array of particle color and alpha ShareBufferClass *m_sizeBuffer; ///< array of particle sizes ShareBufferClass *m_angleBuffer; ///< array of particle orientations + + ParticleSystemInfo::ParticleShaderType m_batchShaderType; Bool m_readyToRender; ///< if true, it is OK to render + Bool m_batchBillboard; }; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index c0a901ec7e6..9ceadaa2894 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -46,6 +46,9 @@ W3DParticleSystemManager::W3DParticleSystemManager() { + m_batchBillboard = true; + m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; + m_pointGroup = nullptr; m_streakLine = nullptr; m_posBuffer = nullptr; @@ -144,6 +147,9 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) TheSmudgeManager->resetDraw(); } + // Number of particles/points being rendered. + UnsignedInt pointCount = 0; + ParticleSystemManager::ParticleSystemList &particleSysList = TheParticleSystemManager->getAllParticleSystems(); for( ParticleSystemManager::ParticleSystemListIt it = particleSysList.begin(); it != particleSysList.end(); ++it) { @@ -156,6 +162,30 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) if (sys->isUsingDrawables()) continue; + // TheSuperHackers @performance Mauller 16/08/2026 Skip processing particle system if no particles are in view. + UnsignedInt particleCount = 0; + for (Particle* vp = sys->getFirstParticle(); vp; vp = vp->m_systemNext) + { + const Coord3D* pos = vp->getPosition(); + const Real psize = vp->getSize(); + + //Test if particle is at the screen or terrain edges. + if (WWMath::Fabs(pos->x - bcX) > (beX + psize) || + WWMath::Fabs(pos->y - bcY) > (beY + psize) || + WWMath::Fabs(pos->z - bcZ) > (beZ + psize)) + { + vp->setIsCulled(true); + continue; + } + + vp->setIsCulled(false); + particleCount++; + } + + // Particle system has no particles on screen + if (particleCount == 0) + continue; + // Handle smudge type particles if (sys->isUsingSmudge()) { @@ -164,17 +194,7 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext) { - const Coord3D *pos = p->getPosition(); - Real psize = p->getSize(); - - //Cull particle to edges of screen and terrain. - if (WWMath::Fabs( pos->x - bcX ) > ( beX + psize ) ) - continue; - - if (WWMath::Fabs( pos->y - bcY ) > ( beY + psize ) ) - continue; - - if (WWMath::Fabs( pos->z - bcZ ) > ( beZ + psize ) ) + if (p->isCulled()) continue; if (Smudge *smudge = TheSmudgeManager->findSmudge(p)) @@ -186,10 +206,28 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) continue; } - /// @todo lorenzen sez: declare these outside the sys loop, and put some in registers - // initialize them here still, of course + // TheSuperHackers @performance Ronin/Mauller 09/08/2026 Implement batched rendering for similar particles. + // Particles with the same properties will now be batched onto a single texture surface before being drawn. + // If a different particle type appears before the batch is filled, the previous batch will be drawn first. + RefCountPtr texture; + texture.Assign_No_Add_Ref(W3DDisplay::m_assetManager->Get_Texture(sys->getParticleTypeName().str())); + + const Bool canBatch = sys->isUsingParticles(); + const Bool batchDone = finishedBatch(*sys, texture); + if (!canBatch || batchDone) + { + flushParticleBatch(rinfo, pointCount); + } + + // setup a new particle batch texture if prior batch was flushed. + if (canBatch && m_batchTexture == nullptr) + { + initializeBatch(*sys, texture); + } + + UnsignedInt startCount = pointCount; + // build W3D particle buffer - Int count = 0; Vector3 *posArray = m_posBuffer->Get_Array(); Real *sizeArray = m_sizeBuffer->Get_Array(); Vector4 *RGBAArray = m_RGBABuffer->Get_Array(); @@ -203,53 +241,58 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) //set-up all the per-particle for (Particle *p = sys->getFirstParticle(); p; p = p->m_systemNext) { - pos = p->getPosition(); - psize = p->getSize(); - - //Cull particle to edges of screen and terrain. - if (WWMath::Fabs(pos->x - bcX) > (beX + psize)) - continue; - - if (WWMath::Fabs(pos->y - bcY) > (beY + psize)) + if (p->isCulled()) continue; - if (WWMath::Fabs(pos->z - bcZ) > (beZ + psize)) - continue; + pos = p->getPosition(); + psize = p->getSize(); m_fieldParticleCount += ( sys->getPriority() == AREA_EFFECT && sys->m_isGroundAligned != FALSE ); //@todo lorenzen sez: use pointer arithmetic for these arrays - personalities[count] = p->getPersonality(); + personalities[pointCount] = p->getPersonality(); - posArray[count].X = pos->x; - posArray[count].Y = pos->y; - posArray[count].Z = pos->z; + posArray[pointCount].X = pos->x; + posArray[pointCount].Y = pos->y; + posArray[pointCount].Z = pos->z; - sizeArray[count] = psize; + sizeArray[pointCount] = psize; color = p->getColor(); - RGBAArray[count].X = color->red; - RGBAArray[count].Y = color->green; - RGBAArray[count].Z = color->blue; - RGBAArray[count].W = p->getAlpha(); + RGBAArray[pointCount].X = color->red; + RGBAArray[pointCount].Y = color->green; + RGBAArray[pointCount].Z = color->blue; + RGBAArray[pointCount].W = p->getAlpha(); - angleArray[count] = (uint8)(p->getAngle() * 255.0f / (2.0f * PI)); + angleArray[pointCount] = (uint8)(p->getAngle() * 255.0f / (2.0f * PI)); + + if (++pointCount == MAX_POINTS_PER_GROUP) + { + if (!canBatch) + { + break; + } - if (++count == MAX_POINTS_PER_GROUP) - break; + // TheSuperHackers @info The Buffer is full mid-system so draw what we have and carry on with the SAME system. + // This prevents particles being dropped. Bank the stats first as the flush resets count to 0. + m_onScreenParticleCount += (pointCount - startCount); + flushParticleBatch(rinfo, pointCount); + initializeBatch(*sys, texture); + startCount = 0; + } } - if ( count == 0 ) + if (pointCount == startCount) + { continue; //this system has no particles to render + } - TextureClass *texture = W3DDisplay::m_assetManager->Get_Texture( sys->getParticleTypeName().str() ); - - if ( m_streakLine && sys->isUsingStreak() && (count >= 2) ) + // Handle drawing streak type particles. + if ( sys->isUsingStreak() && (pointCount >= 2) ) { m_streakLine->Reset_Line(); - m_streakLine->Set_Texture( texture ); - texture->Release_Ref();//release reference since it's held by streakline + m_streakLine->Set_Texture( texture.Peek() ); switch( sys->getShaderType() ) { case ParticleSystemInfo::ADDITIVE: @@ -268,14 +311,14 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) //UPDATE THE STREAK'S ARRAYS m_streakLine->Set_LocsWidthsColors( - count, + pointCount, m_posBuffer->Get_Array(), m_sizeBuffer->Get_Array(), m_RGBABuffer->Get_Array(), &personalities[0] ); - //WWASSERT( m_streakLine->Get_Num_Points() == count ); + //WWASSERT( m_streakLine->Get_Num_Points() == pointCount ); // This is the happy place for this! RGBAArray[0].X = 0;//eliminates the scissor edge on the trailing edge of the streak @@ -286,61 +329,51 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) //RENDER STREAK! m_streakLine->Render( rinfo ); - + m_onScreenParticleCount += (pointCount - startCount); + pointCount = startCount; } - else - { - WWASSERT( m_pointGroup ); + // Handle volumetric type particle systems. + const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); + if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) + { + m_pointGroup->Set_Texture( texture.Peek() ); + m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space - if ( m_pointGroup ) // this catches the particle and volumeparticle cases + switch( sys->getShaderType() ) { - // render all the systems' particles - m_pointGroup->Set_Texture( texture ); - texture->Release_Ref();//release reference since it's held by pointGroup - m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space - - switch( sys->getShaderType() ) - { - case ParticleSystemInfo::ADDITIVE: - m_pointGroup->Set_Shader( ShaderClass::_PresetAdditiveSpriteShader ); - break; - case ParticleSystemInfo::ALPHA: - m_pointGroup->Set_Shader( ShaderClass::_PresetAlphaSpriteShader ); - break; - case ParticleSystemInfo::ALPHA_TEST: - m_pointGroup->Set_Shader( ShaderClass::_PresetATestSpriteShader ); - break; - case ParticleSystemInfo::MULTIPLY: - m_pointGroup->Set_Shader( ShaderClass::_PresetMultiplicativeSpriteShader ); - break; - } - - /// @todo Use both QUADS and TRIS for particles - m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS ); - m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, count ); - m_pointGroup->Set_Billboard(sys->shouldBillboard()); + case ParticleSystemInfo::ADDITIVE: + m_pointGroup->Set_Shader( ShaderClass::_PresetAdditiveSpriteShader ); + break; + case ParticleSystemInfo::ALPHA: + m_pointGroup->Set_Shader( ShaderClass::_PresetAlphaSpriteShader ); + break; + case ParticleSystemInfo::ALPHA_TEST: + m_pointGroup->Set_Shader( ShaderClass::_PresetATestSpriteShader ); + break; + case ParticleSystemInfo::MULTIPLY: + m_pointGroup->Set_Shader( ShaderClass::_PresetMultiplicativeSpriteShader ); + break; + } - /// @todo Support animated texture particles - /// @todo lorenzen sez: unimplemented code wastes cpu cycles - m_pointGroup->Set_Point_Frame( 0 ); + /// @todo Use both QUADS and TRIS for particles + m_pointGroup->Set_Point_Mode( PointGroupClass::QUADS ); + m_pointGroup->Set_Arrays( m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount ); + m_pointGroup->Set_Billboard(sys->shouldBillboard()); - //RENDER IT! - const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); - if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) - { - m_pointGroup->RenderVolumeParticle( rinfo, volumeParticleDepth); - } - else - m_pointGroup->Render( rinfo ); + /// @todo Support animated texture particles + /// @todo lorenzen sez: unimplemented code wastes cpu cycles + m_pointGroup->Set_Point_Frame( 0 ); - } + m_pointGroup->RenderVolumeParticle( rinfo, volumeParticleDepth); + m_onScreenParticleCount += (pointCount - startCount); + pointCount = startCount; } /// @todo lorenzen sez: this should be debug only: //add particle count to total - m_onScreenParticleCount += count; + m_onScreenParticleCount += (pointCount - startCount); /* // draw the wind vector for this particle system on the screen @@ -362,6 +395,9 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) } + // TheSuperHackers @info Flush the last batch if one is pending. + flushParticleBatch(rinfo, pointCount); + /// @todo lorenzen sez: this should be debug only: TheParticleSystemManager->setOnScreenParticleCount(m_onScreenParticleCount); @@ -375,3 +411,54 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) ((W3DSmudgeManager *)TheSmudgeManager)->render(rinfo); } } + +Bool W3DParticleSystemManager::finishedBatch(const ParticleSystem& system, const RefCountPtr& texture) +{ + return texture.Peek() != m_batchTexture.Peek() || + system.getShaderType() != m_batchShaderType || + system.shouldBillboard() != m_batchBillboard; +} + +void W3DParticleSystemManager::initializeBatch(const ParticleSystem& system, const RefCountPtr& texture) +{ + m_batchTexture = texture; + m_batchShaderType = system.getShaderType(); + m_batchBillboard = system.shouldBillboard(); +} + +void W3DParticleSystemManager::flushParticleBatch(RenderInfoClass& rinfo, UnsignedInt& pointCount) +{ + if (pointCount > 0) + { + m_pointGroup->Set_Texture(m_batchTexture.Peek()); + + switch (m_batchShaderType) + { + case ParticleSystemInfo::ADDITIVE: + m_pointGroup->Set_Shader(ShaderClass::_PresetAdditiveSpriteShader); + break; + case ParticleSystemInfo::ALPHA: + m_pointGroup->Set_Shader(ShaderClass::_PresetAlphaSpriteShader); + break; + case ParticleSystemInfo::ALPHA_TEST: + m_pointGroup->Set_Shader(ShaderClass::_PresetATestSpriteShader); + break; + case ParticleSystemInfo::MULTIPLY: + m_pointGroup->Set_Shader(ShaderClass::_PresetMultiplicativeSpriteShader); + break; + } + + m_pointGroup->Set_Flag(PointGroupClass::TRANSFORM, true); + m_pointGroup->Set_Point_Mode(PointGroupClass::QUADS); + m_pointGroup->Set_Arrays(m_posBuffer, m_RGBABuffer, nullptr, m_sizeBuffer, m_angleBuffer, nullptr, pointCount); + m_pointGroup->Set_Billboard(m_batchBillboard); + m_pointGroup->Set_Point_Frame(0); + m_pointGroup->Render(rinfo); + + pointCount = 0; + } + + m_batchTexture.Clear(); + m_batchBillboard = false; + m_batchShaderType = ParticleSystemInfo::INVALID_SHADER; +} From 800e6a6ebe05cef9d042dd59b695aac7623f5971 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:17:31 +0100 Subject: [PATCH 22/40] bugfix(particlesys): Render lone streak type particles as regular particles (#3320) --- .../Source/W3DDevice/GameClient/W3DParticleSys.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp index 9ceadaa2894..e4823359592 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp @@ -287,6 +287,8 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) continue; //this system has no particles to render } + const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); + // Handle drawing streak type particles. if ( sys->isUsingStreak() && (pointCount >= 2) ) { @@ -332,10 +334,16 @@ void W3DParticleSystemManager::doParticles(RenderInfoClass &rinfo) m_onScreenParticleCount += (pointCount - startCount); pointCount = startCount; } - + // Handle lone streak type particles by drawing them as regular particles. + else if (sys->isUsingStreak() && (pointCount == 1)) + { + m_onScreenParticleCount += (pointCount - startCount); + initializeBatch(*sys, texture); + flushParticleBatch(rinfo, pointCount); + startCount = 0; + } // Handle volumetric type particle systems. - const UnsignedInt volumeParticleDepth = sys->getVolumeParticleDepth(); - if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) + else if( sys->isUsingVolumeParticles() && volumeParticleDepth > DEFAULT_VOLUME_PARTICLE_DEPTH ) { m_pointGroup->Set_Texture( texture.Peek() ); m_pointGroup->Set_Flag( PointGroupClass::TRANSFORM, true ); // transform to screen space From e11536cf30073e3a99aac0bf0fa11b8e7d12a16f Mon Sep 17 00:00:00 2001 From: mirelle7 <115191165+mirelle7@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:15:00 +0200 Subject: [PATCH 23/40] bugfix(texturefilter): Fix blocky shroud edges from misconfigured texture filters (#3434) --- .../Source/WWVegas/WW3D2/texturefilter.cpp | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/texturefilter.cpp b/Core/Libraries/Source/WWVegas/WW3D2/texturefilter.cpp index 777f32a0bae..ca60c8e026f 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/texturefilter.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/texturefilter.cpp @@ -139,7 +139,6 @@ void TextureFilterClass::_Init_Filters(TextureFilterMode texture_filter, Anisotr // TheSuperHackers @feature Mauller 08/03/2026 Add full support for all texture filtering modes; // None, Point, Bilinear, Trilinear, Anisotropic. - BOOL FilterSupported = false; switch (texture_filter) { default: @@ -171,15 +170,17 @@ void TextureFilterClass::_Init_Filters(TextureFilterMode texture_filter, Anisotr case TEXTURE_FILTER_BILINEAR: - FilterSupported = (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MINFLINEAR) && - (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MAGFLINEAR); - - if (FilterSupported) { + if (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MINFLINEAR) { _MinTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_LINEAR; - _MagTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_LINEAR; } else { _MinTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_POINT; + } + + if (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MAGFLINEAR) { + _MagTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_LINEAR; + } + else { _MagTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_POINT; } @@ -188,15 +189,17 @@ void TextureFilterClass::_Init_Filters(TextureFilterMode texture_filter, Anisotr case TEXTURE_FILTER_TRILINEAR: - FilterSupported = (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MINFLINEAR) && - (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MAGFLINEAR); - - if (FilterSupported) { + if (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MINFLINEAR) { _MinTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_LINEAR; - _MagTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_LINEAR; } else { _MinTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_POINT; + } + + if (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MAGFLINEAR) { + _MagTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_LINEAR; + } + else { _MagTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_POINT; } @@ -212,18 +215,28 @@ void TextureFilterClass::_Init_Filters(TextureFilterMode texture_filter, Anisotr case TEXTURE_FILTER_ANISOTROPIC: - FilterSupported = (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MAGFANISOTROPIC) && - (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MINFANISOTROPIC); - - if (FilterSupported) { - _MinTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_ANISOTROPIC; - _MagTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_ANISOTROPIC; - + if (dx8caps.TextureFilterCaps & (D3DPTFILTERCAPS_MINFANISOTROPIC | D3DPTFILTERCAPS_MAGFANISOTROPIC)) { // Set the Anisotropic filtering level for all stages _Set_Max_Anisotropy(anisotropy_level); } + + if (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MINFANISOTROPIC) { + _MinTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_ANISOTROPIC; + } + else if (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MINFLINEAR) { + _MinTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_LINEAR; + } else { _MinTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_POINT; + } + + if (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MAGFANISOTROPIC) { + _MagTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_ANISOTROPIC; + } + else if (dx8caps.TextureFilterCaps & D3DPTFILTERCAPS_MAGFLINEAR) { + _MagTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_LINEAR; + } + else { _MagTextureFilters[0][FILTER_TYPE_BEST]=D3DTEXF_POINT; } From a4541c8f829a1413892d0358cf1e416fbff0079d Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Tue, 1 Sep 2026 10:12:03 -0600 Subject: [PATCH 24/40] fix(htree): Reject invalid W3D hierarchy pivot counts (#3195) --- Core/Libraries/Source/WWVegas/WW3D2/htree.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/htree.cpp b/Core/Libraries/Source/WWVegas/WW3D2/htree.cpp index 4b95df9a8ce..53dc80ddeeb 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/htree.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/htree.cpp @@ -209,9 +209,10 @@ int HTreeClass::Load_W3D(ChunkLoadClass & cload) */ memcpy(Name,header.Name,W3D_NAME_LEN); NumPivots = header.NumPivots; - if (NumPivots > 0) { - Pivot = MSGW3DNEWARRAY("HTreeClass::Pivot") PivotClass[NumPivots]; + if (NumPivots < 1) { + return LOAD_ERROR; } + Pivot = MSGW3DNEWARRAY("HTreeClass::Pivot") PivotClass[NumPivots]; /* ** Now, read in all of the other chunks for this hierarchy. From 04170957e1117233bde5327dc116bbc139ee6e85 Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Thu, 17 Sep 2026 00:47:16 -0600 Subject: [PATCH 25/40] fix(controlbar): Prevent null dereference in ControlBar::showRallyPoint() if RallyPointMarker was not defined in INI (#3304) --- .../Source/GameClient/GUI/ControlBar/ControlBar.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp b/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp index 6d072c854ad..362d55c01a3 100644 --- a/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp +++ b/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp @@ -2742,13 +2742,15 @@ void ControlBar::showRallyPoint(const Coord3D* loc) if (m_rallyPointDrawableID == INVALID_DRAWABLE_ID) { const ThingTemplate* ttn = TheThingFactory->findTemplate("RallyPointMarker"); - marker = TheThingFactory->newDrawable(ttn); - DEBUG_ASSERTCRASH(marker, ("showRallyPoint: Unable to create rally point drawable")); - if (marker) + if (!ttn) { - marker->setDrawableStatus(DRAWABLE_STATUS_NO_SAVE); - m_rallyPointDrawableID = marker->getID(); + return; } + + marker = TheThingFactory->newDrawable(ttn); + DEBUG_ASSERTCRASH(marker, ("showRallyPoint: Unable to create rally point drawable")); + marker->setDrawableStatus(DRAWABLE_STATUS_NO_SAVE); + m_rallyPointDrawableID = marker->getID(); } else marker = TheGameClient->findDrawableByID(m_rallyPointDrawableID); From 3e5fa46e5c1ca690b290eb4b1fe70b51238649dd Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Tue, 22 Sep 2026 03:44:47 -0600 Subject: [PATCH 26/40] fix(controlbar): Prevent null dereference in ControlBar::updateSpecialPowerShortcut() (#3330) --- Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp b/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp index 362d55c01a3..eb85eaa1887 100644 --- a/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp +++ b/Core/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp @@ -3604,6 +3604,7 @@ void ControlBar::updateSpecialPowerShortcut() } else if( !hasValidShortcutButton && !m_specialPowerShortcutParent->winIsHidden() + && m_animateWindowManagerForGenShortcuts && m_animateWindowManagerForGenShortcuts->isFinished() ) { animateSpecialPowerShortcut(FALSE); From 17dec29bbe966ec0e02b5ec974659320e140708d Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:19:29 -0600 Subject: [PATCH 27/40] fix(pointgroup): Prevent null dereference in PointGroupClass::RenderVolumeParticle() when no size array is given (#3280) --- Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp b/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp index ab586c86187..6bb8ae3bcfe 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/pointgr.cpp @@ -1106,6 +1106,8 @@ void PointGroupClass::Update_Arrays( case TRIS_SIZE_NOORIENT: { + WWASSERT(point_size); + // Scale vertex offsets and add them to point locations to get vertex locations for (i = 0; i < active_points; i++) { vertex_loc[vert + 0] = point_loc[i] + @@ -1121,6 +1123,8 @@ void PointGroupClass::Update_Arrays( case TRIS_NOSIZE_ORIENT: { + WWASSERT(point_orientation); + // Scale vertex offsets and add them to point locations to get vertex locations for (i = 0; i < active_points; i++) { vertex_loc[vert + 0] = point_loc[i] + @@ -1136,6 +1140,8 @@ void PointGroupClass::Update_Arrays( case TRIS_SIZE_ORIENT: { + WWASSERT(point_size && point_orientation); + // Scale vertex offsets and add them to point locations to get vertex locations for (i = 0; i < active_points; i++) { vertex_loc[vert + 0] = point_loc[i] + @@ -1171,6 +1177,8 @@ void PointGroupClass::Update_Arrays( case QUADS_SIZE_NOORIENT: { + WWASSERT(point_size); + // Scale vertex offsets and add them to point locations to get vertex locations for (i = 0; i < active_points; i++) { vertex_loc[vert + 0] = point_loc[i] + @@ -1188,6 +1196,8 @@ void PointGroupClass::Update_Arrays( case QUADS_NOSIZE_ORIENT: { + WWASSERT(point_orientation); + // Scale vertex offsets and add them to point locations to get vertex locations for (i = 0; i < active_points; i++) { vertex_loc[vert + 0] = point_loc[i] + @@ -1205,6 +1215,8 @@ void PointGroupClass::Update_Arrays( case QUADS_SIZE_ORIENT: { + WWASSERT(point_size && point_orientation); + Matrix4x4 view; Vector4 result; if (!Billboard) { @@ -1312,6 +1324,8 @@ void PointGroupClass::Update_Arrays( case SCREEN_SIZE_NOORIENT: case SCREEN_SIZE_ORIENT: { + WWASSERT(point_size); + // Offsets need to be scaled to the current screen resolution // First find x and y scale factors (sizes in pixels need to be @@ -1790,7 +1804,8 @@ void PointGroupClass::RenderVolumeParticle(RenderInfoClass &rinfo, unsigned int // 3 times per particle when we can do it once float recipDepth = 0.1f / (float)depth; - float shiftInc = ( t * *current_size * recipDepth ); + const float pointSize = current_size ? *current_size : DefaultPointSize; + float shiftInc = t * pointSize * recipDepth; Vector3 volumeLayerShift; Vector3 cameraPosition = rinfo.Camera.Get_Position(); From 39c06b0fa939ba0c8c9ca077d4fab4e0ebd9f322 Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:20:56 -0600 Subject: [PATCH 28/40] fix(terrainvisual): Prevent null dereference in W3DTerrainVisual::loadRequire() (#3306) --- .../Source/W3DDevice/GameClient/W3DTerrainVisual.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp index ad5d6386a98..d004e808ef3 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp @@ -644,7 +644,7 @@ Bool W3DTerrainVisual::load( AsciiString filename ) #ifdef DO_UNIT_TIMINGS #pragma MESSAGE("********************* WARNING- Doing UNIT TIMINGS. ") #else - if (m_waterRenderObject) + if (m_waterRenderObject && W3DDisplay::m_3DScene) { W3DDisplay::m_3DScene->Add_Render_Object( m_waterRenderObject); m_waterRenderObject->enableWaterGrid(false); From afe0961b6a3f95996e5cd888a4e416b1dc5c8e01 Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Sat, 19 Sep 2026 02:17:46 -0600 Subject: [PATCH 29/40] fix(modeldraw): Prevent null dereference in W3DModelDraw::handleClientRecoil() (#3301) --- .../W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp index 6b9d2bd3095..ad45c4757d0 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp @@ -2495,12 +2495,13 @@ void W3DModelDraw::handleClientTurretPositioning() */ void W3DModelDraw::handleClientRecoil() { - const W3DModelDrawModuleData* d = getW3DModelDrawModuleData(); - if (!(m_curState->m_validStuff & ModelConditionInfo::BARRELS_VALID)) + if (!m_curState || !(m_curState->m_validStuff & ModelConditionInfo::BARRELS_VALID)) { return; } + const W3DModelDrawModuleData* d = getW3DModelDrawModuleData(); + // do recoil, if any for (int wslot = 0; wslot < WEAPONSLOT_COUNT; ++wslot) { From 8e1a8f842e950bd45729d9275984d0d0c25b5f5e Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Tue, 22 Sep 2026 01:08:48 -0600 Subject: [PATCH 30/40] fix(gamewindowtransitions): Prevent null dereference in GameWindowTransitionsHandler::remove() (#3329) --- .../Source/GameClient/GUI/GameWindowTransitions.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Core/GameEngine/Source/GameClient/GUI/GameWindowTransitions.cpp b/Core/GameEngine/Source/GameClient/GUI/GameWindowTransitions.cpp index 09f22640cf4..2236c4121f7 100644 --- a/Core/GameEngine/Source/GameClient/GUI/GameWindowTransitions.cpp +++ b/Core/GameEngine/Source/GameClient/GUI/GameWindowTransitions.cpp @@ -533,7 +533,13 @@ void GameWindowTransitionsHandler::reverse( AsciiString groupName ) void GameWindowTransitionsHandler::remove( AsciiString groupName, Bool skipPending ) { - TransitionGroup *g = findGroup(groupName); + TransitionGroup* g = findGroup(groupName); + + if (!g) + { + return; + } + if(m_pendingGroup == g) { if(skipPending) From d5c69b6c301560807b489d8c75ce70bd6e2919c9 Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Thu, 17 Sep 2026 08:13:31 -0600 Subject: [PATCH 31/40] fix(ini): Prevent using unitialized stack variable in INI::parseBitString8() (#3307) --- Core/GameEngine/Source/Common/INI/INI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/GameEngine/Source/Common/INI/INI.cpp b/Core/GameEngine/Source/Common/INI/INI.cpp index c03028c6d4c..1727a289bd9 100644 --- a/Core/GameEngine/Source/Common/INI/INI.cpp +++ b/Core/GameEngine/Source/Common/INI/INI.cpp @@ -916,7 +916,7 @@ void INI::parsePercentToReal( INI* ini, void * /*instance*/, void *store, const //------------------------------------------------------------------------------------------------- void INI::parseBitString8( INI* ini, void * /*instance*/, void *store, const void* userData ) { - UnsignedInt tmp; + UnsignedInt tmp = *(Byte*)store; INI::parseBitString32(ini, nullptr, &tmp, userData); if (tmp & 0xffffff00) { From ae2ad156a36c6ec844d959f7d385c08034af0531 Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Tue, 15 Sep 2026 02:36:19 -0600 Subject: [PATCH 32/40] fix(milesaudiomanager): Prevent heap-buffer-overflow read in MilesAudioManager::selectProvider() (#3281) --- .../Source/MilesAudioDevice/MilesAudioManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp b/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp index c6b2f6e9198..2e4ffaab527 100644 --- a/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp +++ b/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp @@ -1727,7 +1727,7 @@ void MilesAudioManager::selectProvider( UnsignedInt providerNdx ) { providerNdx = getProviderIndex( "Miles Fast 2D Positional Audio" ); } - success = AIL_open_3D_provider( m_provider3D[providerNdx].id ) == 0; + success = providerNdx < m_providerCount && AIL_open_3D_provider( m_provider3D[providerNdx].id ) == 0; //if (providerNdx < m_providerCount) //{ @@ -1741,7 +1741,7 @@ void MilesAudioManager::selectProvider( UnsignedInt providerNdx ) m_selectedProvider = PROVIDER_ERROR; // try to select a failsafe providerNdx = getProviderIndex( "Miles Fast 2D Positional Audio" ); - success = AIL_open_3D_provider( m_provider3D[providerNdx].id ) == 0; + success = providerNdx < m_providerCount && AIL_open_3D_provider( m_provider3D[providerNdx].id ) == 0; } if ( success ) From 6bdd838c17058bb522c843947b8620006694679c Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:50:35 -0600 Subject: [PATCH 33/40] fix(firewallhelper): Prevent stack-buffer-overflow write in FirewallHelperClass::getManglerName() (#3346) --- .../Include/GameNetwork/FirewallHelper.h | 2 +- .../Source/GameNetwork/FirewallHelper.cpp | 10 +++++++--- Core/GameEngine/Source/GameNetwork/NAT.cpp | 14 ++++++++------ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/FirewallHelper.h b/Core/GameEngine/Include/GameNetwork/FirewallHelper.h index 352b1a040bc..a1d4bade996 100644 --- a/Core/GameEngine/Include/GameNetwork/FirewallHelper.h +++ b/Core/GameEngine/Include/GameNetwork/FirewallHelper.h @@ -190,7 +190,7 @@ class FirewallHelperClass { void flagNeedToRefresh(Bool flag); - static void getManglerName(Int manglerIndex, Char *nameBuf); + static AsciiString getManglerName(Int manglerIndex); Bool sendToManglerFromPort(UnsignedInt address, UnsignedShort port, UnsignedShort packetID, Bool blitzme = FALSE); UnsignedShort getManglerResponse(UnsignedShort packetID, Int time = 0); Bool openSpareSocket(UnsignedShort port); diff --git a/Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp b/Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp index 0cb350227bc..0960ef001ad 100644 --- a/Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp +++ b/Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp @@ -597,12 +597,16 @@ Short FirewallHelperClass::getSourcePortAllocationDelta() { return m_sourcePortAllocationDelta; } -/* static */ void FirewallHelperClass::getManglerName(Int manglerIndex, Char *nameBuf) +/* static */ AsciiString FirewallHelperClass::getManglerName(Int manglerIndex) { AsciiString host; UnsignedShort port; - TheGameSpyConfig->getManglerLocation(manglerIndex, host, port); - strcpy(nameBuf, host.str()); + if (TheGameSpyConfig->getManglerLocation(manglerIndex, host, port)) + { + return host; + } + + return AsciiString::TheEmptyString; } Bool FirewallHelperClass::detectionBeginUpdate() { diff --git a/Core/GameEngine/Source/GameNetwork/NAT.cpp b/Core/GameEngine/Source/GameNetwork/NAT.cpp index fdee9824c64..99989abb296 100644 --- a/Core/GameEngine/Source/GameNetwork/NAT.cpp +++ b/Core/GameEngine/Source/GameNetwork/NAT.cpp @@ -802,13 +802,15 @@ void NAT::sendMangledSourcePort() { // next port allocation. // get the address of the mangler we need to talk to. - Char manglerName[256]; - FirewallHelperClass::getManglerName(1, manglerName); - DEBUG_LOG(("NAT::sendMangledSourcePort - about to call gethostbyname for mangler at %s", manglerName)); - struct hostent *hostInfo = gethostbyname(manglerName); + AsciiString manglerName = FirewallHelperClass::getManglerName(1); + DEBUG_LOG(("NAT::sendMangledSourcePort - about to call gethostbyname for mangler at %s", manglerName.str())); + struct hostent* hostInfo = nullptr; + if (!manglerName.isEmpty()) { + hostInfo = gethostbyname(manglerName.str()); + } if (hostInfo == nullptr) { - DEBUG_LOG(("NAT::sendMangledSourcePort - gethostbyname failed for mangler address %s", manglerName)); + DEBUG_LOG(("NAT::sendMangledSourcePort - gethostbyname failed for mangler address %s", manglerName.str())); // can't find the mangler, we're screwed so just send the source port. sendMangledPortNumberToTarget(sourcePort, targetSlot); m_sourcePorts[m_targetNodeNumber] = sourcePort; @@ -818,7 +820,7 @@ void NAT::sendMangledSourcePort() { memcpy(&m_manglerAddress, &(hostInfo->h_addr_list[0][0]), 4); m_manglerAddress = ntohl(m_manglerAddress); - DEBUG_LOG(("NAT::sendMangledSourcePort - mangler %s address is %d.%d.%d.%d", manglerName, + DEBUG_LOG(("NAT::sendMangledSourcePort - mangler %s address is %d.%d.%d.%d", manglerName.str(), PRINTF_IP_AS_4_INTS(m_manglerAddress))); DEBUG_LOG(("NAT::sendMangledSourcePort - NAT behavior = 0x%08x", fwType)); From f9bb5aec3e7b4d0ba4ffbab68268b115f6fb6524 Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Sun, 20 Sep 2026 01:42:27 -0600 Subject: [PATCH 34/40] fix(ww3d2): Copy string setter inputs before freeing potentially aliased storage (#3313) --- Core/Libraries/Source/WWVegas/WW3D2/agg_def.h | 7 ++++++- .../Libraries/Source/WWVegas/WWSaveLoad/parameter.h | 13 ++++--------- .../Libraries/Source/WWVegas/WW3D2/part_emt.cpp | 13 ++++--------- .../Libraries/Source/WWVegas/WW3D2/part_ldr.cpp | 10 ++++++---- .../Libraries/Source/WWVegas/WW3D2/part_emt.cpp | 13 ++++--------- .../Libraries/Source/WWVegas/WW3D2/part_ldr.cpp | 10 ++++++---- 6 files changed, 30 insertions(+), 36 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/agg_def.h b/Core/Libraries/Source/WWVegas/WW3D2/agg_def.h index 97228dfb374..85c994c4d69 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/agg_def.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/agg_def.h @@ -103,7 +103,12 @@ class AggregateDefClass virtual WW3DErrorType Load_W3D (ChunkLoadClass &chunk_load); virtual WW3DErrorType Save_W3D (ChunkSaveClass &chunk_save); const char * Get_Name () const { return m_pName; } - void Set_Name (const char *pname) { SAFE_FREE (m_pName); m_pName = ::_strdup (pname); } + void Set_Name (const char* pname) + { + char* name = ::_strdup(pname); + SAFE_FREE(m_pName); + m_pName = name; + } RenderObjClass * Create (); AggregateDefClass * Clone () const { return W3DNEW AggregateDefClass (*this); } diff --git a/Core/Libraries/Source/WWVegas/WWSaveLoad/parameter.h b/Core/Libraries/Source/WWVegas/WWSaveLoad/parameter.h index 18ba03ecf3a..c7838c49bad 100644 --- a/Core/Libraries/Source/WWVegas/WWSaveLoad/parameter.h +++ b/Core/Libraries/Source/WWVegas/WWSaveLoad/parameter.h @@ -208,16 +208,11 @@ ParameterClass::Get_Name () const // Set_Name ////////////////////////////////////////////////////////////////////////////////// inline void -ParameterClass::Set_Name (const char *new_name) +ParameterClass::Set_Name (const char* new_name) { - if (m_Name != nullptr) { - ::free ((void *)m_Name); - m_Name = nullptr; - } - - if (new_name != nullptr) { - m_Name = ::strdup (new_name); - } + char* name = new_name ? ::strdup(new_name) : nullptr; + ::free((void*)m_Name); + m_Name = name; } diff --git a/Generals/Code/Libraries/Source/WWVegas/WW3D2/part_emt.cpp b/Generals/Code/Libraries/Source/WWVegas/WW3D2/part_emt.cpp index 6d32d14b4ee..f0c2c018fa8 100644 --- a/Generals/Code/Libraries/Source/WWVegas/WW3D2/part_emt.cpp +++ b/Generals/Code/Libraries/Source/WWVegas/WW3D2/part_emt.cpp @@ -818,16 +818,11 @@ ParticleEmitterClass::Save (ChunkSaveClass &chunk_save) const void -ParticleEmitterClass::Set_Name (const char *pname) +ParticleEmitterClass::Set_Name (const char* pname) { - // Free the old name if necessary - if (NameString != nullptr) { - ::free (NameString); - NameString = nullptr; - } - - // Copy the provided name - NameString = ::_strdup (pname); + char* name = ::_strdup(pname); + ::free(NameString); + NameString = name; } diff --git a/Generals/Code/Libraries/Source/WWVegas/WW3D2/part_ldr.cpp b/Generals/Code/Libraries/Source/WWVegas/WW3D2/part_ldr.cpp index b39a05becdc..3f7773785f8 100644 --- a/Generals/Code/Libraries/Source/WWVegas/WW3D2/part_ldr.cpp +++ b/Generals/Code/Libraries/Source/WWVegas/WW3D2/part_ldr.cpp @@ -264,8 +264,9 @@ ParticleEmitterDefClass::Set_Creation_Volume (Vector3Randomizer *randomizer) void ParticleEmitterDefClass::Set_User_String (const char *pstring) { - SAFE_FREE (m_pUserString); - m_pUserString = ::_strdup (pstring); + char* copy = ::_strdup(pstring); + SAFE_FREE(m_pUserString); + m_pUserString = copy; } @@ -276,8 +277,9 @@ ParticleEmitterDefClass::Set_User_String (const char *pstring) void ParticleEmitterDefClass::Set_Name (const char *pname) { - SAFE_FREE (m_pName); - m_pName = ::_strdup (pname); + char* copy = ::_strdup(pname); + SAFE_FREE(m_pName); + m_pName = copy; } diff --git a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/part_emt.cpp b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/part_emt.cpp index ca8d9045bcc..4ffe27a972b 100644 --- a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/part_emt.cpp +++ b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/part_emt.cpp @@ -831,16 +831,11 @@ ParticleEmitterClass::Save (ChunkSaveClass &chunk_save) const void -ParticleEmitterClass::Set_Name (const char *pname) +ParticleEmitterClass::Set_Name (const char* pname) { - // Free the old name if necessary - if (NameString != nullptr) { - ::free (NameString); - NameString = nullptr; - } - - // Copy the provided name - NameString = ::_strdup (pname); + char* name = ::_strdup(pname); + ::free(NameString); + NameString = name; } diff --git a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/part_ldr.cpp b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/part_ldr.cpp index 4b847fd6e2e..356ea079b05 100644 --- a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/part_ldr.cpp +++ b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/part_ldr.cpp @@ -267,8 +267,9 @@ ParticleEmitterDefClass::Set_Creation_Volume (Vector3Randomizer *randomizer) void ParticleEmitterDefClass::Set_User_String (const char *pstring) { - SAFE_FREE (m_pUserString); - m_pUserString = ::_strdup (pstring); + char* copy = ::_strdup(pstring); + SAFE_FREE(m_pUserString); + m_pUserString = copy; } @@ -279,8 +280,9 @@ ParticleEmitterDefClass::Set_User_String (const char *pstring) void ParticleEmitterDefClass::Set_Name (const char *pname) { - SAFE_FREE (m_pName); - m_pName = ::_strdup (pname); + char* copy = ::_strdup(pname); + SAFE_FREE(m_pName); + m_pName = copy; } From 1b977f77f4640b8002754de07a183d0e27579bea Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Tue, 15 Sep 2026 01:05:30 -0600 Subject: [PATCH 35/40] fix(audio): Copy SoundSceneObjClass state safely (#3247) --- .../Source/WWVegas/WWAudio/SoundSceneObj.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WWAudio/SoundSceneObj.cpp b/Core/Libraries/Source/WWVegas/WWAudio/SoundSceneObj.cpp index 7a0bd0ad12a..edd132300f4 100644 --- a/Core/Libraries/Source/WWVegas/WWAudio/SoundSceneObj.cpp +++ b/Core/Libraries/Source/WWVegas/WWAudio/SoundSceneObj.cpp @@ -93,6 +93,7 @@ SoundSceneObjClass::SoundSceneObjClass () m_PhysWrapper (nullptr), m_pCallback (nullptr), m_AttachedObject (nullptr), + m_AttachedBone (-1), m_UserData (0), m_UserObj (nullptr), m_ID (SOUND_OBJ_DEFAULT_ID), @@ -110,18 +111,20 @@ SoundSceneObjClass::SoundSceneObjClass () // //////////////////////////////////////////////////////////////////////////////////////////////// SoundSceneObjClass::SoundSceneObjClass (const SoundSceneObjClass &src) - : m_Scene (nullptr), + : PersistClass(static_cast(src)), + m_Scene (src.m_Scene), m_PhysWrapper (nullptr), - m_pCallback (nullptr), + m_pCallback (src.m_pCallback), m_AttachedObject (nullptr), + m_AttachedBone (src.m_AttachedBone), m_UserData (0), m_UserObj (nullptr), m_ID (SOUND_OBJ_DEFAULT_ID), - m_RegisteredEvents (AudioCallbackClass::EVENT_NONE) + m_RegisteredEvents (src.m_RegisteredEvents) { m_ID = m_NextAvailableID ++; - (*this) = src; + REF_PTR_SET (m_AttachedObject, src.m_AttachedObject); Register_Sound_Object (this); } From 391029eb905b47f37b07c4d1eb5fd4b73ab2eba4 Mon Sep 17 00:00:00 2001 From: Jacob Lane Ledbetter <23038070+CryoTheRenegade@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:45:20 -0600 Subject: [PATCH 36/40] fix(gameinfo): Clear GameSlot IP on reset (#3311) --- Core/GameEngine/Source/GameNetwork/GameInfo.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp index d7bd46bd336..a7011f3b44d 100644 --- a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp @@ -68,6 +68,7 @@ void GameSlot::reset() m_NATBehavior = FirewallHelperClass::FIREWALL_TYPE_SIMPLE; m_lastFrameInGame = 0; m_disconnected = FALSE; + m_IP = 0; m_port = 0; m_isMuted = FALSE; m_hasSavedOriginalSetup = FALSE; From eef9e3d40ade9d509ff785ec7c1838d22aa93327 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Fri, 31 Jul 2026 22:18:26 +0100 Subject: [PATCH 37/40] bugfix(debug): Skip system-modal crash dialogs in headless runs (#3030) --- .../GameEngine/Source/Common/System/Debug.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index 2ae4d88a77f..3bd8225a0f6 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -811,16 +811,22 @@ void ReleaseCrash(const char *reason) #if defined(RTS_DEBUG) /* static */ char buff[8192]; // not so static so we can be threadsafe snprintf(buff, 8192, "Sorry, a serious error occurred. (%s)", reason); - ::MessageBox(nullptr, buff, "Technical Difficulties...", MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); + if (!(TheGlobalData && TheGlobalData->m_headless)) + { + ::MessageBox(nullptr, buff, "Technical Difficulties...", MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); + } #else // crash error messaged changed 3/6/03 BGC // ::MessageBox(nullptr, "Sorry, a serious error occurred.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR); // ::MessageBox(nullptr, "You have encountered a serious error. Serious errors can be caused by many things including viruses, overheated hardware and hardware that does not meet the minimum specifications for the game. Please visit the forums at www.generals.ea.com for suggested courses of action or consult your manual for Technical Support contact information.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR); // crash error message changed again 8/22/03 M Lorenzen... made this message box modal to the system so it will appear on top of any task-modal windows, splash-screen, etc. - ::MessageBox(nullptr, "You have encountered a serious error. Serious errors can be caused by many things including viruses, overheated hardware and hardware that does not meet the minimum specifications for the game. Please visit the forums at www.generals.ea.com for suggested courses of action or consult your manual for Technical Support contact information.", - "Technical Difficulties...", - MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); + if (!(TheGlobalData && TheGlobalData->m_headless)) + { + ::MessageBox(nullptr, "You have encountered a serious error. Serious errors can be caused by many things including viruses, overheated hardware and hardware that does not meet the minimum specifications for the game. Please visit the forums at www.generals.ea.com for suggested courses of action or consult your manual for Technical Support contact information.", + "Technical Difficulties...", + MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); + } #endif @@ -850,7 +856,10 @@ void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m) } } - ::MessageBoxW(nullptr, mesg.str(), prompt.str(), MB_OK | MB_SYSTEMMODAL | MB_ICONERROR); + if (!(TheGlobalData && TheGlobalData->m_headless)) + { + ::MessageBoxW(nullptr, mesg.str(), prompt.str(), MB_OK | MB_SYSTEMMODAL | MB_ICONERROR); + } char prevbuf[ _MAX_PATH ]; char curbuf[ _MAX_PATH ]; From 027d4aaea2d10fb8f60ad51dd8be82b1397ed828 Mon Sep 17 00:00:00 2001 From: Tin Tin Hamans <5984296+tintinhamans@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:24:50 +0200 Subject: [PATCH 38/40] fix(w3ddisplay): Fix cinematic text display string leak (#3517) --- .../Include/W3DDevice/GameClient/W3DDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 21 ++++++++++++------- .../Include/W3DDevice/GameClient/W3DDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 21 ++++++++++++------- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index aa3f0b79d2c..1701363414d 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -212,6 +212,7 @@ class W3DDisplay : public Display DisplayString *m_displayStrings[DisplayStringCount]; DisplayString *m_benchmarkDisplayString; + DisplayString *m_cinematicDisplayString; W3DDebugDisplay *m_nativeDebugDisplay; ///< W3D specific debug display interface diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 4f20d1b77c9..faf87e06c91 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -359,6 +359,8 @@ W3DDisplay::W3DDisplay() for (i = 0; i < DisplayStringCount; i++) m_displayStrings[i] = nullptr; + m_benchmarkDisplayString = nullptr; + m_cinematicDisplayString = nullptr; m_batchTexture = nullptr; m_batchMode = DRAW_IMAGE_ALPHA; @@ -394,6 +396,8 @@ W3DDisplay::~W3DDisplay() TheDisplayStringManager->freeDisplayString(m_benchmarkDisplayString); } + TheDisplayStringManager->freeDisplayString(m_cinematicDisplayString); + // delete 2D renderer if( m_2DRender ) { @@ -1952,28 +1956,29 @@ void W3DDisplay::draw() // display cinematicText over the black if( m_cinematicText != AsciiString::TheEmptyString && m_cinematicTextFrames != 0) { - DisplayString *displayString = TheDisplayStringManager->newDisplayString(); + if( m_cinematicDisplayString == nullptr ) + m_cinematicDisplayString = TheDisplayStringManager->newDisplayString(); // set word wrap if necessary Int wordWrapWidth = TheDisplay->getWidth() - 20; - displayString->setWordWrap( wordWrapWidth ); - displayString->setWordWrapCentered( TRUE ); + m_cinematicDisplayString->setWordWrap( wordWrapWidth ); + m_cinematicDisplayString->setWordWrapCentered( TRUE ); UnicodeString text; text.translate( m_cinematicText ); - displayString->setText( text ); + m_cinematicDisplayString->setText( text ); Color color = GameMakeColor( 255, 255, 255, 255 ); // white Color backColor = GameMakeColor( 0, 0, 0, 0 ); // black - displayString->setFont( m_cinematicFont ); + m_cinematicDisplayString->setFont( m_cinematicFont ); Int height = TheDisplay->getHeight() * .9; Int width; - if( displayString->getWidth() > TheDisplay->getWidth() ) + if( m_cinematicDisplayString->getWidth() > TheDisplay->getWidth() ) width = 20; else - width = ( TheDisplay->getWidth() - displayString->getWidth() ) / 2; - displayString->draw( width, height, color, backColor ); + width = ( TheDisplay->getWidth() - m_cinematicDisplayString->getWidth() ) / 2; + m_cinematicDisplayString->draw( width, height, color, backColor ); m_cinematicTextFrames--; } diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 08ee22dcdc4..543a92587a8 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -212,6 +212,7 @@ class W3DDisplay : public Display DisplayString* m_displayStrings[DisplayStringCount]; DisplayString* m_benchmarkDisplayString; + DisplayString* m_cinematicDisplayString; W3DDebugDisplay* m_nativeDebugDisplay; ///< W3D specific debug display interface diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index a1293592d53..b7910fef974 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -444,6 +444,8 @@ W3DDisplay::W3DDisplay() for (i = 0; i < DisplayStringCount; i++) m_displayStrings[i] = nullptr; + m_benchmarkDisplayString = nullptr; + m_cinematicDisplayString = nullptr; m_batchTexture = nullptr; m_batchMode = DRAW_IMAGE_ALPHA; @@ -479,6 +481,8 @@ W3DDisplay::~W3DDisplay() TheDisplayStringManager->freeDisplayString(m_benchmarkDisplayString); } + TheDisplayStringManager->freeDisplayString(m_cinematicDisplayString); + // delete 2D renderer if( m_2DRender ) { @@ -2072,28 +2076,29 @@ void W3DDisplay::draw() // display cinematicText over the black if( m_cinematicText != AsciiString::TheEmptyString && m_cinematicTextFrames != 0) { - DisplayString *displayString = TheDisplayStringManager->newDisplayString(); + if( m_cinematicDisplayString == nullptr ) + m_cinematicDisplayString = TheDisplayStringManager->newDisplayString(); // set word wrap if necessary Int wordWrapWidth = TheDisplay->getWidth() - 20; - displayString->setWordWrap( wordWrapWidth ); - displayString->setWordWrapCentered( TRUE ); + m_cinematicDisplayString->setWordWrap( wordWrapWidth ); + m_cinematicDisplayString->setWordWrapCentered( TRUE ); UnicodeString text; text.translate( m_cinematicText ); - displayString->setText( text ); + m_cinematicDisplayString->setText( text ); Color color = GameMakeColor( 255, 255, 255, 255 ); // white Color backColor = GameMakeColor( 0, 0, 0, 0 ); // black - displayString->setFont( m_cinematicFont ); + m_cinematicDisplayString->setFont( m_cinematicFont ); Int height = TheDisplay->getHeight() * .9; Int width; - if( displayString->getWidth() > TheDisplay->getWidth() ) + if( m_cinematicDisplayString->getWidth() > TheDisplay->getWidth() ) width = 20; else - width = ( TheDisplay->getWidth() - displayString->getWidth() ) / 2; - displayString->draw( width, height, color, backColor ); + width = ( TheDisplay->getWidth() - m_cinematicDisplayString->getWidth() ) / 2; + m_cinematicDisplayString->draw( width, height, color, backColor ); m_cinematicTextFrames--; } From 19761e0d8ccc2a4de531428b4dc937dc79bdccd3 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:19:58 +0200 Subject: [PATCH 39/40] fix(ingameui): Properly delete ControlBar and ReplayControl game windows on resolution change and/or shutdown (#3414) --- .../GUI/GUICallbacks/ControlBarCallback.cpp | 26 +++++- .../GameEngine/Include/GameClient/InGameUI.h | 9 ++ .../GameEngine/Source/GameClient/InGameUI.cpp | 92 ++++++++++--------- .../GameEngine/Include/GameClient/InGameUI.h | 9 ++ .../GameEngine/Source/GameClient/InGameUI.cpp | 91 ++++++++++-------- 5 files changed, 143 insertions(+), 84 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp b/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp index b9058daab1e..458bde534ae 100644 --- a/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp +++ b/Core/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp @@ -486,9 +486,29 @@ WindowMsgHandledType ControlBarSystem( GameWindow *window, UnsignedInt msg, } -extern void showReplayControls(); -extern void hideReplayControls(); -extern void toggleReplayControls(); +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +void showReplayControls() +{ + if (TheInGameUI != nullptr) + TheInGameUI->showReplayControls(); +} + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +void hideReplayControls() +{ + if (TheInGameUI != nullptr) + TheInGameUI->hideReplayControls(); +} + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +void toggleReplayControls() +{ + if (TheInGameUI != nullptr) + TheInGameUI->toggleReplayControls(); +} //------------------------------------------------------------------------------------------------- /** Force the control bar to be shown */ diff --git a/Generals/Code/GameEngine/Include/GameClient/InGameUI.h b/Generals/Code/GameEngine/Include/GameClient/InGameUI.h index 59e753c1d05..204dd76e368 100644 --- a/Generals/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/Generals/Code/GameEngine/Include/GameClient/InGameUI.h @@ -570,6 +570,10 @@ friend class Drawable; // for selection/deselection transactions void setDrawRMBScrollAnchor(Bool b) { m_drawRMBScrollAnchor = b; } void setMoveRMBScrollAnchor(Bool b) { m_moveRMBScrollAnchor = b; } + void showReplayControls(); + void hideReplayControls(); + void toggleReplayControls(); + private: virtual Int getIdleWorkerCount(); virtual Object *findIdleWorker( Object *obj); @@ -682,7 +686,9 @@ friend class Drawable; // for selection/deselection transactions void expireHint( HintType type, UnsignedInt hintIndex ); void createControlBar(); ///< create the control bar user interface + void destroyControlBar(); void createReplayControl(); ///< create the replay control window + void destroyReplayControl(); void setMouseCursor(Mouse::MouseCursor c); @@ -948,6 +954,9 @@ friend class Drawable; // for selection/deselection transactions Bool m_clientQuiet; ///< When the user clicks exit,restart, etc. this is set true ///< to skip some client sounds/fx during shutdown + GameWindow *m_controlBarWindow; + GameWindow *m_replayWindow; + // World Animation Data WorldAnimationList m_worldAnimationList; ///< the list of world animations diff --git a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp index b79818490a2..ddf469d66c1 100644 --- a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -133,8 +133,6 @@ static UnicodeString formatIncomeValue(UnsignedInt cashPerMin) /// The InGameUI singleton instance. InGameUI *TheInGameUI = nullptr; -GameWindow *m_replayWindow = nullptr; - // ------------------------------------------------------------------------------------------------ struct KindOfSelectionData { @@ -250,38 +248,6 @@ static Bool similarUnitSelection( Drawable *test, void *userData ) return FALSE; } -// ------------------------------------------------------------------------------------------------ -// ------------------------------------------------------------------------------------------------ -void showReplayControls() -{ - if (m_replayWindow) - { - Bool show = TheGameLogic->isInReplayGame(); - m_replayWindow->winHide(!show); - } -} - -// ------------------------------------------------------------------------------------------------ -// ------------------------------------------------------------------------------------------------ -void hideReplayControls() -{ - if (m_replayWindow) - { - m_replayWindow->winHide(TRUE); - } -} - -// ------------------------------------------------------------------------------------------------ -// ------------------------------------------------------------------------------------------------ -void toggleReplayControls() -{ - if (m_replayWindow) - { - Bool show = TheGameLogic->isInReplayGame() && m_replayWindow->winIsHidden(); - m_replayWindow->winHide(!show); - } -} - // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ SuperweaponInfo::SuperweaponInfo( @@ -1123,6 +1089,7 @@ InGameUI::InGameUI() } + m_controlBarWindow = nullptr; m_replayWindow = nullptr; m_messagesOn = TRUE; @@ -1275,6 +1242,9 @@ InGameUI::~InGameUI() // clear world animations clearWorldAnimations(); resetIdleWorker(); + + destroyReplayControl(); + destroyControlBar(); } //------------------------------------------------------------------------------------------------- @@ -4128,7 +4098,7 @@ void InGameUI::expireHint( HintType type, UnsignedInt hintIndex ) void InGameUI::createControlBar() { - TheWindowManager->winCreateFromScript( "ControlBar.wnd" ); + m_controlBarWindow = TheWindowManager->winCreateFromScript( "ControlBar.wnd" ); HideControlBar(); /* // hide all windows created from this layout @@ -4139,6 +4109,16 @@ void InGameUI::createControlBar() } +void InGameUI::destroyControlBar() +{ + if (m_controlBarWindow != nullptr) + { + TheWindowManager->winDestroy(m_controlBarWindow); + m_controlBarWindow = nullptr; + m_idleWorkerWin = nullptr; // This window belongs to the Control Bar and is implicitly deleted as well. + } +} + //------------------------------------------------------------------------------------------------- /** Create the replay control GUI */ //------------------------------------------------------------------------------------------------- @@ -4156,6 +4136,41 @@ void InGameUI::createReplayControl() } +void InGameUI::destroyReplayControl() +{ + if (m_replayWindow != nullptr) + { + TheWindowManager->winDestroy(m_replayWindow); + m_replayWindow = nullptr; + } +} + +void InGameUI::showReplayControls() +{ + if (m_replayWindow) + { + Bool show = TheGameLogic->isInReplayGame(); + m_replayWindow->winHide(!show); + } +} + +void InGameUI::hideReplayControls() +{ + if (m_replayWindow) + { + m_replayWindow->winHide(TRUE); + } +} + +void InGameUI::toggleReplayControls() +{ + if (m_replayWindow) + { + Bool show = TheGameLogic->isInReplayGame() && m_replayWindow->winIsHidden(); + m_replayWindow->winHide(!show); + } +} + // ------------------------------------------------------------------------------------------------ // InGameUI::playMovie // ------------------------------------------------------------------------------------------------ @@ -5894,12 +5909,7 @@ void InGameUI::resetIdleWorker() void InGameUI::recreateControlBar() { - GameWindow *win = TheWindowManager->winGetWindowFromId(nullptr, TheNameKeyGenerator->nameToKey("ControlBar.wnd")); - if(win) - deleteInstance(win); - - m_idleWorkerWin = nullptr; - + destroyControlBar(); createControlBar(); delete TheControlBar; diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h index d5798a48ec7..7e39fd7e8f2 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h @@ -602,6 +602,10 @@ class InGameUI : public SubsystemInterface, public Snapshot void setDrawRMBScrollAnchor(Bool b) { m_drawRMBScrollAnchor = b; } void setMoveRMBScrollAnchor(Bool b) { m_moveRMBScrollAnchor = b; } + void showReplayControls(); + void hideReplayControls(); + void toggleReplayControls(); + private: virtual Int getIdleWorkerCount(); virtual Object* findIdleWorker(Object* obj); @@ -760,7 +764,9 @@ class InGameUI : public SubsystemInterface, public Snapshot void expireHint(HintType type, UnsignedInt hintIndex); void createControlBar(); ///< create the control bar user interface + void destroyControlBar(); void createReplayControl(); ///< create the replay control window + void destroyReplayControl(); void setMouseCursor(Mouse::MouseCursor c); @@ -1078,6 +1084,9 @@ class InGameUI : public SubsystemInterface, public Snapshot Int totalHeight = 0; bool isUpdating = false; + GameWindow *m_controlBarWindow; + GameWindow *m_replayWindow; + // World Animation Data WorldAnimationList m_worldAnimationList; ///< the list of world animations diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index b833f4c59d1..f9d4c1f8a51 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -144,8 +144,6 @@ static UnicodeString formatIncomeValue(UnsignedInt cashPerMin) /// The InGameUI singleton instance. InGameUI* TheInGameUI = nullptr; -GameWindow* m_replayWindow = nullptr; - // ------------------------------------------------------------------------------------------------ struct KindOfSelectionData { @@ -261,38 +259,6 @@ static Bool similarUnitSelection(Drawable* test, void* userData) return FALSE; } -// ------------------------------------------------------------------------------------------------ -// ------------------------------------------------------------------------------------------------ -void showReplayControls() -{ - if (m_replayWindow) - { - Bool show = TheGameLogic->isInReplayGame(); - m_replayWindow->winHide(!show); - } -} - -// ------------------------------------------------------------------------------------------------ -// ------------------------------------------------------------------------------------------------ -void hideReplayControls() -{ - if (m_replayWindow) - { - m_replayWindow->winHide(TRUE); - } -} - -// ------------------------------------------------------------------------------------------------ -// ------------------------------------------------------------------------------------------------ -void toggleReplayControls() -{ - if (m_replayWindow) - { - Bool show = TheGameLogic->isInReplayGame() && m_replayWindow->winIsHidden(); - m_replayWindow->winHide(!show); - } -} - // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ SuperweaponInfo::SuperweaponInfo( @@ -1161,6 +1127,7 @@ InGameUI::InGameUI() #endif } + m_controlBarWindow = nullptr; m_replayWindow = nullptr; m_messagesOn = TRUE; @@ -1337,6 +1304,9 @@ InGameUI::~InGameUI() // clean up obs overlay cleanupObserverOverlay(); + + destroyReplayControl(); + destroyControlBar(); } //------------------------------------------------------------------------------------------------- @@ -4326,7 +4296,7 @@ void InGameUI::expireHint(HintType type, UnsignedInt hintIndex) void InGameUI::createControlBar() { - TheWindowManager->winCreateFromScript("ControlBar.wnd"); + m_controlBarWindow = TheWindowManager->winCreateFromScript("ControlBar.wnd"); HideControlBar(); /* // hide all windows created from this layout @@ -4337,6 +4307,16 @@ void InGameUI::createControlBar() } +void InGameUI::destroyControlBar() +{ + if (m_controlBarWindow != nullptr) + { + TheWindowManager->winDestroy(m_controlBarWindow); + m_controlBarWindow = nullptr; + m_idleWorkerWin = nullptr; // This window belongs to the Control Bar and is implicitly deleted as well. + } +} + //------------------------------------------------------------------------------------------------- /** Create the replay control GUI */ //------------------------------------------------------------------------------------------------- @@ -4354,6 +4334,41 @@ void InGameUI::createReplayControl() } +void InGameUI::destroyReplayControl() +{ + if (m_replayWindow != nullptr) + { + TheWindowManager->winDestroy(m_replayWindow); + m_replayWindow = nullptr; + } +} + +void InGameUI::showReplayControls() +{ + if (m_replayWindow) + { + Bool show = TheGameLogic->isInReplayGame(); + m_replayWindow->winHide(!show); + } +} + +void InGameUI::hideReplayControls() +{ + if (m_replayWindow) + { + m_replayWindow->winHide(TRUE); + } +} + +void InGameUI::toggleReplayControls() +{ + if (m_replayWindow) + { + Bool show = TheGameLogic->isInReplayGame() && m_replayWindow->winIsHidden(); + m_replayWindow->winHide(!show); + } +} + // ------------------------------------------------------------------------------------------------ // InGameUI::playMovie // ------------------------------------------------------------------------------------------------ @@ -6202,11 +6217,7 @@ void InGameUI::resetIdleWorker() void InGameUI::recreateControlBar() { - GameWindow* win = TheWindowManager->winGetWindowFromId(nullptr, TheNameKeyGenerator->nameToKey("ControlBar.wnd")); - deleteInstance(win); - - m_idleWorkerWin = nullptr; - + destroyControlBar(); createControlBar(); delete TheControlBar; From fa57b67463591959224b9710a65051ef8fe94448 Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Wed, 16 Sep 2026 08:55:20 +0200 Subject: [PATCH 40/40] bugfix(pathfinder): Prevent crash in Pathfinder::processHierarchicalCell (#3296) --- .../Source/GameLogic/AI/AIPathfind.cpp | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 3bb73b71a9f..f6270786838 100644 --- a/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/Core/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -7583,24 +7583,16 @@ void Pathfinder::processHierarchicalCell( const ICoord2D &scanCell, const ICoord return; } - newCell->allocateInfo(scanCell); -#if RETAIL_COMPATIBLE_PATHFINDING - if (!s_useFixedPathfinding) - { - if (!newCell->getClosed() && !newCell->getOpen()) { - newCell->putOnClosedList(m_closedList); - } - } - else +#if RTS_GENERALS && RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @bugfix Caball009 14/09/2026 Check newCell->m_info before accessing it to prevent a possible crash. + // The Zero Hour / non-retail compatible pathfinding code performs this check earlier in the function. + if (newCell->allocateInfo(scanCell) && !newCell->getOpen() && !newCell->getClosed()) #endif { - if (newCell->hasInfo() && !newCell->getClosed() && !newCell->getOpen()) { - newCell->putOnClosedList(m_closedList); - } + newCell->putOnClosedList(m_closedList); } - adjNewCell->allocateInfo(adjacentCell); - if( adjNewCell->hasInfo() ) + if (adjNewCell->allocateInfo(adjacentCell)) { cellCount++;