From 1e7ee6aec347179693592d1d795dc71f8c34bd83 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:15 -0500 Subject: [PATCH 01/26] Add stb library dependency via FetchContent --- CMakeLists.txt | 1 + cmake/stb.cmake | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 cmake/stb.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 28ce09560e..39062e3fbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,7 @@ endif() include(cmake/config.cmake) include(cmake/gamespy.cmake) include(cmake/lzhl.cmake) +include(cmake/stb.cmake) if (IS_VS6_BUILD) # The original max sdk does not compile against a modern compiler. diff --git a/cmake/stb.cmake b/cmake/stb.cmake new file mode 100644 index 0000000000..957c7d235d --- /dev/null +++ b/cmake/stb.cmake @@ -0,0 +1,17 @@ +# TheSuperHackers @bobtista 02/11/2025 +# STB single-file public domain libraries for image encoding +# https://github.com/nothings/stb + +FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG master # Could pin to specific commit for stability + GIT_SHALLOW TRUE +) + +FetchContent_MakeAvailable(stb) + +# Create interface library for stb headers +add_library(stb INTERFACE) +target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) + From 3126d6f6cc65c20955149e39ca61cbc478693fd0 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:22 -0500 Subject: [PATCH 02/26] Add MSG_META_TAKE_SCREENSHOT_COMPRESSED message and F11 keybinding Add takeScreenShotCompressed() to Display interface --- .../Code/GameEngine/Include/Common/MessageStream.h | 1 + Generals/Code/GameEngine/Include/GameClient/Display.h | 1 + .../Code/GameEngine/Source/Common/MessageStream.cpp | 1 + .../Source/GameClient/MessageStream/CommandXlat.cpp | 8 ++++++++ .../Source/GameClient/MessageStream/MetaEvent.cpp | 11 +++++++++++ .../Code/GameEngine/Include/Common/MessageStream.h | 1 + .../Code/GameEngine/Include/GameClient/Display.h | 1 + .../Code/GameEngine/Source/Common/MessageStream.cpp | 1 + .../Source/GameClient/MessageStream/CommandXlat.cpp | 8 ++++++++ .../Source/GameClient/MessageStream/MetaEvent.cpp | 11 +++++++++++ 10 files changed, 44 insertions(+) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index a8b937ef53..9399cf1781 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -258,6 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take screenshot + MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< TheSuperHackers @bobtista take compressed screenshot (JPG/PNG) without stalling MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/Generals/Code/GameEngine/Include/GameClient/Display.h b/Generals/Code/GameEngine/Include/GameClient/Display.h index 4443069ea8..bbf7da0b62 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -167,6 +167,7 @@ class Display : public SubsystemInterface virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset virtual void takeScreenShot() = 0; ///< saves screenshot to a file + virtual void takeScreenShotCompressed() = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp index 92d65b9897..52a414d1e9 100644 --- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,6 +364,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION) CASE_LABEL(MSG_META_END_PREFER_SELECTION) CASE_LABEL(MSG_META_TAKE_SCREENSHOT) + CASE_LABEL(MSG_META_TAKE_SCREENSHOT_COMPRESSED) CASE_LABEL(MSG_META_ALL_CHEER) CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE) CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT) diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 13103412d0..cae9fa7f61 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3419,6 +3419,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage { if (TheDisplay) TheDisplay->takeScreenShot(); + break; + } + + // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling + case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: + { + if (TheDisplay) + TheDisplay->takeScreenShotCompressed(); disp = DESTROY_MESSAGE; break; } diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index c1dbae122e..c9102fd1f5 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -163,6 +163,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, + { "TAKE_SCREENSHOT_COMPRESSED", GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -813,6 +814,16 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) map->m_usableIn = COMMANDUSABLE_GAME; } } + { + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED); + if (map->m_key == MK_NONE) + { + map->m_key = MK_F11; + map->m_transition = DOWN; + map->m_modState = NONE; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } #if defined(RTS_DEBUG) { diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 100e6dd682..148f217e0f 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -258,6 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take screenshot + MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< TheSuperHackers @bobtista take compressed screenshot (JPG/PNG) without stalling MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h index 4b02b8817e..c845690bbe 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h @@ -167,6 +167,7 @@ class Display : public SubsystemInterface virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset virtual void takeScreenShot() = 0; ///< saves screenshot to a file + virtual void takeScreenShotCompressed() = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp index 500179a4e9..a42809add4 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,6 +364,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION) CASE_LABEL(MSG_META_END_PREFER_SELECTION) CASE_LABEL(MSG_META_TAKE_SCREENSHOT) + CASE_LABEL(MSG_META_TAKE_SCREENSHOT_COMPRESSED) CASE_LABEL(MSG_META_ALL_CHEER) CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE) CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index bf06bd0a85..1578fad04b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3752,6 +3752,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage { if (TheDisplay) TheDisplay->takeScreenShot(); + break; + } + + // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling + case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: + { + if (TheDisplay) + TheDisplay->takeScreenShotCompressed(); disp = DESTROY_MESSAGE; break; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 9d9df747c0..92d405b250 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -171,6 +171,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, + { "TAKE_SCREENSHOT_COMPRESSED", GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -871,6 +872,16 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) map->m_usableIn = COMMANDUSABLE_GAME; } } + { + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED); + if (map->m_key == MK_NONE) + { + map->m_key = MK_F11; + map->m_transition = DOWN; + map->m_modState = NONE; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } #if defined(RTS_DEBUG) { From ab2d333782c480c70dc44c819ce40e47c7bb9757 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:34 -0500 Subject: [PATCH 03/26] Implement threaded JPEG screenshot for GeneralsMD Implement threaded JPEG screenshot for Generals Link stb library to GameEngineDevice targets Remove excessive comments from screenshot implementation --- .../GameEngine/Include/Common/MessageStream.h | 2 +- .../GameEngine/Include/GameClient/Display.h | 2 +- .../GameClient/MessageStream/CommandXlat.cpp | 1 - Generals/Code/GameEngineDevice/CMakeLists.txt | 1 + .../Include/W3DDevice/GameClient/W3DDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 103 ++++++++++++++++++ .../GameEngine/Include/Common/MessageStream.h | 2 +- .../GameEngine/Include/GameClient/Display.h | 2 +- .../GameClient/MessageStream/CommandXlat.cpp | 1 - .../Code/GameEngineDevice/CMakeLists.txt | 1 + .../Include/W3DDevice/GameClient/W3DDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 103 ++++++++++++++++++ 12 files changed, 214 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 9399cf1781..920a855dc1 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -258,7 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take screenshot - MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< TheSuperHackers @bobtista take compressed screenshot (JPG/PNG) without stalling + MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< take compressed screenshot without stalling MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/Generals/Code/GameEngine/Include/GameClient/Display.h b/Generals/Code/GameEngine/Include/GameClient/Display.h index bbf7da0b62..65d398976c 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -167,7 +167,7 @@ class Display : public SubsystemInterface virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset virtual void takeScreenShot() = 0; ///< saves screenshot to a file - virtual void takeScreenShotCompressed() = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling + virtual void takeScreenShotCompressed() = 0; ///< saves compressed screenshot without stalling virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index cae9fa7f61..917a91f14d 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3422,7 +3422,6 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage break; } - // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: { if (TheDisplay) diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 59678dbfcb..ac8ee37aae 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -200,6 +200,7 @@ target_link_libraries(g_gameenginedevice PRIVATE corei_gameenginedevice_private gi_always gi_main + stb ) target_link_libraries(g_gameenginedevice PUBLIC diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 34b0c54a20..20815d7318 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -121,6 +121,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer() ; ///< Create a video buffer that can be used for this display virtual void takeScreenShot(); //save screenshot to file + virtual void takeScreenShotCompressed(); //save compressed screenshot to file (JPG/PNG) without stalling virtual void toggleMovieCapture(); //enable AVI or frame capture mode. virtual void toggleLetterBox(); /// #include #include +#include +#include + +// TheSuperHackers @bobtista 02/11/2025 STB for image encoding +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include // USER INCLUDES ////////////////////////////////////////////////////////////// #include "Common/FramePacer.h" @@ -3016,6 +3022,103 @@ void W3DDisplay::takeScreenShot() TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } +/// TheSuperHackers @bobtista 02/11/2025 Save compressed screenshot (JPEG/PNG) to file without stalling the game +/// This implementation captures the frame buffer on the main thread, then spawns a background thread +/// to compress and save the image, allowing the game to continue running smoothly. +void W3DDisplay::takeScreenShotCompressed(void) +{ + // TheSuperHackers @bobtista 02/11/2025 Find next available filename + char leafname[256]; + char pathname[1024]; + static int frame_number = 1; + + Bool done = false; + while (!done) { + sprintf(leafname, "sshot%.3d.jpg", frame_number++); + strcpy(pathname, TheGlobalData->getPath_UserData().str()); + strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + if (_access(pathname, 0) == -1) + done = true; + } + + // TheSuperHackers @bobtista 02/11/2025 Get the back buffer and create a copy + SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); + SurfaceClass::SurfaceDescription surfaceDesc; + surface->Get_Description(surfaceDesc); + + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + + surface->Release_Ref(); + surface = NULL; + + struct Rect + { + int Pitch; + void* pBits; + } lrect; + + lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); + if (lrect.pBits == NULL) + { + surfaceCopy->Release_Ref(); + return; + } + + unsigned int x, y, index, index2; + unsigned int width = surfaceDesc.Width; + unsigned int height = surfaceDesc.Height; + + // TheSuperHackers @bobtista 02/11/2025 Allocate buffer for RGB image data + // Using shared_ptr for automatic cleanup in the background thread + std::shared_ptr imageData(new unsigned char[3 * width * height], + std::default_delete()); + unsigned char* image = imageData.get(); + + // TheSuperHackers @bobtista 02/11/2025 Copy and convert BGRA to RGB + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + index = 3 * (x + y * width); + index2 = y * lrect.Pitch + 4 * x; + + image[index] = *((unsigned char*)lrect.pBits + index2 + 2); // R + image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); // G + image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); // B + } + } + + surfaceCopy->Unlock(); + surfaceCopy->Release_Ref(); + surfaceCopy = NULL; + + // TheSuperHackers @bobtista 02/11/2025 Make a copy of the pathname for the background thread + std::string pathnameCopy(pathname); + std::string leafnameCopy(leafname); + + // TheSuperHackers @bobtista 02/11/2025 Spawn background thread to compress and save the image + // This allows the game to continue running without freezing + std::thread([imageData, width, height, pathnameCopy, leafnameCopy]() { + // TheSuperHackers @bobtista 02/11/2025 Write JPEG with quality 90 (range: 1-100) + // stbi_write_jpg expects image data with Y-axis going down, which matches our data + int result = stbi_write_jpg(pathnameCopy.c_str(), width, height, 3, imageData.get(), 90); + + if (!result) { + // TheSuperHackers @bobtista 02/11/2025 Log error if write failed + // Note: Can't show UI message from background thread + OutputDebugStringA("Failed to write screenshot JPEG\n"); + } + + // TheSuperHackers @bobtista 02/11/2025 imageData will be automatically cleaned up when shared_ptr goes out of scope + }).detach(); // TheSuperHackers @bobtista 02/11/2025 Detach thread to run independently + + // TheSuperHackers @bobtista 02/11/2025 Show message to user immediately (file is being saved in background) + UnicodeString ufileName; + ufileName.translate(leafnameCopy.c_str()); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); +} + /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture() { diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 148f217e0f..863bce6219 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -258,7 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take screenshot - MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< TheSuperHackers @bobtista take compressed screenshot (JPG/PNG) without stalling + MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< take compressed screenshot without stalling MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h index c845690bbe..3022800652 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h @@ -167,7 +167,7 @@ class Display : public SubsystemInterface virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset virtual void takeScreenShot() = 0; ///< saves screenshot to a file - virtual void takeScreenShotCompressed() = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling + virtual void takeScreenShotCompressed() = 0; ///< saves compressed screenshot without stalling virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 1578fad04b..05756ff13f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3755,7 +3755,6 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage break; } - // TheSuperHackers @bobtista 02/11/2025 Compressed screenshot (JPG/PNG) without stalling case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: { if (TheDisplay) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 6ed51d08bb..062e4e60b8 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -213,6 +213,7 @@ target_link_libraries(z_gameenginedevice PRIVATE corei_gameenginedevice_private zi_always zi_main + stb ) target_link_libraries(z_gameenginedevice PUBLIC diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 18a10ef863..65628e1b0d 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -121,6 +121,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer() ; ///< Create a video buffer that can be used for this display virtual void takeScreenShot(); //save screenshot to file + virtual void takeScreenShotCompressed(); //save compressed screenshot to file (JPG/PNG) without stalling virtual void toggleMovieCapture(); //enable AVI or frame capture mode. virtual void toggleLetterBox(); /// #include #include +#include +#include + +// TheSuperHackers @bobtista 02/11/2025 STB for image encoding +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include // USER INCLUDES ////////////////////////////////////////////////////////////// #include "Common/FramePacer.h" @@ -3128,6 +3134,103 @@ void W3DDisplay::takeScreenShot() TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } +/// TheSuperHackers @bobtista 02/11/2025 Save compressed screenshot (JPEG/PNG) to file without stalling the game +/// This implementation captures the frame buffer on the main thread, then spawns a background thread +/// to compress and save the image, allowing the game to continue running smoothly. +void W3DDisplay::takeScreenShotCompressed(void) +{ + // TheSuperHackers @bobtista 02/11/2025 Find next available filename + char leafname[256]; + char pathname[1024]; + static int frame_number = 1; + + Bool done = false; + while (!done) { + sprintf(leafname, "sshot%.3d.jpg", frame_number++); + strcpy(pathname, TheGlobalData->getPath_UserData().str()); + strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + if (_access(pathname, 0) == -1) + done = true; + } + + // TheSuperHackers @bobtista 02/11/2025 Get the back buffer and create a copy + SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); + SurfaceClass::SurfaceDescription surfaceDesc; + surface->Get_Description(surfaceDesc); + + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + + surface->Release_Ref(); + surface = NULL; + + struct Rect + { + int Pitch; + void* pBits; + } lrect; + + lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); + if (lrect.pBits == NULL) + { + surfaceCopy->Release_Ref(); + return; + } + + unsigned int x, y, index, index2; + unsigned int width = surfaceDesc.Width; + unsigned int height = surfaceDesc.Height; + + // TheSuperHackers @bobtista 02/11/2025 Allocate buffer for RGB image data + // Using shared_ptr for automatic cleanup in the background thread + std::shared_ptr imageData(new unsigned char[3 * width * height], + std::default_delete()); + unsigned char* image = imageData.get(); + + // TheSuperHackers @bobtista 02/11/2025 Copy and convert BGRA to RGB + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + index = 3 * (x + y * width); + index2 = y * lrect.Pitch + 4 * x; + + image[index] = *((unsigned char*)lrect.pBits + index2 + 2); // R + image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); // G + image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); // B + } + } + + surfaceCopy->Unlock(); + surfaceCopy->Release_Ref(); + surfaceCopy = NULL; + + // TheSuperHackers @bobtista 02/11/2025 Make a copy of the pathname for the background thread + std::string pathnameCopy(pathname); + std::string leafnameCopy(leafname); + + // TheSuperHackers @bobtista 02/11/2025 Spawn background thread to compress and save the image + // This allows the game to continue running without freezing + std::thread([imageData, width, height, pathnameCopy, leafnameCopy]() { + // TheSuperHackers @bobtista 02/11/2025 Write JPEG with quality 90 (range: 1-100) + // stbi_write_jpg expects image data with Y-axis going down, which matches our data + int result = stbi_write_jpg(pathnameCopy.c_str(), width, height, 3, imageData.get(), 90); + + if (!result) { + // TheSuperHackers @bobtista 02/11/2025 Log error if write failed + // Note: Can't show UI message from background thread + OutputDebugStringA("Failed to write screenshot JPEG\n"); + } + + // TheSuperHackers @bobtista 02/11/2025 imageData will be automatically cleaned up when shared_ptr goes out of scope + }).detach(); // TheSuperHackers @bobtista 02/11/2025 Detach thread to run independently + + // TheSuperHackers @bobtista 02/11/2025 Show message to user immediately (file is being saved in background) + UnicodeString ufileName; + ufileName.translate(leafnameCopy.c_str()); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); +} + /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture() { From 53fd0d593264d6d047eea9eb7a288f4636ba3312 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 23:29:47 -0500 Subject: [PATCH 04/26] Use Win32 CreateThread for VC6 compatibility and add GUIEditDisplay stub --- .../W3DDevice/GameClient/W3DDisplay.cpp | 82 ++++++++++--------- .../Tools/GUIEdit/Include/GUIEditDisplay.h | 1 + .../W3DDevice/GameClient/W3DDisplay.cpp | 82 ++++++++++--------- .../Tools/GUIEdit/Include/GUIEditDisplay.h | 1 + 4 files changed, 88 insertions(+), 78 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index e4e1845a23..340dcda7e5 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -39,10 +39,7 @@ static void drawFramerateBar(); #include #include #include -#include -#include -// TheSuperHackers @bobtista 02/11/2025 STB for image encoding #define STB_IMAGE_WRITE_IMPLEMENTATION #include @@ -3022,12 +3019,33 @@ void W3DDisplay::takeScreenShot() TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } -/// TheSuperHackers @bobtista 02/11/2025 Save compressed screenshot (JPEG/PNG) to file without stalling the game -/// This implementation captures the frame buffer on the main thread, then spawns a background thread -/// to compress and save the image, allowing the game to continue running smoothly. +struct ScreenshotThreadData +{ + unsigned char* imageData; + unsigned int width; + unsigned int height; + char pathname[1024]; + char leafname[256]; +}; + +static DWORD WINAPI screenshotThreadFunc(LPVOID param) +{ + ScreenshotThreadData* data = (ScreenshotThreadData*)param; + + int result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, 90); + + if (!result) { + OutputDebugStringA("Failed to write screenshot JPEG\n"); + } + + delete [] data->imageData; + delete data; + + return 0; +} + void W3DDisplay::takeScreenShotCompressed(void) { - // TheSuperHackers @bobtista 02/11/2025 Find next available filename char leafname[256]; char pathname[1024]; static int frame_number = 1; @@ -3041,7 +3059,6 @@ void W3DDisplay::takeScreenShotCompressed(void) done = true; } - // TheSuperHackers @bobtista 02/11/2025 Get the back buffer and create a copy SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); @@ -3069,13 +3086,8 @@ void W3DDisplay::takeScreenShotCompressed(void) unsigned int width = surfaceDesc.Width; unsigned int height = surfaceDesc.Height; - // TheSuperHackers @bobtista 02/11/2025 Allocate buffer for RGB image data - // Using shared_ptr for automatic cleanup in the background thread - std::shared_ptr imageData(new unsigned char[3 * width * height], - std::default_delete()); - unsigned char* image = imageData.get(); + unsigned char* image = new unsigned char[3 * width * height]; - // TheSuperHackers @bobtista 02/11/2025 Copy and convert BGRA to RGB for (y = 0; y < height; y++) { for (x = 0; x < width; x++) @@ -3083,9 +3095,9 @@ void W3DDisplay::takeScreenShotCompressed(void) index = 3 * (x + y * width); index2 = y * lrect.Pitch + 4 * x; - image[index] = *((unsigned char*)lrect.pBits + index2 + 2); // R - image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); // G - image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); // B + image[index] = *((unsigned char*)lrect.pBits + index2 + 2); + image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); + image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); } } @@ -3093,29 +3105,21 @@ void W3DDisplay::takeScreenShotCompressed(void) surfaceCopy->Release_Ref(); surfaceCopy = NULL; - // TheSuperHackers @bobtista 02/11/2025 Make a copy of the pathname for the background thread - std::string pathnameCopy(pathname); - std::string leafnameCopy(leafname); - - // TheSuperHackers @bobtista 02/11/2025 Spawn background thread to compress and save the image - // This allows the game to continue running without freezing - std::thread([imageData, width, height, pathnameCopy, leafnameCopy]() { - // TheSuperHackers @bobtista 02/11/2025 Write JPEG with quality 90 (range: 1-100) - // stbi_write_jpg expects image data with Y-axis going down, which matches our data - int result = stbi_write_jpg(pathnameCopy.c_str(), width, height, 3, imageData.get(), 90); - - if (!result) { - // TheSuperHackers @bobtista 02/11/2025 Log error if write failed - // Note: Can't show UI message from background thread - OutputDebugStringA("Failed to write screenshot JPEG\n"); - } - - // TheSuperHackers @bobtista 02/11/2025 imageData will be automatically cleaned up when shared_ptr goes out of scope - }).detach(); // TheSuperHackers @bobtista 02/11/2025 Detach thread to run independently - - // TheSuperHackers @bobtista 02/11/2025 Show message to user immediately (file is being saved in background) + ScreenshotThreadData* threadData = new ScreenshotThreadData(); + threadData->imageData = image; + threadData->width = width; + threadData->height = height; + strcpy(threadData->pathname, pathname); + strcpy(threadData->leafname, leafname); + + DWORD threadId; + HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + if (hThread) { + CloseHandle(hThread); + } + UnicodeString ufileName; - ufileName.translate(leafnameCopy.c_str()); + ufileName.translate(leafname); TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } diff --git a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index 24086a3e22..1b261394f8 100644 --- a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -102,6 +102,7 @@ class GUIEditDisplay : public Display virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY, Int endX, Int endY ) { } virtual void takeScreenShot(void){ } + virtual void takeScreenShotCompressed(void){ } virtual void toggleMovieCapture(void) {} // methods that we need to stub diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 0871e74aff..0b3570724f 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -39,10 +39,7 @@ static void drawFramerateBar(); #include #include #include -#include -#include -// TheSuperHackers @bobtista 02/11/2025 STB for image encoding #define STB_IMAGE_WRITE_IMPLEMENTATION #include @@ -3134,12 +3131,33 @@ void W3DDisplay::takeScreenShot() TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } -/// TheSuperHackers @bobtista 02/11/2025 Save compressed screenshot (JPEG/PNG) to file without stalling the game -/// This implementation captures the frame buffer on the main thread, then spawns a background thread -/// to compress and save the image, allowing the game to continue running smoothly. +struct ScreenshotThreadData +{ + unsigned char* imageData; + unsigned int width; + unsigned int height; + char pathname[1024]; + char leafname[256]; +}; + +static DWORD WINAPI screenshotThreadFunc(LPVOID param) +{ + ScreenshotThreadData* data = (ScreenshotThreadData*)param; + + int result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, 90); + + if (!result) { + OutputDebugStringA("Failed to write screenshot JPEG\n"); + } + + delete [] data->imageData; + delete data; + + return 0; +} + void W3DDisplay::takeScreenShotCompressed(void) { - // TheSuperHackers @bobtista 02/11/2025 Find next available filename char leafname[256]; char pathname[1024]; static int frame_number = 1; @@ -3153,7 +3171,6 @@ void W3DDisplay::takeScreenShotCompressed(void) done = true; } - // TheSuperHackers @bobtista 02/11/2025 Get the back buffer and create a copy SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); SurfaceClass::SurfaceDescription surfaceDesc; surface->Get_Description(surfaceDesc); @@ -3181,13 +3198,8 @@ void W3DDisplay::takeScreenShotCompressed(void) unsigned int width = surfaceDesc.Width; unsigned int height = surfaceDesc.Height; - // TheSuperHackers @bobtista 02/11/2025 Allocate buffer for RGB image data - // Using shared_ptr for automatic cleanup in the background thread - std::shared_ptr imageData(new unsigned char[3 * width * height], - std::default_delete()); - unsigned char* image = imageData.get(); + unsigned char* image = new unsigned char[3 * width * height]; - // TheSuperHackers @bobtista 02/11/2025 Copy and convert BGRA to RGB for (y = 0; y < height; y++) { for (x = 0; x < width; x++) @@ -3195,9 +3207,9 @@ void W3DDisplay::takeScreenShotCompressed(void) index = 3 * (x + y * width); index2 = y * lrect.Pitch + 4 * x; - image[index] = *((unsigned char*)lrect.pBits + index2 + 2); // R - image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); // G - image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); // B + image[index] = *((unsigned char*)lrect.pBits + index2 + 2); + image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); + image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); } } @@ -3205,29 +3217,21 @@ void W3DDisplay::takeScreenShotCompressed(void) surfaceCopy->Release_Ref(); surfaceCopy = NULL; - // TheSuperHackers @bobtista 02/11/2025 Make a copy of the pathname for the background thread - std::string pathnameCopy(pathname); - std::string leafnameCopy(leafname); - - // TheSuperHackers @bobtista 02/11/2025 Spawn background thread to compress and save the image - // This allows the game to continue running without freezing - std::thread([imageData, width, height, pathnameCopy, leafnameCopy]() { - // TheSuperHackers @bobtista 02/11/2025 Write JPEG with quality 90 (range: 1-100) - // stbi_write_jpg expects image data with Y-axis going down, which matches our data - int result = stbi_write_jpg(pathnameCopy.c_str(), width, height, 3, imageData.get(), 90); - - if (!result) { - // TheSuperHackers @bobtista 02/11/2025 Log error if write failed - // Note: Can't show UI message from background thread - OutputDebugStringA("Failed to write screenshot JPEG\n"); - } - - // TheSuperHackers @bobtista 02/11/2025 imageData will be automatically cleaned up when shared_ptr goes out of scope - }).detach(); // TheSuperHackers @bobtista 02/11/2025 Detach thread to run independently - - // TheSuperHackers @bobtista 02/11/2025 Show message to user immediately (file is being saved in background) + ScreenshotThreadData* threadData = new ScreenshotThreadData(); + threadData->imageData = image; + threadData->width = width; + threadData->height = height; + strcpy(threadData->pathname, pathname); + strcpy(threadData->leafname, leafname); + + DWORD threadId; + HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + if (hThread) { + CloseHandle(hThread); + } + UnicodeString ufileName; - ufileName.translate(leafnameCopy.c_str()); + ufileName.translate(leafname); TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } diff --git a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index 19f2f4803e..b3e3afb022 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -102,6 +102,7 @@ class GUIEditDisplay : public Display virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY, Int endX, Int endY ) { } virtual void takeScreenShot(void){ } + virtual void takeScreenShotCompressed(void){ } virtual void toggleMovieCapture(void) {} // methods that we need to stub From 00ff8727a4866ce61afb506b6b9cfe1a11ca3f3f Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:04:47 -0500 Subject: [PATCH 05/26] Move screenshot logic to Core to eliminate code duplication Add shared screenshot implementation in Core --- Core/GameEngineDevice/CMakeLists.txt | 3 + .../W3DDevice/GameClient/W3DScreenshot.h | 28 ++++ .../W3DDevice/GameClient/W3DScreenshot.cpp | 152 ++++++++++++++++++ .../W3DDevice/GameClient/W3DDisplay.cpp | 105 +----------- .../W3DDevice/GameClient/W3DDisplay.cpp | 105 +----------- 5 files changed, 187 insertions(+), 206 deletions(-) create mode 100644 Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h create mode 100644 Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 74b040200a..f733f19fb2 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -72,6 +72,7 @@ set(GAMEENGINEDEVICE_SRC Include/W3DDevice/GameClient/W3DTreeBuffer.h Include/W3DDevice/GameClient/W3DVideoBuffer.h Include/W3DDevice/GameClient/W3DView.h + Include/W3DDevice/GameClient/W3DScreenshot.h # Include/W3DDevice/GameClient/W3DVolumetricShadow.h Include/W3DDevice/GameClient/W3DWater.h Include/W3DDevice/GameClient/W3DWaterTracks.h @@ -173,6 +174,7 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DTreeBuffer.cpp Source/W3DDevice/GameClient/W3DVideoBuffer.cpp Source/W3DDevice/GameClient/W3DView.cpp + Source/W3DDevice/GameClient/W3DScreenshot.cpp # Source/W3DDevice/GameClient/W3dWaypointBuffer.cpp # Source/W3DDevice/GameClient/W3DWebBrowser.cpp Source/W3DDevice/GameClient/Water/W3DWater.cpp @@ -218,6 +220,7 @@ target_include_directories(corei_gameenginedevice_public INTERFACE target_link_libraries(corei_gameenginedevice_private INTERFACE corei_always corei_main + stb ) target_link_libraries(corei_gameenginedevice_public INTERFACE diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h new file mode 100644 index 0000000000..234560af0f --- /dev/null +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h @@ -0,0 +1,28 @@ +/* +** Command & Conquer Generals Zero Hour(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 . +*/ + +#pragma once + +enum ScreenshotFormat +{ + SCREENSHOT_JPEG, + SCREENSHOT_PNG +}; + +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 80); + diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp new file mode 100644 index 0000000000..08a37c9b9d --- /dev/null +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -0,0 +1,152 @@ +/* +** Command & Conquer Generals Zero Hour(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 . +*/ + +#include +#include +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + +#include "W3DDevice/GameClient/W3DScreenshot.h" +#include "Common/GlobalData.h" +#include "GameClient/InGameUI.h" +#include "GameClient/GameText.h" +#include "WW3D2/dx8wrapper.h" +#include "WW3D2/surface.h" + +struct ScreenshotThreadData +{ + unsigned char* imageData; + unsigned int width; + unsigned int height; + char pathname[1024]; + char leafname[256]; + int quality; + ScreenshotFormat format; +}; + +static DWORD WINAPI screenshotThreadFunc(LPVOID param) +{ + ScreenshotThreadData* data = (ScreenshotThreadData*)param; + + int result = 0; + if (data->format == SCREENSHOT_JPEG) + { + result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); + } + else if (data->format == SCREENSHOT_PNG) + { + result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); + } + + if (!result) { + OutputDebugStringA("Failed to write screenshot\n"); + } + + delete [] data->imageData; + delete data; + + return 0; +} + +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) +{ + char leafname[256]; + char pathname[1024]; + static int jpegFrameNumber = 1; + static int pngFrameNumber = 1; + + int* frameNumber = (format == SCREENSHOT_JPEG) ? &jpegFrameNumber : &pngFrameNumber; + const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png"; + + Bool done = false; + while (!done) { + sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension); + strcpy(pathname, TheGlobalData->getPath_UserData().str()); + strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + if (_access(pathname, 0) == -1) + done = true; + } + + SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); + SurfaceClass::SurfaceDescription surfaceDesc; + surface->Get_Description(surfaceDesc); + + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + + surface->Release_Ref(); + surface = NULL; + + struct Rect + { + int Pitch; + void* pBits; + } lrect; + + lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); + if (lrect.pBits == NULL) + { + surfaceCopy->Release_Ref(); + return; + } + + unsigned int x, y, index, index2; + unsigned int width = surfaceDesc.Width; + unsigned int height = surfaceDesc.Height; + + unsigned char* image = new unsigned char[3 * width * height]; + + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + index = 3 * (x + y * width); + index2 = y * lrect.Pitch + 4 * x; + + image[index] = *((unsigned char*)lrect.pBits + index2 + 2); + image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); + image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); + } + } + + surfaceCopy->Unlock(); + surfaceCopy->Release_Ref(); + surfaceCopy = NULL; + + ScreenshotThreadData* threadData = new ScreenshotThreadData(); + threadData->imageData = image; + threadData->width = width; + threadData->height = height; + threadData->quality = quality; + threadData->format = format; + strcpy(threadData->pathname, pathname); + strcpy(threadData->leafname, leafname); + + DWORD threadId; + HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + if (hThread) { + CloseHandle(hThread); + } + + UnicodeString ufileName; + ufileName.translate(leafname); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); +} + diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 340dcda7e5..90a543ad6d 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -40,10 +40,8 @@ static void drawFramerateBar(); #include #include -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include - // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "W3DDevice/GameClient/W3DScreenshot.h" #include "Common/FramePacer.h" #include "Common/ThingFactory.h" #include "Common/GlobalData.h" @@ -3019,108 +3017,9 @@ void W3DDisplay::takeScreenShot() TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } -struct ScreenshotThreadData -{ - unsigned char* imageData; - unsigned int width; - unsigned int height; - char pathname[1024]; - char leafname[256]; -}; - -static DWORD WINAPI screenshotThreadFunc(LPVOID param) -{ - ScreenshotThreadData* data = (ScreenshotThreadData*)param; - - int result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, 90); - - if (!result) { - OutputDebugStringA("Failed to write screenshot JPEG\n"); - } - - delete [] data->imageData; - delete data; - - return 0; -} - void W3DDisplay::takeScreenShotCompressed(void) { - char leafname[256]; - char pathname[1024]; - static int frame_number = 1; - - Bool done = false; - while (!done) { - sprintf(leafname, "sshot%.3d.jpg", frame_number++); - strcpy(pathname, TheGlobalData->getPath_UserData().str()); - strlcat(pathname, leafname, ARRAY_SIZE(pathname)); - if (_access(pathname, 0) == -1) - done = true; - } - - SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); - SurfaceClass::SurfaceDescription surfaceDesc; - surface->Get_Description(surfaceDesc); - - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); - DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); - - surface->Release_Ref(); - surface = NULL; - - struct Rect - { - int Pitch; - void* pBits; - } lrect; - - lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == NULL) - { - surfaceCopy->Release_Ref(); - return; - } - - unsigned int x, y, index, index2; - unsigned int width = surfaceDesc.Width; - unsigned int height = surfaceDesc.Height; - - unsigned char* image = new unsigned char[3 * width * height]; - - for (y = 0; y < height; y++) - { - for (x = 0; x < width; x++) - { - index = 3 * (x + y * width); - index2 = y * lrect.Pitch + 4 * x; - - image[index] = *((unsigned char*)lrect.pBits + index2 + 2); - image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); - image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); - } - } - - surfaceCopy->Unlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = NULL; - - ScreenshotThreadData* threadData = new ScreenshotThreadData(); - threadData->imageData = image; - threadData->width = width; - threadData->height = height; - strcpy(threadData->pathname, pathname); - strcpy(threadData->leafname, leafname); - - DWORD threadId; - HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); - if (hThread) { - CloseHandle(hThread); - } - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); + W3D_TakeCompressedScreenshot(SCREENSHOT_JPEG, 80); } /** Start/Stop capturing an AVI movie*/ diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 0b3570724f..7b559271ae 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -40,10 +40,8 @@ static void drawFramerateBar(); #include #include -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include - // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "W3DDevice/GameClient/W3DScreenshot.h" #include "Common/FramePacer.h" #include "Common/ThingFactory.h" #include "Common/GlobalData.h" @@ -3131,108 +3129,9 @@ void W3DDisplay::takeScreenShot() TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } -struct ScreenshotThreadData -{ - unsigned char* imageData; - unsigned int width; - unsigned int height; - char pathname[1024]; - char leafname[256]; -}; - -static DWORD WINAPI screenshotThreadFunc(LPVOID param) -{ - ScreenshotThreadData* data = (ScreenshotThreadData*)param; - - int result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, 90); - - if (!result) { - OutputDebugStringA("Failed to write screenshot JPEG\n"); - } - - delete [] data->imageData; - delete data; - - return 0; -} - void W3DDisplay::takeScreenShotCompressed(void) { - char leafname[256]; - char pathname[1024]; - static int frame_number = 1; - - Bool done = false; - while (!done) { - sprintf(leafname, "sshot%.3d.jpg", frame_number++); - strcpy(pathname, TheGlobalData->getPath_UserData().str()); - strlcat(pathname, leafname, ARRAY_SIZE(pathname)); - if (_access(pathname, 0) == -1) - done = true; - } - - SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); - SurfaceClass::SurfaceDescription surfaceDesc; - surface->Get_Description(surfaceDesc); - - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); - DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); - - surface->Release_Ref(); - surface = NULL; - - struct Rect - { - int Pitch; - void* pBits; - } lrect; - - lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == NULL) - { - surfaceCopy->Release_Ref(); - return; - } - - unsigned int x, y, index, index2; - unsigned int width = surfaceDesc.Width; - unsigned int height = surfaceDesc.Height; - - unsigned char* image = new unsigned char[3 * width * height]; - - for (y = 0; y < height; y++) - { - for (x = 0; x < width; x++) - { - index = 3 * (x + y * width); - index2 = y * lrect.Pitch + 4 * x; - - image[index] = *((unsigned char*)lrect.pBits + index2 + 2); - image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); - image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); - } - } - - surfaceCopy->Unlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = NULL; - - ScreenshotThreadData* threadData = new ScreenshotThreadData(); - threadData->imageData = image; - threadData->width = width; - threadData->height = height; - strcpy(threadData->pathname, pathname); - strcpy(threadData->leafname, leafname); - - DWORD threadId; - HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); - if (hThread) { - CloseHandle(hThread); - } - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); + W3D_TakeCompressedScreenshot(SCREENSHOT_JPEG, 80); } /** Start/Stop capturing an AVI movie*/ From 734385f6d1fcb8947c191a64a73fddd65a978842 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:12:09 -0500 Subject: [PATCH 06/26] Change F12 to JPEG and add CTRL+F12 for PNG screenshots --- Generals/Code/GameEngine/Include/Common/MessageStream.h | 4 ++-- Generals/Code/GameEngine/Include/GameClient/Display.h | 4 ++-- Generals/Code/GameEngine/Source/Common/MessageStream.cpp | 2 +- .../Source/GameClient/MessageStream/CommandXlat.cpp | 6 +++--- .../Source/GameClient/MessageStream/MetaEvent.cpp | 8 ++++---- .../Include/W3DDevice/GameClient/W3DDisplay.h | 4 ++-- .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 5 +++++ Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h | 2 +- GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h | 4 ++-- GeneralsMD/Code/GameEngine/Include/GameClient/Display.h | 4 ++-- .../Code/GameEngine/Source/Common/MessageStream.cpp | 2 +- .../Source/GameClient/MessageStream/CommandXlat.cpp | 6 +++--- .../Source/GameClient/MessageStream/MetaEvent.cpp | 8 ++++---- .../Include/W3DDevice/GameClient/W3DDisplay.h | 4 ++-- .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 5 +++++ GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h | 2 +- 16 files changed, 40 insertions(+), 30 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 920a855dc1..a027158e6a 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -257,8 +257,8 @@ class GameMessage : public MemoryPoolObject MSG_META_BEGIN_PREFER_SELECTION, ///< The Shift key has been depressed alone MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. - MSG_META_TAKE_SCREENSHOT, ///< take screenshot - MSG_META_TAKE_SCREENSHOT_COMPRESSED, ///< take compressed screenshot without stalling + MSG_META_TAKE_SCREENSHOT, ///< take screenshot (JPEG) + MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/Generals/Code/GameEngine/Include/GameClient/Display.h b/Generals/Code/GameEngine/Include/GameClient/Display.h index 65d398976c..2ce863b22d 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -166,8 +166,8 @@ class Display : public SubsystemInterface virtual void preloadModelAssets( AsciiString model ) = 0; ///< preload model asset virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset - virtual void takeScreenShot() = 0; ///< saves screenshot to a file - virtual void takeScreenShotCompressed() = 0; ///< saves compressed screenshot without stalling + virtual void takeScreenShotCompressed() = 0; ///< saves JPEG screenshot + virtual void takeScreenShotPNG() = 0; ///< saves PNG screenshot virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp index 52a414d1e9..27eb52f3b3 100644 --- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,7 +364,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION) CASE_LABEL(MSG_META_END_PREFER_SELECTION) CASE_LABEL(MSG_META_TAKE_SCREENSHOT) - CASE_LABEL(MSG_META_TAKE_SCREENSHOT_COMPRESSED) + CASE_LABEL(MSG_META_TAKE_SCREENSHOT_PNG) CASE_LABEL(MSG_META_ALL_CHEER) CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE) CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT) diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 917a91f14d..23d3b6b166 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3418,14 +3418,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage case GameMessage::MSG_META_TAKE_SCREENSHOT: { if (TheDisplay) - TheDisplay->takeScreenShot(); + TheDisplay->takeScreenShotCompressed(); break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: + case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: { if (TheDisplay) - TheDisplay->takeScreenShotCompressed(); + TheDisplay->takeScreenShotPNG(); disp = DESTROY_MESSAGE; break; } diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index c9102fd1f5..e700febc5d 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -163,7 +163,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, - { "TAKE_SCREENSHOT_COMPRESSED", GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED }, + { "TAKE_SCREENSHOT_PNG", GameMessage::MSG_META_TAKE_SCREENSHOT_PNG }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -815,12 +815,12 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED); + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); if (map->m_key == MK_NONE) { - map->m_key = MK_F11; + map->m_key = MK_F12; map->m_transition = DOWN; - map->m_modState = NONE; + map->m_modState = CTRL; map->m_usableIn = COMMANDUSABLE_EVERYWHERE; } } diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 20815d7318..abf93e25ab 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -120,8 +120,8 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer() ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShot(); //save screenshot to file - virtual void takeScreenShotCompressed(); //save compressed screenshot to file (JPG/PNG) without stalling + virtual void takeScreenShotCompressed(); //save JPEG screenshot + virtual void takeScreenShotPNG(); //save PNG screenshot virtual void toggleMovieCapture(); //enable AVI or frame capture mode. virtual void toggleLetterBox(); ///takeScreenShot(); + TheDisplay->takeScreenShotCompressed(); break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED: + case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: { if (TheDisplay) - TheDisplay->takeScreenShotCompressed(); + TheDisplay->takeScreenShotPNG(); disp = DESTROY_MESSAGE; break; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 92d405b250..2a62398be1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -171,7 +171,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, - { "TAKE_SCREENSHOT_COMPRESSED", GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED }, + { "TAKE_SCREENSHOT_PNG", GameMessage::MSG_META_TAKE_SCREENSHOT_PNG }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -873,12 +873,12 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_COMPRESSED); + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); if (map->m_key == MK_NONE) { - map->m_key = MK_F11; + map->m_key = MK_F12; map->m_transition = DOWN; - map->m_modState = NONE; + map->m_modState = CTRL; map->m_usableIn = COMMANDUSABLE_EVERYWHERE; } } diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 65628e1b0d..c0c4bb93c8 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -120,8 +120,8 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer() ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShot(); //save screenshot to file - virtual void takeScreenShotCompressed(); //save compressed screenshot to file (JPG/PNG) without stalling + virtual void takeScreenShotCompressed(); //save JPEG screenshot + virtual void takeScreenShotPNG(); //save PNG screenshot virtual void toggleMovieCapture(); //enable AVI or frame capture mode. virtual void toggleLetterBox(); /// Date: Mon, 3 Nov 2025 11:13:05 -0500 Subject: [PATCH 07/26] Remove old BMP screenshot code --- .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 6 ------ .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 6 ------ 2 files changed, 12 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 6bb2f4e497..e5431ca591 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2879,11 +2879,6 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) LocalFree( (HLOCAL) pbmi); } -///Save Screen Capture to a file -void W3DDisplay::takeScreenShot() -{ - char leafname[256]; - char pathname[1024]; static int frame_number = 1; @@ -3016,7 +3011,6 @@ void W3DDisplay::takeScreenShot() ufileName.translate(leafname); TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } - void W3DDisplay::takeScreenShotCompressed(void) { W3D_TakeCompressedScreenshot(SCREENSHOT_JPEG, 80); diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index aadf9b3390..a0880c99ca 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2991,11 +2991,6 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) LocalFree( (HLOCAL) pbmi); } -///Save Screen Capture to a file -void W3DDisplay::takeScreenShot() -{ - char leafname[256]; - char pathname[1024]; static int frame_number = 1; @@ -3128,7 +3123,6 @@ void W3DDisplay::takeScreenShot() ufileName.translate(leafname); TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } - void W3DDisplay::takeScreenShotCompressed(void) { W3D_TakeCompressedScreenshot(SCREENSHOT_JPEG, 80); From 8dac55fbd1dd9fd5921d16ce2211763a0a69d96c Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:15:41 -0500 Subject: [PATCH 08/26] Add JPEGQuality option to Options.ini (default 80) Move W3DScreenshot implementation to game-specific directories Fix include order for VC6 precompiled headers Remove default parameter from function definition Move STB implementation to separate file to avoid PCH issues Include screenshot implementation directly in W3DDisplay.cpp to avoid PCH issues Use Windows constants and switch statement in screenshot code Use vcpkg for stb dependency with FetchContent fallback --- .../Include/Common/OptionPreferences.h | 1 + .../Source/Common/OptionPreferences.cpp | 12 ++ .../W3DDevice/GameClient/W3DScreenshot.h | 6 +- .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Include/Common/MessageStream.h | 4 +- .../GameEngine/Include/GameClient/Display.h | 9 +- .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../Source/Common/MessageStream.cpp | 2 +- .../GameClient/MessageStream/CommandXlat.cpp | 6 +- .../GameClient/MessageStream/MetaEvent.cpp | 14 +- Generals/Code/GameEngineDevice/CMakeLists.txt | 10 ++ .../Include/W3DDevice/GameClient/W3DDisplay.h | 3 +- .../W3DDevice/GameClient/W3DDisplay.cpp | 143 +----------------- .../W3DDevice/GameClient/W3DScreenshot.cpp | 58 +++---- .../GameClient/stb_image_write_impl.cpp | 21 +++ .../Tools/GUIEdit/Include/GUIEditDisplay.h | 3 +- .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Include/Common/MessageStream.h | 4 +- .../GameEngine/Include/GameClient/Display.h | 9 +- .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../Source/Common/MessageStream.cpp | 2 +- .../GameClient/MessageStream/CommandXlat.cpp | 6 +- .../GameClient/MessageStream/MetaEvent.cpp | 14 +- .../Code/GameEngineDevice/CMakeLists.txt | 10 ++ .../Include/W3DDevice/GameClient/W3DDisplay.h | 3 +- .../W3DDevice/GameClient/W3DDisplay.cpp | 143 +----------------- .../W3DDevice/GameClient/W3DScreenshot.cpp | 130 ++++++++++++++++ .../GameClient/stb_image_write_impl.cpp | 21 +++ .../Tools/GUIEdit/Include/GUIEditDisplay.h | 3 +- cmake/stb.cmake | 22 +-- vcpkg.json | 3 +- 31 files changed, 300 insertions(+), 366 deletions(-) rename {Core => Generals/Code}/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp (66%) create mode 100644 Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp create mode 100644 GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp create mode 100644 GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index ea500e97ca..9780f06898 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -56,6 +56,7 @@ class OptionPreferences : public UserPreferences Bool getAlternateMouseModeEnabled(); Bool getRetaliationModeEnabled(); Bool getDoubleClickAttackMoveEnabled(); + Int getJPEGQuality(); Real getScrollFactor(); Bool getDrawScrollAnchor(); Bool getMoveScrollAnchor(); diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index 063de6838d..5201f1ede8 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -192,6 +192,18 @@ Bool OptionPreferences::getDoubleClickAttackMoveEnabled() return FALSE; } +Int OptionPreferences::getJPEGQuality() +{ + OptionPreferences::const_iterator it = find("JPEGQuality"); + if (it == end()) + return 80; + + Int quality = atoi(it->second.str()); + if (quality < 1) quality = 1; + if (quality > 100) quality = 100; + return quality; +} + Real OptionPreferences::getScrollFactor() { OptionPreferences::const_iterator it = find("ScrollFactor"); diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index 234560af0f..fe43aa8032 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h @@ -18,11 +18,7 @@ #pragma once -enum ScreenshotFormat -{ - SCREENSHOT_JPEG, - SCREENSHOT_PNG -}; +#include "GameClient/Display.h" void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 80); diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 7974dd6486..b3d5702013 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -142,6 +142,7 @@ class GlobalData : public SubsystemInterface Bool m_clientRetaliationModeEnabled; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; + Int m_jpegQuality; Bool m_useWaterPlane; Bool m_useCloudPlane; Bool m_useShadowVolumes; diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index a027158e6a..99996c3fd7 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -257,8 +257,8 @@ class GameMessage : public MemoryPoolObject MSG_META_BEGIN_PREFER_SELECTION, ///< The Shift key has been depressed alone MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. - MSG_META_TAKE_SCREENSHOT, ///< take screenshot (JPEG) - MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot + MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) + MSG_META_TAKE_SCREENSHOT_JPEG, ///< take PNG screenshot (CTRL+F12, lossless) MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/Generals/Code/GameEngine/Include/GameClient/Display.h b/Generals/Code/GameEngine/Include/GameClient/Display.h index 2ce863b22d..f3c5853c28 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Display.h +++ b/Generals/Code/GameEngine/Include/GameClient/Display.h @@ -33,6 +33,12 @@ #include "GameClient/GameFont.h" #include "GameClient/View.h" +enum ScreenshotFormat +{ + SCREENSHOT_JPEG, + SCREENSHOT_PNG +}; + struct ShroudLevel { Short m_currentShroud; ///< A Value of 1 means shrouded. 0 is not. Negative is the count of people looking. @@ -166,8 +172,7 @@ class Display : public SubsystemInterface virtual void preloadModelAssets( AsciiString model ) = 0; ///< preload model asset virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset - virtual void takeScreenShotCompressed() = 0; ///< saves JPEG screenshot - virtual void takeScreenShotPNG() = 0; ///< saves PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format) = 0; ///< saves screenshot in specified format virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index b105179b71..f1024e32a0 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1189,6 +1189,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useAlternateMouse = optionPref.getAlternateMouseModeEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_jpegQuality = optionPref.getJPEGQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); TheWritableGlobalData->m_moveScrollAnchor = optionPref.getMoveScrollAnchor(); diff --git a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp index 27eb52f3b3..2ba36c24fc 100644 --- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,7 +364,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION) CASE_LABEL(MSG_META_END_PREFER_SELECTION) CASE_LABEL(MSG_META_TAKE_SCREENSHOT) - CASE_LABEL(MSG_META_TAKE_SCREENSHOT_PNG) + CASE_LABEL(MSG_META_TAKE_SCREENSHOT_JPEG) CASE_LABEL(MSG_META_ALL_CHEER) CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE) CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT) diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 23d3b6b166..fbd31cd5a2 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3418,14 +3418,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage case GameMessage::MSG_META_TAKE_SCREENSHOT: { if (TheDisplay) - TheDisplay->takeScreenShotCompressed(); + TheDisplay->takeScreenShot(SCREENSHOT_JPEG); break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: + case GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG: { if (TheDisplay) - TheDisplay->takeScreenShotPNG(); + TheDisplay->takeScreenShot(SCREENSHOT_PNG); disp = DESTROY_MESSAGE; break; } diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index e700febc5d..4fdf92e728 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -163,7 +163,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, - { "TAKE_SCREENSHOT_PNG", GameMessage::MSG_META_TAKE_SCREENSHOT_PNG }, + { "TAKE_SCREENSHOT_JPEG", GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -815,7 +815,17 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT); + if (map->m_key == MK_NONE) + { + map->m_key = MK_F12; + map->m_transition = DOWN; + map->m_modState = NONE; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } + { + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG); if (map->m_key == MK_NONE) { map->m_key = MK_F12; diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index ac8ee37aae..feb45ced75 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -203,6 +203,16 @@ target_link_libraries(g_gameenginedevice PRIVATE stb ) +target_sources(g_gameenginedevice PRIVATE + Source/W3DDevice/GameClient/stb_image_write_impl.cpp +) + +set_source_files_properties( + Source/W3DDevice/GameClient/stb_image_write_impl.cpp + PROPERTIES + SKIP_PRECOMPILE_HEADERS ON +) + target_link_libraries(g_gameenginedevice PUBLIC corei_gameenginedevice_public g_gameengine diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index abf93e25ab..305bc2e467 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -120,8 +120,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer() ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShotCompressed(); //save JPEG screenshot - virtual void takeScreenShotPNG(); //save PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format); //save screenshot in specified format virtual void toggleMovieCapture(); //enable AVI or frame capture mode. virtual void toggleLetterBox(); ///getPath_UserData().str(), ARRAY_SIZE(pathname)); - strlcat(pathname, leafname, ARRAY_SIZE(pathname)); - if (_access( pathname, 0 ) == -1) - done = true; - } - - // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface. - // Originally this code took the front buffer and tried to lock it. This does not work when the - // render view clips outside the desktop boundaries. It crashed the game. - SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); - - SurfaceClass::SurfaceDescription surfaceDesc; - surface->Get_Description(surfaceDesc); - - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); - DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); - - surface->Release_Ref(); - surface = nullptr; - - struct Rect - { - int Pitch; - void* pBits; - } lrect; - - lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == nullptr) - { - surfaceCopy->Release_Ref(); - return; - } - - unsigned int x,y,index,index2,width,height; - - width = surfaceDesc.Width; - height = surfaceDesc.Height; - - char *image=NEW char[3*width*height]; -#ifdef CAPTURE_TO_TARGA - //bytes are mixed in targa files, not rgb order. - for (y=0; yUnlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = nullptr; - - Targa targ; - memset(&targ.Header,0,sizeof(targ.Header)); - targ.Header.Width=width; - targ.Header.Height=height; - targ.Header.PixelDepth=24; - targ.Header.ImageType=TGA_TRUECOLOR; - targ.SetImage(image); - targ.YFlip(); - - targ.Save(pathname,TGAF_IMAGE,false); -#else //capturing to bmp file - //bmp is same byte order - for (y=0; yUnlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = nullptr; - - //Flip the image - char *ptr,*ptr1; - char v,v1; - - for (y = 0; y < (height >> 1); y++) - { - /* Compute address of lines to exchange. */ - ptr = (image + ((width * y) * 3)); - ptr1 = (image + ((width * (height - 1)) * 3)); - ptr1 -= ((width * y) * 3); - - /* Exchange all the pixels on this scan line. */ - for (x = 0; x < (width * 3); x++) - { - v = *ptr; - v1 = *ptr1; - *ptr = v1; - *ptr1 = v; - ptr++; - ptr1++; - } - } - CreateBMPFile(pathname, image, width, height); -#endif - - delete [] image; - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); -} -void W3DDisplay::takeScreenShotCompressed(void) -{ - W3D_TakeCompressedScreenshot(SCREENSHOT_JPEG, 80); -} - -void W3DDisplay::takeScreenShotPNG(void) -{ - W3D_TakeCompressedScreenshot(SCREENSHOT_PNG, 0); -} +///Save Screen Capture to a file +#include "W3DScreenshot.cpp" /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture() diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp similarity index 66% rename from Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp rename to Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 08a37c9b9d..9a601d1232 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -1,42 +1,12 @@ -/* -** Command & Conquer Generals Zero Hour(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 . -*/ - -#include -#include -#include - -#define STB_IMAGE_WRITE_IMPLEMENTATION #include -#include "W3DDevice/GameClient/W3DScreenshot.h" -#include "Common/GlobalData.h" -#include "GameClient/InGameUI.h" -#include "GameClient/GameText.h" -#include "WW3D2/dx8wrapper.h" -#include "WW3D2/surface.h" - struct ScreenshotThreadData { unsigned char* imageData; unsigned int width; unsigned int height; - char pathname[1024]; - char leafname[256]; + char pathname[_MAX_PATH]; + char leafname[_MAX_FNAME]; int quality; ScreenshotFormat format; }; @@ -46,13 +16,14 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) ScreenshotThreadData* data = (ScreenshotThreadData*)param; int result = 0; - if (data->format == SCREENSHOT_JPEG) - { - result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); - } - else if (data->format == SCREENSHOT_PNG) + switch (data->format) { - result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); + case SCREENSHOT_JPEG: + result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); + break; + case SCREENSHOT_PNG: + result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); + break; } if (!result) { @@ -67,8 +38,8 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) { - char leafname[256]; - char pathname[1024]; + char leafname[_MAX_FNAME]; + char pathname[_MAX_PATH]; static int jpegFrameNumber = 1; static int pngFrameNumber = 1; @@ -130,6 +101,9 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surfaceCopy->Release_Ref(); surfaceCopy = NULL; + if (quality <= 0 && format == SCREENSHOT_JPEG) + quality = TheGlobalData->m_jpegQuality; + ScreenshotThreadData* threadData = new ScreenshotThreadData(); threadData->imageData = image; threadData->width = width; @@ -150,3 +124,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } +void W3DDisplay::takeScreenShot(ScreenshotFormat format) +{ + W3D_TakeCompressedScreenshot(format); +} diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp new file mode 100644 index 0000000000..364368901a --- /dev/null +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp @@ -0,0 +1,21 @@ +/* +** 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 . +*/ + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + diff --git a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index 962305e190..a581174d56 100644 --- a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -101,8 +101,7 @@ class GUIEditDisplay : public Display virtual void drawScaledVideoBuffer( VideoBuffer *buffer, VideoStreamInterface *stream ) { } virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY, Int endX, Int endY ) { } - virtual void takeScreenShotCompressed(void){ } - virtual void takeScreenShotPNG(void){ } + virtual void takeScreenShot(ScreenshotFormat){ } virtual void toggleMovieCapture(void) {} // methods that we need to stub diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index e826ca7c35..5df4c32378 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -143,6 +143,7 @@ class GlobalData : public SubsystemInterface Bool m_clientRetaliationModeEnabled; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; + Int m_jpegQuality; Bool m_useWaterPlane; Bool m_useCloudPlane; Bool m_useShadowVolumes; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 60db8e7b5e..42ab138cff 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -257,8 +257,8 @@ class GameMessage : public MemoryPoolObject MSG_META_BEGIN_PREFER_SELECTION, ///< The Shift key has been depressed alone MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. - MSG_META_TAKE_SCREENSHOT, ///< take screenshot (JPEG) - MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot + MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) + MSG_META_TAKE_SCREENSHOT_JPEG, ///< take PNG screenshot (CTRL+F12, lossless) MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h index 86af13a30d..f421c0789b 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h @@ -33,6 +33,12 @@ #include "GameClient/GameFont.h" #include "GameClient/View.h" +enum ScreenshotFormat +{ + SCREENSHOT_JPEG, + SCREENSHOT_PNG +}; + struct ShroudLevel { Short m_currentShroud; ///< A Value of 1 means shrouded. 0 is not. Negative is the count of people looking. @@ -166,8 +172,7 @@ class Display : public SubsystemInterface virtual void preloadModelAssets( AsciiString model ) = 0; ///< preload model asset virtual void preloadTextureAssets( AsciiString texture ) = 0; ///< preload texture asset - virtual void takeScreenShotCompressed() = 0; ///< saves JPEG screenshot - virtual void takeScreenShotPNG() = 0; ///< saves PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format) = 0; ///< saves screenshot in specified format virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e0a3bde39f..2781811f7f 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1209,6 +1209,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useAlternateMouse = optionPref.getAlternateMouseModeEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_jpegQuality = optionPref.getJPEGQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); TheWritableGlobalData->m_moveScrollAnchor = optionPref.getMoveScrollAnchor(); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp index 1e6103de3f..68431b46ad 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,7 +364,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION) CASE_LABEL(MSG_META_END_PREFER_SELECTION) CASE_LABEL(MSG_META_TAKE_SCREENSHOT) - CASE_LABEL(MSG_META_TAKE_SCREENSHOT_PNG) + CASE_LABEL(MSG_META_TAKE_SCREENSHOT_JPEG) CASE_LABEL(MSG_META_ALL_CHEER) CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE) CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 4846f19256..64ffe90687 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3751,14 +3751,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage case GameMessage::MSG_META_TAKE_SCREENSHOT: { if (TheDisplay) - TheDisplay->takeScreenShotCompressed(); + TheDisplay->takeScreenShot(SCREENSHOT_JPEG); break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: + case GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG: { if (TheDisplay) - TheDisplay->takeScreenShotPNG(); + TheDisplay->takeScreenShot(SCREENSHOT_PNG); disp = DESTROY_MESSAGE; break; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 2a62398be1..b8482ea78a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -171,7 +171,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, - { "TAKE_SCREENSHOT_PNG", GameMessage::MSG_META_TAKE_SCREENSHOT_PNG }, + { "TAKE_SCREENSHOT_JPEG", GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -873,7 +873,17 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT); + if (map->m_key == MK_NONE) + { + map->m_key = MK_F12; + map->m_transition = DOWN; + map->m_modState = NONE; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } + { + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG); if (map->m_key == MK_NONE) { map->m_key = MK_F12; diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 062e4e60b8..07235a2b9d 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -216,6 +216,16 @@ target_link_libraries(z_gameenginedevice PRIVATE stb ) +target_sources(z_gameenginedevice PRIVATE + Source/W3DDevice/GameClient/stb_image_write_impl.cpp +) + +set_source_files_properties( + Source/W3DDevice/GameClient/stb_image_write_impl.cpp + PROPERTIES + SKIP_PRECOMPILE_HEADERS ON +) + target_link_libraries(z_gameenginedevice PUBLIC corei_gameenginedevice_public z_gameengine diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index c0c4bb93c8..842fcf4b89 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -120,8 +120,7 @@ class W3DDisplay : public Display virtual VideoBuffer* createVideoBuffer() ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShotCompressed(); //save JPEG screenshot - virtual void takeScreenShotPNG(); //save PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format); //save screenshot in specified format virtual void toggleMovieCapture(); //enable AVI or frame capture mode. virtual void toggleLetterBox(); ///getPath_UserData().str(), ARRAY_SIZE(pathname)); - strlcat(pathname, leafname, ARRAY_SIZE(pathname)); - if (_access( pathname, 0 ) == -1) - done = true; - } - - // TheSuperHackers @bugfix xezon 21/05/2025 Get the back buffer and create a copy of the surface. - // Originally this code took the front buffer and tried to lock it. This does not work when the - // render view clips outside the desktop boundaries. It crashed the game. - SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); - - SurfaceClass::SurfaceDescription surfaceDesc; - surface->Get_Description(surfaceDesc); - - SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); - DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); - - surface->Release_Ref(); - surface = nullptr; - - struct Rect - { - int Pitch; - void* pBits; - } lrect; - - lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == nullptr) - { - surfaceCopy->Release_Ref(); - return; - } - - unsigned int x,y,index,index2,width,height; - - width = surfaceDesc.Width; - height = surfaceDesc.Height; - - char *image=NEW char[3*width*height]; -#ifdef CAPTURE_TO_TARGA - //bytes are mixed in targa files, not rgb order. - for (y=0; yUnlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = nullptr; - - Targa targ; - memset(&targ.Header,0,sizeof(targ.Header)); - targ.Header.Width=width; - targ.Header.Height=height; - targ.Header.PixelDepth=24; - targ.Header.ImageType=TGA_TRUECOLOR; - targ.SetImage(image); - targ.YFlip(); - - targ.Save(pathname,TGAF_IMAGE,false); -#else //capturing to bmp file - //bmp is same byte order - for (y=0; yUnlock(); - surfaceCopy->Release_Ref(); - surfaceCopy = nullptr; - - //Flip the image - char *ptr,*ptr1; - char v,v1; - - for (y = 0; y < (height >> 1); y++) - { - /* Compute address of lines to exchange. */ - ptr = (image + ((width * y) * 3)); - ptr1 = (image + ((width * (height - 1)) * 3)); - ptr1 -= ((width * y) * 3); - - /* Exchange all the pixels on this scan line. */ - for (x = 0; x < (width * 3); x++) - { - v = *ptr; - v1 = *ptr1; - *ptr = v1; - *ptr1 = v; - ptr++; - ptr1++; - } - } - CreateBMPFile(pathname, image, width, height); -#endif - - delete [] image; - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); -} -void W3DDisplay::takeScreenShotCompressed(void) -{ - W3D_TakeCompressedScreenshot(SCREENSHOT_JPEG, 80); -} - -void W3DDisplay::takeScreenShotPNG(void) -{ - W3D_TakeCompressedScreenshot(SCREENSHOT_PNG, 0); -} +///Save Screen Capture to a file +#include "W3DScreenshot.cpp" /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture() diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp new file mode 100644 index 0000000000..9a601d1232 --- /dev/null +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -0,0 +1,130 @@ +#include + +struct ScreenshotThreadData +{ + unsigned char* imageData; + unsigned int width; + unsigned int height; + char pathname[_MAX_PATH]; + char leafname[_MAX_FNAME]; + int quality; + ScreenshotFormat format; +}; + +static DWORD WINAPI screenshotThreadFunc(LPVOID param) +{ + ScreenshotThreadData* data = (ScreenshotThreadData*)param; + + int result = 0; + switch (data->format) + { + case SCREENSHOT_JPEG: + result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality); + break; + case SCREENSHOT_PNG: + result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); + break; + } + + if (!result) { + OutputDebugStringA("Failed to write screenshot\n"); + } + + delete [] data->imageData; + delete data; + + return 0; +} + +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) +{ + char leafname[_MAX_FNAME]; + char pathname[_MAX_PATH]; + static int jpegFrameNumber = 1; + static int pngFrameNumber = 1; + + int* frameNumber = (format == SCREENSHOT_JPEG) ? &jpegFrameNumber : &pngFrameNumber; + const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png"; + + Bool done = false; + while (!done) { + sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension); + strcpy(pathname, TheGlobalData->getPath_UserData().str()); + strlcat(pathname, leafname, ARRAY_SIZE(pathname)); + if (_access(pathname, 0) == -1) + done = true; + } + + SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer(); + SurfaceClass::SurfaceDescription surfaceDesc; + surface->Get_Description(surfaceDesc); + + SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); + DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + + surface->Release_Ref(); + surface = NULL; + + struct Rect + { + int Pitch; + void* pBits; + } lrect; + + lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); + if (lrect.pBits == NULL) + { + surfaceCopy->Release_Ref(); + return; + } + + unsigned int x, y, index, index2; + unsigned int width = surfaceDesc.Width; + unsigned int height = surfaceDesc.Height; + + unsigned char* image = new unsigned char[3 * width * height]; + + for (y = 0; y < height; y++) + { + for (x = 0; x < width; x++) + { + index = 3 * (x + y * width); + index2 = y * lrect.Pitch + 4 * x; + + image[index] = *((unsigned char*)lrect.pBits + index2 + 2); + image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1); + image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0); + } + } + + surfaceCopy->Unlock(); + surfaceCopy->Release_Ref(); + surfaceCopy = NULL; + + if (quality <= 0 && format == SCREENSHOT_JPEG) + quality = TheGlobalData->m_jpegQuality; + + ScreenshotThreadData* threadData = new ScreenshotThreadData(); + threadData->imageData = image; + threadData->width = width; + threadData->height = height; + threadData->quality = quality; + threadData->format = format; + strcpy(threadData->pathname, pathname); + strcpy(threadData->leafname, leafname); + + DWORD threadId; + HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + if (hThread) { + CloseHandle(hThread); + } + + UnicodeString ufileName; + ufileName.translate(leafname); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); +} + +void W3DDisplay::takeScreenShot(ScreenshotFormat format) +{ + W3D_TakeCompressedScreenshot(format); +} diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp new file mode 100644 index 0000000000..364368901a --- /dev/null +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp @@ -0,0 +1,21 @@ +/* +** 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 . +*/ + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + diff --git a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index a90791bb23..94cdca5c8a 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -101,8 +101,7 @@ class GUIEditDisplay : public Display virtual void drawScaledVideoBuffer( VideoBuffer *buffer, VideoStreamInterface *stream ) { } virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY, Int endX, Int endY ) { } - virtual void takeScreenShotCompressed(void){ } - virtual void takeScreenShotPNG(void){ } + virtual void takeScreenShot(ScreenshotFormat){ } virtual void toggleMovieCapture(void) {} // methods that we need to stub diff --git a/cmake/stb.cmake b/cmake/stb.cmake index 957c7d235d..8f2078a810 100644 --- a/cmake/stb.cmake +++ b/cmake/stb.cmake @@ -2,16 +2,18 @@ # STB single-file public domain libraries for image encoding # https://github.com/nothings/stb -FetchContent_Declare( - stb - GIT_REPOSITORY https://github.com/nothings/stb.git - GIT_TAG master # Could pin to specific commit for stability - GIT_SHALLOW TRUE -) +find_package(Stb CONFIG QUIET) -FetchContent_MakeAvailable(stb) +if(NOT Stb_FOUND) + include(FetchContent) + FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG 5c205738c191bcb0abc65c4febfa9bd25ff35234 + ) -# Create interface library for stb headers -add_library(stb INTERFACE) -target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) + FetchContent_MakeAvailable(stb) + add_library(stb INTERFACE) + target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) +endif() diff --git a/vcpkg.json b/vcpkg.json index 011b913c8a..9ce3c6667c 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -3,6 +3,7 @@ "builtin-baseline": "b02e341c927f16d991edbd915d8ea43eac52096c", "dependencies": [ "zlib", - "ffmpeg" + "ffmpeg", + "stb" ] } \ No newline at end of file From 148b260b0d3055b14d2f0745d0d2c905399f083b Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 13:10:09 -0500 Subject: [PATCH 09/26] Remove trailing whitespace from empty lines --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 10 +++++----- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 9a601d1232..a091022dd4 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -14,7 +14,7 @@ struct ScreenshotThreadData static DWORD WINAPI screenshotThreadFunc(LPVOID param) { ScreenshotThreadData* data = (ScreenshotThreadData*)param; - + int result = 0; switch (data->format) { @@ -25,14 +25,14 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); break; } - + if (!result) { OutputDebugStringA("Failed to write screenshot\n"); } - + delete [] data->imageData; delete data; - + return 0; } @@ -42,7 +42,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) char pathname[_MAX_PATH]; static int jpegFrameNumber = 1; static int pngFrameNumber = 1; - + int* frameNumber = (format == SCREENSHOT_JPEG) ? &jpegFrameNumber : &pngFrameNumber; const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png"; diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 9a601d1232..a091022dd4 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -14,7 +14,7 @@ struct ScreenshotThreadData static DWORD WINAPI screenshotThreadFunc(LPVOID param) { ScreenshotThreadData* data = (ScreenshotThreadData*)param; - + int result = 0; switch (data->format) { @@ -25,14 +25,14 @@ static DWORD WINAPI screenshotThreadFunc(LPVOID param) result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3); break; } - + if (!result) { OutputDebugStringA("Failed to write screenshot\n"); } - + delete [] data->imageData; delete data; - + return 0; } @@ -42,7 +42,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) char pathname[_MAX_PATH]; static int jpegFrameNumber = 1; static int pngFrameNumber = 1; - + int* frameNumber = (format == SCREENSHOT_JPEG) ? &jpegFrameNumber : &pngFrameNumber; const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png"; From c50941249a745e4658981a84bcd14f9d91596eb8 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 14:10:19 -0500 Subject: [PATCH 10/26] Fix copyright headers: Change Electronic Arts Inc. to TheSuperHackers --- .../Include/W3DDevice/GameClient/W3DScreenshot.h | 2 +- .../Source/W3DDevice/GameClient/stb_image_write_impl.cpp | 2 +- .../Source/W3DDevice/GameClient/stb_image_write_impl.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index fe43aa8032..892952645d 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 Electronic Arts Inc. +** Copyright 2025 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/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp index 364368901a..2264ba2c40 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. +** Copyright 2025 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/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp index 364368901a..2264ba2c40 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. +** Copyright 2025 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 From de4929c83fe63894f3ab370fa98104a0a647b17f Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 14:10:22 -0500 Subject: [PATCH 11/26] Add file headers to W3DScreenshot.cpp files --- .../W3DDevice/GameClient/W3DScreenshot.cpp | 18 ++++++++++++++++++ .../W3DDevice/GameClient/W3DScreenshot.cpp | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index a091022dd4..e5554cb26e 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -1,3 +1,21 @@ +/* +** Command & Conquer Generals(tm) +** Copyright 2025 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 +** 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 . +*/ + #include struct ScreenshotThreadData diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index a091022dd4..e6a5ab2fe4 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -1,3 +1,21 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 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 +** 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 . +*/ + #include struct ScreenshotThreadData From 269af80f7c0ba53662cd24350ddb20810dd0a5c2 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 14:10:27 -0500 Subject: [PATCH 12/26] Rename MSG_META_TAKE_SCREENSHOT_JPEG to MSG_META_TAKE_SCREENSHOT_PNG --- Generals/Code/GameEngine/Include/Common/MessageStream.h | 2 +- Generals/Code/GameEngine/Source/Common/MessageStream.cpp | 2 +- .../Source/GameClient/MessageStream/CommandXlat.cpp | 2 +- .../GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp | 4 ++-- GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h | 2 +- GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp | 2 +- .../Source/GameClient/MessageStream/CommandXlat.cpp | 2 +- .../GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h index 99996c3fd7..2985380d4f 100644 --- a/Generals/Code/GameEngine/Include/Common/MessageStream.h +++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h @@ -258,7 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) - MSG_META_TAKE_SCREENSHOT_JPEG, ///< take PNG screenshot (CTRL+F12, lossless) + MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot (CTRL+F12, lossless) MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp index 2ba36c24fc..27eb52f3b3 100644 --- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,7 +364,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION) CASE_LABEL(MSG_META_END_PREFER_SELECTION) CASE_LABEL(MSG_META_TAKE_SCREENSHOT) - CASE_LABEL(MSG_META_TAKE_SCREENSHOT_JPEG) + CASE_LABEL(MSG_META_TAKE_SCREENSHOT_PNG) CASE_LABEL(MSG_META_ALL_CHEER) CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE) CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT) diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index fbd31cd5a2..73b75e4939 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3422,7 +3422,7 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG: + case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: { if (TheDisplay) TheDisplay->takeScreenShot(SCREENSHOT_PNG); diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 4fdf92e728..051983e651 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -163,7 +163,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, - { "TAKE_SCREENSHOT_JPEG", GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG }, + { "TAKE_SCREENSHOT_PNG", GameMessage::MSG_META_TAKE_SCREENSHOT_PNG }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -825,7 +825,7 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG); + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); if (map->m_key == MK_NONE) { map->m_key = MK_F12; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 42ab138cff..aac0ac630c 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -258,7 +258,7 @@ class GameMessage : public MemoryPoolObject MSG_META_END_PREFER_SELECTION, ///< The Shift key has been released. MSG_META_TAKE_SCREENSHOT, ///< take JPEG screenshot (F12) - MSG_META_TAKE_SCREENSHOT_JPEG, ///< take PNG screenshot (CTRL+F12, lossless) + MSG_META_TAKE_SCREENSHOT_PNG, ///< take PNG screenshot (CTRL+F12, lossless) MSG_META_ALL_CHEER, ///< Yay! :) MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp index 68431b46ad..1e6103de3f 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp @@ -364,7 +364,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_BEGIN_PREFER_SELECTION) CASE_LABEL(MSG_META_END_PREFER_SELECTION) CASE_LABEL(MSG_META_TAKE_SCREENSHOT) - CASE_LABEL(MSG_META_TAKE_SCREENSHOT_JPEG) + CASE_LABEL(MSG_META_TAKE_SCREENSHOT_PNG) CASE_LABEL(MSG_META_ALL_CHEER) CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE) CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 64ffe90687..72f377d4b9 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3755,7 +3755,7 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage break; } - case GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG: + case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG: { if (TheDisplay) TheDisplay->takeScreenShot(SCREENSHOT_PNG); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index b8482ea78a..c84e93f177 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -171,7 +171,7 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "END_PREFER_SELECTION", GameMessage::MSG_META_END_PREFER_SELECTION }, { "TAKE_SCREENSHOT", GameMessage::MSG_META_TAKE_SCREENSHOT }, - { "TAKE_SCREENSHOT_JPEG", GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG }, + { "TAKE_SCREENSHOT_PNG", GameMessage::MSG_META_TAKE_SCREENSHOT_PNG }, { "ALL_CHEER", GameMessage::MSG_META_ALL_CHEER }, { "BEGIN_CAMERA_ROTATE_LEFT", GameMessage::MSG_META_BEGIN_CAMERA_ROTATE_LEFT }, @@ -883,7 +883,7 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) } } { - MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG); + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TAKE_SCREENSHOT_PNG); if (map->m_key == MK_NONE) { map->m_key = MK_F12; From ef67a08c7a8b6030aba6784ce389df2b3d5ffea9 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 11:51:30 -0500 Subject: [PATCH 13/26] fix(screenshot): Fix W3DScreenshot.cpp references in CMakeLists after rebase --- Core/GameEngineDevice/CMakeLists.txt | 2 +- Generals/Code/GameEngineDevice/CMakeLists.txt | 1 + GeneralsMD/Code/GameEngineDevice/CMakeLists.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index f733f19fb2..8d38f0b565 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -174,7 +174,7 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DTreeBuffer.cpp Source/W3DDevice/GameClient/W3DVideoBuffer.cpp Source/W3DDevice/GameClient/W3DView.cpp - Source/W3DDevice/GameClient/W3DScreenshot.cpp +# Source/W3DDevice/GameClient/W3DScreenshot.cpp # Source/W3DDevice/GameClient/W3dWaypointBuffer.cpp # Source/W3DDevice/GameClient/W3DWebBrowser.cpp Source/W3DDevice/GameClient/Water/W3DWater.cpp diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index feb45ced75..1a8b1edcbe 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -204,6 +204,7 @@ target_link_libraries(g_gameenginedevice PRIVATE ) target_sources(g_gameenginedevice PRIVATE + Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 07235a2b9d..13e5a32e6f 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -217,6 +217,7 @@ target_link_libraries(z_gameenginedevice PRIVATE ) target_sources(z_gameenginedevice PRIVATE + Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) From 82f858a3c6c44d96bc5c4e8a962c2363fcae405a Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 11:57:18 -0500 Subject: [PATCH 14/26] fix(screenshot): Add missing W3DScreenshot.h include for ScreenshotFormat enum --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 1 + .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index e5554cb26e..dffda88bae 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,6 +16,7 @@ ** along with this program. If not, see . */ +#include "W3DScreenshot.h" #include struct ScreenshotThreadData diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index e6a5ab2fe4..4ad41ebbba 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,6 +16,7 @@ ** along with this program. If not, see . */ +#include "W3DScreenshot.h" #include struct ScreenshotThreadData From a888657a9bcc27c0a2cd25ad7f1beadc265a3b77 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:01:13 -0500 Subject: [PATCH 15/26] fix(screenshot): Use full include path for W3DScreenshot.h from Core --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 2 +- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index dffda88bae..c378638649 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,7 +16,7 @@ ** along with this program. If not, see . */ -#include "W3DScreenshot.h" +#include "W3DDevice/GameClient/W3DScreenshot.h" #include struct ScreenshotThreadData diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 4ad41ebbba..b3fe609c46 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,7 +16,7 @@ ** along with this program. If not, see . */ -#include "W3DScreenshot.h" +#include "W3DDevice/GameClient/W3DScreenshot.h" #include struct ScreenshotThreadData From d1ed4f1b13db5d43eb634da65c831e054071317e Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:06:23 -0500 Subject: [PATCH 16/26] fix(screenshot): Remove W3DScreenshot.cpp from separate compilation since it's included in W3DDisplay.cpp --- Generals/Code/GameEngineDevice/CMakeLists.txt | 1 - GeneralsMD/Code/GameEngineDevice/CMakeLists.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 1a8b1edcbe..feb45ced75 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -204,7 +204,6 @@ target_link_libraries(g_gameenginedevice PRIVATE ) target_sources(g_gameenginedevice PRIVATE - Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 13e5a32e6f..07235a2b9d 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -217,7 +217,6 @@ target_link_libraries(z_gameenginedevice PRIVATE ) target_sources(z_gameenginedevice PRIVATE - Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) From ea0abcc17998147a7a62b5a1173eaca1e85d554e Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:40:54 -0500 Subject: [PATCH 17/26] refactor(screenshot): Consolidate stb_image_write_impl.cpp to Core Move stb_image_write_impl.cpp from game-specific directories to Core since it's identical between Generals and GeneralsMD and contains only STB library implementation code with no game-specific logic. --- Core/GameEngineDevice/CMakeLists.txt | 1 + .../GameClient/stb_image_write_impl.cpp | 0 Generals/Code/GameEngineDevice/CMakeLists.txt | 4 ++-- .../Code/GameEngineDevice/CMakeLists.txt | 4 ++-- .../GameClient/stb_image_write_impl.cpp | 21 ------------------- 5 files changed, 5 insertions(+), 25 deletions(-) rename {Generals/Code => Core}/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp (100%) delete mode 100644 GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 8d38f0b565..0f9a31299b 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -175,6 +175,7 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DVideoBuffer.cpp Source/W3DDevice/GameClient/W3DView.cpp # Source/W3DDevice/GameClient/W3DScreenshot.cpp + Source/W3DDevice/GameClient/stb_image_write_impl.cpp # Source/W3DDevice/GameClient/W3dWaypointBuffer.cpp # Source/W3DDevice/GameClient/W3DWebBrowser.cpp Source/W3DDevice/GameClient/Water/W3DWater.cpp diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp similarity index 100% rename from Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp rename to Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index feb45ced75..17c96701b6 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -204,11 +204,11 @@ target_link_libraries(g_gameenginedevice PRIVATE ) target_sources(g_gameenginedevice PRIVATE - Source/W3DDevice/GameClient/stb_image_write_impl.cpp +# Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) set_source_files_properties( - Source/W3DDevice/GameClient/stb_image_write_impl.cpp +# Source/W3DDevice/GameClient/stb_image_write_impl.cpp PROPERTIES SKIP_PRECOMPILE_HEADERS ON ) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 07235a2b9d..a645be3327 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -217,11 +217,11 @@ target_link_libraries(z_gameenginedevice PRIVATE ) target_sources(z_gameenginedevice PRIVATE - Source/W3DDevice/GameClient/stb_image_write_impl.cpp +# Source/W3DDevice/GameClient/stb_image_write_impl.cpp ) set_source_files_properties( - Source/W3DDevice/GameClient/stb_image_write_impl.cpp +# Source/W3DDevice/GameClient/stb_image_write_impl.cpp PROPERTIES SKIP_PRECOMPILE_HEADERS ON ) diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp deleted file mode 100644 index 2264ba2c40..0000000000 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 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 -** 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 . -*/ - -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include - From 7d700d7f414fed0250bff83b9cc0963e0499c8c7 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:41:36 -0500 Subject: [PATCH 18/26] docs(unify): Document stb_image_write_impl.cpp unification in script --- scripts/cpp/unify_move_files.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/cpp/unify_move_files.py b/scripts/cpp/unify_move_files.py index 009fe4f7da..a2bf83bc1a 100644 --- a/scripts/cpp/unify_move_files.py +++ b/scripts/cpp/unify_move_files.py @@ -256,6 +256,7 @@ def main(): #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp") #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTreeBuffer.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTreeBuffer.cpp") #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp") + #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp") #unify_file(Game.ZEROHOUR, "GameEngine/Include/Common/UserPreferences.h", Game.CORE, "GameEngine/Include/Common/UserPreferences.h") #unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/UserPreferences.cpp", Game.CORE, "GameEngine/Source/Common/UserPreferences.cpp") From a4de747a64a94e4ae073b488ab5c082a34108966 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 14:46:26 -0500 Subject: [PATCH 19/26] fix(screenshot): Restore DESTROY_MESSAGE, fix quality default, NULL to nullptr, strcpy to strlcpy, fix thread leak --- .../W3DDevice/GameClient/W3DScreenshot.h | 2 +- .../GameClient/MessageStream/CommandXlat.cpp | 1 + .../W3DDevice/GameClient/W3DScreenshot.cpp | 19 +++++++++++-------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index 892952645d..01b0403083 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h @@ -20,5 +20,5 @@ #include "GameClient/Display.h" -void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 80); +void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 0); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 72f377d4b9..2d511e3dca 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3752,6 +3752,7 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage { if (TheDisplay) TheDisplay->takeScreenShot(SCREENSHOT_JPEG); + disp = DESTROY_MESSAGE; break; } diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index b3fe609c46..9aaac56d3e 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -68,7 +68,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) Bool done = false; while (!done) { sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension); - strcpy(pathname, TheGlobalData->getPath_UserData().str()); + strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); strlcat(pathname, leafname, ARRAY_SIZE(pathname)); if (_access(pathname, 0) == -1) done = true; @@ -79,10 +79,10 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surface->Get_Description(surfaceDesc); SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); - DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); surface->Release_Ref(); - surface = NULL; + surface = nullptr; struct Rect { @@ -91,7 +91,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) } lrect; lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == NULL) + if (lrect.pBits == nullptr) { surfaceCopy->Release_Ref(); return; @@ -118,7 +118,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surfaceCopy->Unlock(); surfaceCopy->Release_Ref(); - surfaceCopy = NULL; + surfaceCopy = nullptr; if (quality <= 0 && format == SCREENSHOT_JPEG) quality = TheGlobalData->m_jpegQuality; @@ -129,13 +129,16 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) threadData->height = height; threadData->quality = quality; threadData->format = format; - strcpy(threadData->pathname, pathname); - strcpy(threadData->leafname, leafname); + strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); + strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname)); DWORD threadId; - HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); if (hThread) { CloseHandle(hThread); + } else { + delete [] threadData->imageData; + delete threadData; } UnicodeString ufileName; From ce0362794724ec6ecdaa707f3ff1ceead13dbc10 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 14:46:35 -0500 Subject: [PATCH 20/26] fix(screenshot): Replicate screenshot fixes to Generals --- .../GameClient/MessageStream/CommandXlat.cpp | 1 + .../W3DDevice/GameClient/W3DScreenshot.cpp | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 73b75e4939..bb43a763ff 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -3419,6 +3419,7 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage { if (TheDisplay) TheDisplay->takeScreenShot(SCREENSHOT_JPEG); + disp = DESTROY_MESSAGE; break; } diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index c378638649..715834345c 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -68,7 +68,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) Bool done = false; while (!done) { sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension); - strcpy(pathname, TheGlobalData->getPath_UserData().str()); + strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname)); strlcat(pathname, leafname, ARRAY_SIZE(pathname)); if (_access(pathname, 0) == -1) done = true; @@ -79,10 +79,10 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surface->Get_Description(surfaceDesc); SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format))); - DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL); + DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), nullptr, 0, surfaceCopy->Peek_D3D_Surface(), nullptr); surface->Release_Ref(); - surface = NULL; + surface = nullptr; struct Rect { @@ -91,7 +91,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) } lrect; lrect.pBits = surfaceCopy->Lock(&lrect.Pitch); - if (lrect.pBits == NULL) + if (lrect.pBits == nullptr) { surfaceCopy->Release_Ref(); return; @@ -118,7 +118,7 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) surfaceCopy->Unlock(); surfaceCopy->Release_Ref(); - surfaceCopy = NULL; + surfaceCopy = nullptr; if (quality <= 0 && format == SCREENSHOT_JPEG) quality = TheGlobalData->m_jpegQuality; @@ -129,13 +129,16 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) threadData->height = height; threadData->quality = quality; threadData->format = format; - strcpy(threadData->pathname, pathname); - strcpy(threadData->leafname, leafname); + strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname)); + strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname)); DWORD threadId; - HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId); + HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); if (hThread) { CloseHandle(hThread); + } else { + delete [] threadData->imageData; + delete threadData; } UnicodeString ufileName; From e62f2c2e6899fa7d9295df25eef3d033bdf4383d Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 15:04:19 -0500 Subject: [PATCH 21/26] refactor(screenshot): Compile W3DScreenshot.cpp as separate translation unit instead of #including it --- GeneralsMD/Code/GameEngineDevice/CMakeLists.txt | 1 + .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 1 - .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 6 ++++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index a645be3327..2b669e8494 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -150,6 +150,7 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DDebugDisplay.cpp Source/W3DDevice/GameClient/W3DDebugIcons.cpp Source/W3DDevice/GameClient/W3DDisplay.cpp + Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/W3DDisplayString.cpp Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp Source/W3DDevice/GameClient/W3DDynamicLight.cpp diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 118e462dba..860be62299 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2992,7 +2992,6 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) } ///Save Screen Capture to a file -#include "W3DScreenshot.cpp" /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture() diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 9aaac56d3e..0c20405f84 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,7 +16,13 @@ ** along with this program. If not, see . */ +#include + #include "W3DDevice/GameClient/W3DScreenshot.h" +#include "W3DDevice/GameClient/W3DDisplay.h" +#include "Common/GlobalData.h" +#include "GameClient/GameText.h" +#include "GameClient/InGameUI.h" #include struct ScreenshotThreadData From 30b405db06b47d947b4a9c19d27e423e55770ef7 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 15:04:34 -0500 Subject: [PATCH 22/26] refactor(screenshot): Replicate separate compilation fix to Generals --- Generals/Code/GameEngineDevice/CMakeLists.txt | 1 + .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 1 - .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 8 ++++++++ .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 2 ++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index 17c96701b6..a4a97990cf 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -139,6 +139,7 @@ set(GAMEENGINEDEVICE_SRC Source/W3DDevice/GameClient/W3DDebugDisplay.cpp Source/W3DDevice/GameClient/W3DDebugIcons.cpp Source/W3DDevice/GameClient/W3DDisplay.cpp + Source/W3DDevice/GameClient/W3DScreenshot.cpp Source/W3DDevice/GameClient/W3DDisplayString.cpp Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp Source/W3DDevice/GameClient/W3DDynamicLight.cpp diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 006b86f0ea..94597a1728 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2880,7 +2880,6 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) } ///Save Screen Capture to a file -#include "W3DScreenshot.cpp" /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture() diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 715834345c..0961fd6163 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -16,7 +16,15 @@ ** along with this program. If not, see . */ +#include + #include "W3DDevice/GameClient/W3DScreenshot.h" +#include "W3DDevice/GameClient/W3DDisplay.h" +#include "Common/GlobalData.h" +#include "GameClient/GameText.h" +#include "GameClient/InGameUI.h" +#include "WW3D2/dx8wrapper.h" +#include "WW3D2/surfaceclass.h" #include struct ScreenshotThreadData diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 0c20405f84..05d30bc67f 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -23,6 +23,8 @@ #include "Common/GlobalData.h" #include "GameClient/GameText.h" #include "GameClient/InGameUI.h" +#include "WW3D2/dx8wrapper.h" +#include "WW3D2/surfaceclass.h" #include struct ScreenshotThreadData From b50793c83155378ddf168d2021ab156426a8c3a5 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 16:50:31 -0500 Subject: [PATCH 23/26] fix(screenshot): Initialize m_jpegQuality in constructor and remove empty CMake blocks --- .../Code/GameEngine/Source/Common/GlobalData.cpp | 1 + GeneralsMD/Code/GameEngineDevice/CMakeLists.txt | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 2781811f7f..5e5e2ddcf5 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -649,6 +649,7 @@ GlobalData::GlobalData() m_enableDynamicLOD = TRUE; m_enableStaticLOD = TRUE; m_rightMouseAlwaysScrolls = FALSE; + m_jpegQuality = 80; m_useWaterPlane = FALSE; m_useCloudPlane = FALSE; m_downwindAngle = ( -0.785f );//Northeast! diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index 2b669e8494..3913422abb 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -217,16 +217,6 @@ target_link_libraries(z_gameenginedevice PRIVATE stb ) -target_sources(z_gameenginedevice PRIVATE -# Source/W3DDevice/GameClient/stb_image_write_impl.cpp -) - -set_source_files_properties( -# Source/W3DDevice/GameClient/stb_image_write_impl.cpp - PROPERTIES - SKIP_PRECOMPILE_HEADERS ON -) - target_link_libraries(z_gameenginedevice PUBLIC corei_gameenginedevice_public z_gameengine From c339280bbc842095b320eabd4b8b146dbcb069df Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 16 Feb 2026 16:50:43 -0500 Subject: [PATCH 24/26] fix(screenshot): Replicate m_jpegQuality init and CMake cleanup to Generals --- Generals/Code/GameEngine/Source/Common/GlobalData.cpp | 1 + Generals/Code/GameEngineDevice/CMakeLists.txt | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index f1024e32a0..6ed2881797 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -645,6 +645,7 @@ GlobalData::GlobalData() m_enableDynamicLOD = TRUE; m_enableStaticLOD = TRUE; m_rightMouseAlwaysScrolls = FALSE; + m_jpegQuality = 80; m_useWaterPlane = FALSE; m_useCloudPlane = FALSE; m_downwindAngle = ( -0.785f );//Northeast! diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index a4a97990cf..b060675980 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -204,16 +204,6 @@ target_link_libraries(g_gameenginedevice PRIVATE stb ) -target_sources(g_gameenginedevice PRIVATE -# Source/W3DDevice/GameClient/stb_image_write_impl.cpp -) - -set_source_files_properties( -# Source/W3DDevice/GameClient/stb_image_write_impl.cpp - PROPERTIES - SKIP_PRECOMPILE_HEADERS ON -) - target_link_libraries(g_gameenginedevice PUBLIC corei_gameenginedevice_public g_gameengine From 20a9eb28fb25fcceb865e282788e405ca962f1b2 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Tue, 24 Feb 2026 13:05:54 -0500 Subject: [PATCH 25/26] Only show screen capture message when screenshot thread is successfully created --- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 8 ++++---- .../Source/W3DDevice/GameClient/W3DScreenshot.cpp | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 0961fd6163..b1fc685f1d 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -144,14 +144,14 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); if (hThread) { CloseHandle(hThread); + + UnicodeString ufileName; + ufileName.translate(leafname); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } else { delete [] threadData->imageData; delete threadData; } - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } void W3DDisplay::takeScreenShot(ScreenshotFormat format) diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp index 05d30bc67f..1526ec921a 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp @@ -144,14 +144,14 @@ void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality) HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId); if (hThread) { CloseHandle(hThread); + + UnicodeString ufileName; + ufileName.translate(leafname); + TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } else { delete [] threadData->imageData; delete threadData; } - - UnicodeString ufileName; - ufileName.translate(leafname); - TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str()); } void W3DDisplay::takeScreenShot(ScreenshotFormat format) From b709c32b12ae009008b2e06011438fa281250297 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Tue, 24 Feb 2026 13:11:33 -0500 Subject: [PATCH 26/26] Remove dead CreateBMPFile function replaced by compressed screenshot --- .../W3DDevice/GameClient/W3DDisplay.cpp | 79 ------------------- .../W3DDevice/GameClient/W3DDisplay.cpp | 79 ------------------- 2 files changed, 158 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 94597a1728..f631f037e9 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2802,85 +2802,6 @@ void W3DDisplay::setShroudLevel( Int x, Int y, CellShroudStatus setting ) } } -//============================================================================= -///Utility function to dump data into a .BMP file -static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) -{ - HANDLE hf; // file handle - BITMAPFILEHEADER hdr; // bitmap file-header - PBITMAPINFOHEADER pbih; // bitmap info-header - LPBYTE lpBits; // memory pointer - DWORD dwTotal; // total count of bytes - DWORD cb; // incremental count of bytes - BYTE *hp; // byte pointer - DWORD dwTmp; - - PBITMAPINFO pbmi; - - pbmi = (PBITMAPINFO) LocalAlloc(LPTR,sizeof(BITMAPINFOHEADER)); - if (pbmi == nullptr) - return; - - pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - pbmi->bmiHeader.biWidth = width; - pbmi->bmiHeader.biHeight = height; - pbmi->bmiHeader.biPlanes = 1; - pbmi->bmiHeader.biBitCount = 24; - pbmi->bmiHeader.biCompression = BI_RGB; - pbmi->bmiHeader.biSizeImage = (pbmi->bmiHeader.biWidth + 7) /8 * pbmi->bmiHeader.biHeight * 24; - pbmi->bmiHeader.biClrImportant = 0; - - pbih = (PBITMAPINFOHEADER) pbmi; - lpBits = (LPBYTE) image; - - // Create the .BMP file. - hf = CreateFile(pszFile, - GENERIC_READ | GENERIC_WRITE, - (DWORD) 0, - nullptr, - CREATE_ALWAYS, - FILE_ATTRIBUTE_NORMAL, - (HANDLE) nullptr); - - if (hf != INVALID_HANDLE_VALUE) - { - hdr.bfType = 0x4d42; // 0x42 = "B" 0x4d = "M" - // Compute the size of the entire file. - hdr.bfSize = (DWORD) (sizeof(BITMAPFILEHEADER) + - pbih->biSize + pbih->biClrUsed - * sizeof(RGBQUAD) + pbih->biSizeImage); - hdr.bfReserved1 = 0; - hdr.bfReserved2 = 0; - - // Compute the offset to the array of color indices. - hdr.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) + - pbih->biSize + pbih->biClrUsed - * sizeof (RGBQUAD); - - // Copy the BITMAPFILEHEADER into the .BMP file. - if (WriteFile(hf, (LPVOID) &hdr, sizeof(BITMAPFILEHEADER), - (LPDWORD) &dwTmp, nullptr)) - { - // Copy the BITMAPINFOHEADER and RGBQUAD array into the file. - if (WriteFile(hf, (LPVOID) pbih, sizeof(BITMAPINFOHEADER) + pbih->biClrUsed * sizeof (RGBQUAD),(LPDWORD) &dwTmp, nullptr)) - { - // Copy the array of color indices into the .BMP file. - dwTotal = cb = pbih->biSizeImage; - hp = lpBits; - WriteFile(hf, (LPSTR) hp, (int) cb, (LPDWORD) &dwTmp, nullptr); - } - } - - // Close the .BMP file. - CloseHandle(hf); - } - - // Free memory. - LocalFree( (HLOCAL) pbmi); -} - -///Save Screen Capture to a file - /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture() { diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 860be62299..396862181b 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2914,85 +2914,6 @@ void W3DDisplay::setShroudLevel( Int x, Int y, CellShroudStatus setting ) } } -//============================================================================= -///Utility function to dump data into a .BMP file -static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) -{ - HANDLE hf; // file handle - BITMAPFILEHEADER hdr; // bitmap file-header - PBITMAPINFOHEADER pbih; // bitmap info-header - LPBYTE lpBits; // memory pointer - DWORD dwTotal; // total count of bytes - DWORD cb; // incremental count of bytes - BYTE *hp; // byte pointer - DWORD dwTmp; - - PBITMAPINFO pbmi; - - pbmi = (PBITMAPINFO) LocalAlloc(LPTR,sizeof(BITMAPINFOHEADER)); - if (pbmi == nullptr) - return; - - pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - pbmi->bmiHeader.biWidth = width; - pbmi->bmiHeader.biHeight = height; - pbmi->bmiHeader.biPlanes = 1; - pbmi->bmiHeader.biBitCount = 24; - pbmi->bmiHeader.biCompression = BI_RGB; - pbmi->bmiHeader.biSizeImage = (pbmi->bmiHeader.biWidth + 7) /8 * pbmi->bmiHeader.biHeight * 24; - pbmi->bmiHeader.biClrImportant = 0; - - pbih = (PBITMAPINFOHEADER) pbmi; - lpBits = (LPBYTE) image; - - // Create the .BMP file. - hf = CreateFile(pszFile, - GENERIC_READ | GENERIC_WRITE, - (DWORD) 0, - nullptr, - CREATE_ALWAYS, - FILE_ATTRIBUTE_NORMAL, - (HANDLE) nullptr); - - if (hf != INVALID_HANDLE_VALUE) - { - hdr.bfType = 0x4d42; // 0x42 = "B" 0x4d = "M" - // Compute the size of the entire file. - hdr.bfSize = (DWORD) (sizeof(BITMAPFILEHEADER) + - pbih->biSize + pbih->biClrUsed - * sizeof(RGBQUAD) + pbih->biSizeImage); - hdr.bfReserved1 = 0; - hdr.bfReserved2 = 0; - - // Compute the offset to the array of color indices. - hdr.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) + - pbih->biSize + pbih->biClrUsed - * sizeof (RGBQUAD); - - // Copy the BITMAPFILEHEADER into the .BMP file. - if (WriteFile(hf, (LPVOID) &hdr, sizeof(BITMAPFILEHEADER), - (LPDWORD) &dwTmp, nullptr)) - { - // Copy the BITMAPINFOHEADER and RGBQUAD array into the file. - if (WriteFile(hf, (LPVOID) pbih, sizeof(BITMAPINFOHEADER) + pbih->biClrUsed * sizeof (RGBQUAD),(LPDWORD) &dwTmp, nullptr)) - { - // Copy the array of color indices into the .BMP file. - dwTotal = cb = pbih->biSizeImage; - hp = lpBits; - WriteFile(hf, (LPSTR) hp, (int) cb, (LPDWORD) &dwTmp, nullptr); - } - } - - // Close the .BMP file. - CloseHandle(hf); - } - - // Free memory. - LocalFree( (HLOCAL) pbmi); -} - -///Save Screen Capture to a file - /** Start/Stop capturing an AVI movie*/ void W3DDisplay::toggleMovieCapture() {