From e812fdccfafc407b180a9d37d449a73ecc8d69d0 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:15 -0500 Subject: [PATCH 01/18] 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 4160c918c74..7256433de2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,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 00000000000..957c7d235d0 --- /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 cace3215765c5d12f8f92614b5759ec9fa7d704a Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:22 -0500 Subject: [PATCH 02/18] 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 8c5e6c188eb..cfe0a3ce999 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 1f213388e99..f95009a9cea 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(void) = 0; ///< saves screenshot to a file + virtual void takeScreenShotCompressed(void) = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 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 c11407a85ee..c283005b7ef 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 5bcee74c6dd..6d6a3dae675 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 c1dbae122eb..c9102fd1f5a 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 b0ae5db5e4a..f81e04c09f4 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 352a2517d86..dae44c8054f 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(void) = 0; ///< saves screenshot to a file + virtual void takeScreenShotCompressed(void) = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 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 e4b39574979..bb21d4f3eef 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 140e50322cf..ed8c66c0015 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 9d9df747c00..92d405b2506 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 74595040974f313b80dde3cd5c3e9eefac7a4200 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 11:23:34 -0500 Subject: [PATCH 03/18] 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 cfe0a3ce999..883fd57d0f8 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 f95009a9cea..1e8826ef5c0 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(void) = 0; ///< saves screenshot to a file - virtual void takeScreenShotCompressed(void) = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling + virtual void takeScreenShotCompressed(void) = 0; ///< saves compressed screenshot without stalling virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 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 6d6a3dae675..441df9ac256 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 ca1022e3e61..3cc1a64aaed 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -202,6 +202,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 472d9d31bcd..4afd44ab8f3 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( void ) ; ///< Create a video buffer that can be used for this display virtual void takeScreenShot(void); //save screenshot to file + virtual void takeScreenShotCompressed(void); //save compressed screenshot to file (JPG/PNG) without stalling virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); /// #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" @@ -3022,6 +3028,103 @@ void W3DDisplay::takeScreenShot(void) 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(void) { diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index f81e04c09f4..3a56370751c 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 dae44c8054f..b63126192cc 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(void) = 0; ///< saves screenshot to a file - virtual void takeScreenShotCompressed(void) = 0; ///< TheSuperHackers @bobtista saves compressed screenshot (JPG/PNG) without stalling + virtual void takeScreenShotCompressed(void) = 0; ///< saves compressed screenshot without stalling virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 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 ed8c66c0015..9af1cd34678 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 db9be4f3cdc..4f9f57c8473 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -215,6 +215,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 7f5ad9f1748..f5916e202ae 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( void ) ; ///< Create a video buffer that can be used for this display virtual void takeScreenShot(void); //save screenshot to file + virtual void takeScreenShotCompressed(void); //save compressed screenshot to file (JPG/PNG) without stalling virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); /// #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" @@ -3134,6 +3140,103 @@ void W3DDisplay::takeScreenShot(void) 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(void) { From 1bd3ab44943543bda3def8441879798b6fdc5755 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sun, 2 Nov 2025 23:29:47 -0500 Subject: [PATCH 04/18] 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 73715351746..7eff375034e 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(void); #include #include #include -#include -#include -// TheSuperHackers @bobtista 02/11/2025 STB for image encoding #define STB_IMAGE_WRITE_IMPLEMENTATION #include @@ -3028,12 +3025,33 @@ void W3DDisplay::takeScreenShot(void) 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; @@ -3047,7 +3065,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); @@ -3075,13 +3092,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++) @@ -3089,9 +3101,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); } } @@ -3099,29 +3111,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 0a0cca6e32c..3e99729744d 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 c2f4c323847..ddbb78b40f8 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(void); #include #include #include -#include -#include -// TheSuperHackers @bobtista 02/11/2025 STB for image encoding #define STB_IMAGE_WRITE_IMPLEMENTATION #include @@ -3140,12 +3137,33 @@ void W3DDisplay::takeScreenShot(void) 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; @@ -3159,7 +3177,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); @@ -3187,13 +3204,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++) @@ -3201,9 +3213,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); } } @@ -3211,29 +3223,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 45356e4ee16..532bee4a4df 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 918315c8485471d819794277a8be7a9328278c6f Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:04:47 -0500 Subject: [PATCH 05/18] 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 7ebd9b02944..90e85b9e6c0 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 @@ -174,6 +175,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 @@ -220,6 +222,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 00000000000..234560af0fe --- /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 00000000000..08a37c9b9d6 --- /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 7eff375034e..d1a62cd55d5 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(void); #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" @@ -3025,108 +3023,9 @@ void W3DDisplay::takeScreenShot(void) 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 ddbb78b40f8..99a35d0c4c0 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(void); #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" @@ -3137,108 +3135,9 @@ void W3DDisplay::takeScreenShot(void) 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 9f4fdd562b0faf08f8a782c4725d765d47d042b4 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:12:09 -0500 Subject: [PATCH 06/18] 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 883fd57d0f8..74b3c557e6c 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 1e8826ef5c0..b31c3f3e51d 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(void) = 0; ///< saves screenshot to a file - virtual void takeScreenShotCompressed(void) = 0; ///< saves compressed screenshot without stalling + virtual void takeScreenShotCompressed(void) = 0; ///< saves JPEG screenshot + virtual void takeScreenShotPNG(void) = 0; ///< saves PNG screenshot virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 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 c283005b7ef..b410f505da9 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 441df9ac256..94869c23ed8 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 c9102fd1f5a..e700febc5df 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 4afd44ab8f3..e1c4636ff0a 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( void ) ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShot(void); //save screenshot to file - virtual void takeScreenShotCompressed(void); //save compressed screenshot to file (JPG/PNG) without stalling + virtual void takeScreenShotCompressed(void); //save JPEG screenshot + virtual void takeScreenShotPNG(void); //save PNG screenshot virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); ///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 92d405b2506..2a62398be19 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 f5916e202ae..a37dcf9486e 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( void ) ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShot(void); //save screenshot to file - virtual void takeScreenShotCompressed(void); //save compressed screenshot to file (JPG/PNG) without stalling + virtual void takeScreenShotCompressed(void); //save JPEG screenshot + virtual void takeScreenShotPNG(void); //save PNG screenshot virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); /// Date: Mon, 3 Nov 2025 11:13:05 -0500 Subject: [PATCH 07/18] Remove old BMP screenshot code --- .../W3DDevice/GameClient/W3DDisplay.cpp | 138 ------------------ .../W3DDevice/GameClient/W3DDisplay.cpp | 138 ------------------ 2 files changed, 276 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index d2ee67065ef..7b02b377f5d 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2885,144 +2885,6 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) LocalFree( (HLOCAL) pbmi); } -///Save Screen Capture to a file -void W3DDisplay::takeScreenShot(void) -{ - char leafname[256]; - char pathname[1024]; - - static int frame_number = 1; - - Bool done = false; - while (!done) { -#ifdef CAPTURE_TO_TARGA - sprintf( leafname, "%s%.3d.tga", "sshot", frame_number++); -#else - sprintf( leafname, "%s%.3d.bmp", "sshot", frame_number++); -#endif - strlcpy(pathname, TheGlobalData->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); diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 5513ae834a4..1b281dfc964 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -2997,144 +2997,6 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height) LocalFree( (HLOCAL) pbmi); } -///Save Screen Capture to a file -void W3DDisplay::takeScreenShot(void) -{ - char leafname[256]; - char pathname[1024]; - - static int frame_number = 1; - - Bool done = false; - while (!done) { -#ifdef CAPTURE_TO_TARGA - sprintf( leafname, "%s%.3d.tga", "sshot", frame_number++); -#else - sprintf( leafname, "%s%.3d.bmp", "sshot", frame_number++); -#endif - strlcpy(pathname, TheGlobalData->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); From 67b66bed57b912e88f5bb37623242999a34340e7 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Mon, 3 Nov 2025 11:15:41 -0500 Subject: [PATCH 08/18] 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 --- .../W3DDevice/GameClient/W3DScreenshot.h | 6 +- .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Include/Common/MessageStream.h | 4 +- .../Include/Common/UserPreferences.h | 1 + .../GameEngine/Include/GameClient/Display.h | 9 +- .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../Source/Common/MessageStream.cpp | 2 +- .../GUI/GUICallbacks/Menus/OptionsMenu.cpp | 11 ++ .../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 | 11 +- .../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 +- .../Include/Common/UserPreferences.h | 1 + .../GameEngine/Include/GameClient/Display.h | 9 +- .../GameEngine/Source/Common/GlobalData.cpp | 1 + .../Source/Common/MessageStream.cpp | 2 +- .../GUI/GUICallbacks/Menus/OptionsMenu.cpp | 12 ++ .../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 | 11 +- .../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 +- 33 files changed, 312 insertions(+), 102 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/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h index 234560af0fe..fe43aa8032c 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 cb7c0d1dda5..8947a58c033 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -139,6 +139,7 @@ class GlobalData : public SubsystemInterface Int m_terrainLODTargetTimeMS; Bool m_useAlternateMouse; 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 74b3c557e6c..727f2ebc147 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/Common/UserPreferences.h b/Generals/Code/GameEngine/Include/Common/UserPreferences.h index aef49361d36..175ce1eddd8 100644 --- a/Generals/Code/GameEngine/Include/Common/UserPreferences.h +++ b/Generals/Code/GameEngine/Include/Common/UserPreferences.h @@ -91,6 +91,7 @@ class OptionPreferences : public UserPreferences void setOnlineIPAddress(UnsignedInt IP); // convenience function Bool getArchiveReplaysEnabled() const; // convenience function Bool getAlternateMouseModeEnabled(void); // convenience function + Int getJPEGQuality(void); // convenience function Real getScrollFactor(void); // convenience function Bool getDrawScrollAnchor(void); Bool getMoveScrollAnchor(void); diff --git a/Generals/Code/GameEngine/Include/GameClient/Display.h b/Generals/Code/GameEngine/Include/GameClient/Display.h index b31c3f3e51d..85508c8e5ec 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(void) = 0; ///< saves JPEG screenshot - virtual void takeScreenShotPNG(void) = 0; ///< saves PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format) = 0; ///< saves screenshot in specified format virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 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 cf0aff23aa7..6f1c6b61e99 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1181,6 +1181,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) // override INI values with user preferences OptionPreferences optionPref; TheWritableGlobalData->m_useAlternateMouse = optionPref.getAlternateMouseModeEnabled(); + 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 b410f505da9..97b7f8cb57d 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/GUI/GUICallbacks/Menus/OptionsMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp index d6fc89193c7..cc38f892621 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp @@ -330,6 +330,17 @@ Bool OptionPreferences::getAlternateMouseModeEnabled(void) return FALSE; } +Int OptionPreferences::getJPEGQuality(void) +{ + 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(void) { diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 94869c23ed8..da5bee607e4 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 e700febc5df..4fdf92e728a 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 3cc1a64aaed..ca5c15ba034 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -205,6 +205,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 e1c4636ff0a..01c341ab19c 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( void ) ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShotCompressed(void); //save JPEG screenshot - virtual void takeScreenShotPNG(void); //save PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format); //save screenshot in specified format virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); ///. -*/ - -#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 00000000000..364368901a5 --- /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 16ab5ec912e..dc950f713cf 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 0ffbd28506b..0e97bc3434f 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 9f7fd14c77a..22ee2acd821 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/Common/UserPreferences.h b/GeneralsMD/Code/GameEngine/Include/Common/UserPreferences.h index 7936cfd8ee6..324918566c4 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/UserPreferences.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/UserPreferences.h @@ -93,6 +93,7 @@ class OptionPreferences : public UserPreferences Bool getAlternateMouseModeEnabled(void); // convenience function Bool getRetaliationModeEnabled(); // convenience function Bool getDoubleClickAttackMoveEnabled(void); // convenience function + Int getJPEGQuality(void); // convenience function Real getScrollFactor(void); // convenience function Bool getDrawScrollAnchor(void); Bool getMoveScrollAnchor(void); diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h index 21e102fb658..55dfccde442 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(void) = 0; ///< saves JPEG screenshot - virtual void takeScreenShotPNG(void) = 0; ///< saves PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format) = 0; ///< saves screenshot in specified format virtual void toggleMovieCapture(void) = 0; ///< starts saving frames to an avi or frame sequence virtual void toggleLetterBox(void) = 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 e3e7b831f8d..cb753ba5ef0 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1208,6 +1208,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 1b9bd87596b..d2b381312e2 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/GUI/GUICallbacks/Menus/OptionsMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp index f8f484e17dc..f83110d6a2f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp @@ -363,6 +363,18 @@ Bool OptionPreferences::getDoubleClickAttackMoveEnabled(void) return FALSE; } +Int OptionPreferences::getJPEGQuality(void) +{ + 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(void) { OptionPreferences::const_iterator it = find("ScrollFactor"); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 75fb992372d..14242353778 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 2a62398be19..b8482ea78a0 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 4f9f57c8473..5b5c0aad178 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -218,6 +218,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 a37dcf9486e..df14b0480ae 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( void ) ; ///< Create a video buffer that can be used for this display - virtual void takeScreenShotCompressed(void); //save JPEG screenshot - virtual void takeScreenShotPNG(void); //save PNG screenshot + virtual void takeScreenShot(ScreenshotFormat format); //save screenshot in specified format virtual void toggleMovieCapture(void); //enable AVI or frame capture mode. virtual void toggleLetterBox(void); /// + +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 00000000000..364368901a5 --- /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 fd33fa7a256..0e37208799b 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 957c7d235d0..8f2078a8106 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 011b913c8aa..9ce3c6667c3 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 9f1d2f3d93c30c154be7f012e6252a1bdfbe0a9f Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 13:10:09 -0500 Subject: [PATCH 09/18] 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 9a601d12327..a091022dd43 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 9a601d12327..a091022dd43 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 3e50e1b7485d94a1263b5f3de86a53c1b2eaf460 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 14:10:19 -0500 Subject: [PATCH 10/18] 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 fe43aa8032c..892952645d7 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 364368901a5..2264ba2c403 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 364368901a5..2264ba2c403 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 0f5e63d2187635f163e7f320c1d476ce35ef9322 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 14:10:22 -0500 Subject: [PATCH 11/18] 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 a091022dd43..e5554cb26e2 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 a091022dd43..e6a5ab2fe4e 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 cbce4f7d2f3345cd71dd2c07e00ab0dd9be578ea Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Sat, 22 Nov 2025 14:10:27 -0500 Subject: [PATCH 12/18] 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 727f2ebc147..4506c1dc1f1 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 97b7f8cb57d..b410f505da9 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 da5bee607e4..60ba9a25eda 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 4fdf92e728a..051983e6513 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 22ee2acd821..e5ba50251dd 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 d2b381312e2..1b9bd87596b 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 14242353778..a804f86eab4 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 b8482ea78a0..c84e93f177f 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 0f506db8a5a66c51bc731ed1346ce816ae7eecb0 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 11:51:30 -0500 Subject: [PATCH 13/18] 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 90e85b9e6c0..89a9783ddb9 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -175,7 +175,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 ca5c15ba034..067d9085d81 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -206,6 +206,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 5b5c0aad178..164e9e9ac81 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -219,6 +219,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 34eb7970bf26655c11057a03a362a6d0e05f672e Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 11:57:18 -0500 Subject: [PATCH 14/18] 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 e5554cb26e2..dffda88bae7 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 e6a5ab2fe4e..4ad41ebbba8 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 c7b3d910a400dceb38975ab15e694701d4a37fa6 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:01:13 -0500 Subject: [PATCH 15/18] 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 dffda88bae7..c3786386497 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 4ad41ebbba8..b3fe609c467 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 ffaad2563af92c143986a06e48996739a5111007 Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:06:23 -0500 Subject: [PATCH 16/18] 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 067d9085d81..ca5c15ba034 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -206,7 +206,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 164e9e9ac81..5b5c0aad178 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -219,7 +219,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 e82581032944ce9ef2b9aca6d630e146ad4053dc Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:40:54 -0500 Subject: [PATCH 17/18] 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 89a9783ddb9..f5f66ba21f0 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -176,6 +176,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 ca5c15ba034..8674993198e 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -206,11 +206,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 5b5c0aad178..d018a98c59b 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -219,11 +219,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 2264ba2c403..00000000000 --- 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 7d40c120b7a152dc1214746a67442e411fc13bed Mon Sep 17 00:00:00 2001 From: Bobby Battista Date: Wed, 3 Dec 2025 12:41:36 -0500 Subject: [PATCH 18/18] 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 e2757328ad9..19f4df69c3c 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") return