diff --git a/CMakeLists.txt b/CMakeLists.txt
index 28ce09560e9..39062e3fbe6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -65,6 +65,7 @@ endif()
include(cmake/config.cmake)
include(cmake/gamespy.cmake)
include(cmake/lzhl.cmake)
+include(cmake/stb.cmake)
if (IS_VS6_BUILD)
# The original max sdk does not compile against a modern compiler.
diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h
index 8448ccd1a14..3abcbb9a0aa 100644
--- a/Core/GameEngine/Include/Common/OptionPreferences.h
+++ b/Core/GameEngine/Include/Common/OptionPreferences.h
@@ -72,6 +72,7 @@ class OptionPreferences : public UserPreferences
Bool getRightMouseScrollWithAlternateMouseEnabled() const;
Bool getRetaliationModeEnabled();
Bool getDoubleClickAttackMoveEnabled();
+ Int getJPEGQuality();
Real getScrollFactor();
Bool getDrawScrollAnchor();
Bool getMoveScrollAnchor();
diff --git a/Core/GameEngine/Include/GameClient/Display.h b/Core/GameEngine/Include/GameClient/Display.h
index 8c022244206..e7bf9e465e7 100644
--- a/Core/GameEngine/Include/GameClient/Display.h
+++ b/Core/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.
@@ -172,7 +178,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 takeScreenShot() = 0; ///< saves screenshot to a file
+ virtual void takeScreenShot(ScreenshotFormat format) = 0; ///< saves screenshot in specified format
virtual void toggleMovieCapture() = 0; ///< starts saving frames to an avi or frame sequence
virtual void toggleLetterBox() = 0; ///< enabled letter-boxed display
virtual void enableLetterBox(Bool enable) = 0; ///< forces letter-boxed display on/off
diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp
index bab11cb7c76..755b4b0c439 100644
--- a/Core/GameEngine/Source/Common/OptionPreferences.cpp
+++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp
@@ -239,6 +239,16 @@ Bool OptionPreferences::getDoubleClickAttackMoveEnabled()
return FALSE;
}
+Int OptionPreferences::getJPEGQuality()
+{
+ OptionPreferences::const_iterator it = find("JPEGQuality");
+ if (it == end())
+ return 80;
+
+ Int quality = atoi(it->second.str());
+ return clamp(1, quality, 100);
+}
+
Real OptionPreferences::getScrollFactor()
{
OptionPreferences::const_iterator it = find("ScrollFactor");
diff --git a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
index ac77d3c6017..6410806cb9c 100644
--- a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
+++ b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
@@ -3738,7 +3738,15 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage
case GameMessage::MSG_META_TAKE_SCREENSHOT:
{
if (TheDisplay)
- TheDisplay->takeScreenShot();
+ TheDisplay->takeScreenShot(SCREENSHOT_JPEG);
+ disp = DESTROY_MESSAGE;
+ break;
+ }
+
+ case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG:
+ {
+ if (TheDisplay)
+ TheDisplay->takeScreenShot(SCREENSHOT_PNG);
disp = DESTROY_MESSAGE;
break;
}
diff --git a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp
index e5032c93b75..31a954e67b5 100644
--- a/Core/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp
+++ b/Core/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_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 },
@@ -945,6 +946,26 @@ void MetaMap::generateMetaMap()
map->m_usableIn = COMMANDUSABLE_GAME;
}
}
+ {
+ 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_PNG);
+ if (map->m_key == MK_NONE)
+ {
+ map->m_key = MK_F12;
+ map->m_transition = DOWN;
+ map->m_modState = CTRL;
+ map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
+ }
+ }
#if defined(RTS_DEBUG)
{
diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt
index a7016b52e73..82bcc335b07 100644
--- a/Core/GameEngineDevice/CMakeLists.txt
+++ b/Core/GameEngineDevice/CMakeLists.txt
@@ -73,6 +73,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
@@ -175,6 +176,8 @@ 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/stb_image_write_impl.cpp
# Source/W3DDevice/GameClient/W3dWaypointBuffer.cpp
# Source/W3DDevice/GameClient/W3DWebBrowser.cpp
Source/W3DDevice/GameClient/Water/W3DWater.cpp
@@ -220,6 +223,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..01b04030838
--- /dev/null
+++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h
@@ -0,0 +1,24 @@
+/*
+** 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 .
+*/
+
+#pragma once
+
+#include "GameClient/Display.h"
+
+void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 0);
+
diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp
new file mode 100644
index 00000000000..2264ba2c403
--- /dev/null
+++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp
@@ -0,0 +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 .
+*/
+
+#define STB_IMAGE_WRITE_IMPLEMENTATION
+#include
+
diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h
index 67264cbea62..6f79e4c1fbd 100644
--- a/Generals/Code/GameEngine/Include/Common/GlobalData.h
+++ b/Generals/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/Generals/Code/GameEngine/Include/Common/MessageStream.h b/Generals/Code/GameEngine/Include/Common/MessageStream.h
index 36f09af52bb..15a9b5e2344 100644
--- a/Generals/Code/GameEngine/Include/Common/MessageStream.h
+++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h
@@ -256,7 +256,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, ///< take JPEG screenshot (F12)
+ 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/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
index 3aabf4ca449..858ff1ecee9 100644
--- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
+++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
@@ -650,6 +650,7 @@ GlobalData::GlobalData()
m_enableDynamicLOD = TRUE;
m_enableStaticLOD = TRUE;
m_rightMouseAlwaysScrolls = FALSE;
+ m_jpegQuality = 80;
m_useWaterPlane = FALSE;
m_useCloudPlane = FALSE;
m_downwindAngle = ( -0.785f );//Northeast!
@@ -1198,6 +1199,7 @@ void GlobalData::parseGameDataDefinition( INI* ini )
TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled();
TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled();
TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled();
+ TheWritableGlobalData->m_jpegQuality = optionPref.getJPEGQuality();
TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor();
TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor();
TheWritableGlobalData->m_moveScrollAnchor = optionPref.getMoveScrollAnchor();
diff --git a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp
index e89dbd6f969..6081067f857 100644
--- a/Generals/Code/GameEngine/Source/Common/MessageStream.cpp
+++ b/Generals/Code/GameEngine/Source/Common/MessageStream.cpp
@@ -337,6 +337,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_ALL_CHEER)
CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE)
CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT)
diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt
index 704352959cf..500aa9eb304 100644
--- a/Generals/Code/GameEngineDevice/CMakeLists.txt
+++ b/Generals/Code/GameEngineDevice/CMakeLists.txt
@@ -139,6 +139,7 @@ set(GAMEENGINEDEVICE_SRC
Source/W3DDevice/GameClient/W3DDebugDisplay.cpp
Source/W3DDevice/GameClient/W3DDebugIcons.cpp
Source/W3DDevice/GameClient/W3DDisplay.cpp
+ Source/W3DDevice/GameClient/W3DScreenshot.cpp
Source/W3DDevice/GameClient/W3DDisplayString.cpp
Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp
Source/W3DDevice/GameClient/W3DDynamicLight.cpp
@@ -200,6 +201,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 11e480e210c..2c90f1471c6 100644
--- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
+++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
@@ -122,7 +122,7 @@ class W3DDisplay : public Display
virtual VideoBuffer* createVideoBuffer() override; ///< Create a video buffer that can be used for this display
- virtual void takeScreenShot() override; //save screenshot to file
+ virtual void takeScreenShot(ScreenshotFormat format) override; //save screenshot in specified format
virtual void toggleMovieCapture() override; //enable AVI or frame capture mode.
virtual void toggleLetterBox() override; ///
// USER INCLUDES //////////////////////////////////////////////////////////////
+#include "W3DDevice/GameClient/W3DScreenshot.h"
#include "Common/FramePacer.h"
#include "Common/ThingFactory.h"
#include "Common/GlobalData.h"
@@ -2941,221 +2942,6 @@ void W3DDisplay::setShroudLevel( Int x, Int y, CellShroudStatus setting )
}
}
-//=============================================================================
-///Utility function to dump data into a .BMP file
-static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height)
-{
- HANDLE hf; // file handle
- BITMAPFILEHEADER hdr; // bitmap file-header
- PBITMAPINFOHEADER pbih; // bitmap info-header
- LPBYTE lpBits; // memory pointer
- DWORD dwTotal; // total count of bytes
- DWORD cb; // incremental count of bytes
- BYTE *hp; // byte pointer
- DWORD dwTmp;
-
- PBITMAPINFO pbmi;
-
- pbmi = (PBITMAPINFO) LocalAlloc(LPTR,sizeof(BITMAPINFOHEADER));
- if (pbmi == nullptr)
- return;
-
- pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
- pbmi->bmiHeader.biWidth = width;
- pbmi->bmiHeader.biHeight = height;
- pbmi->bmiHeader.biPlanes = 1;
- pbmi->bmiHeader.biBitCount = 24;
- pbmi->bmiHeader.biCompression = BI_RGB;
- pbmi->bmiHeader.biSizeImage = (pbmi->bmiHeader.biWidth + 7) /8 * pbmi->bmiHeader.biHeight * 24;
- pbmi->bmiHeader.biClrImportant = 0;
-
- pbih = (PBITMAPINFOHEADER) pbmi;
- lpBits = (LPBYTE) image;
-
- // Create the .BMP file.
- hf = CreateFile(pszFile,
- GENERIC_READ | GENERIC_WRITE,
- (DWORD) 0,
- nullptr,
- CREATE_ALWAYS,
- FILE_ATTRIBUTE_NORMAL,
- (HANDLE) nullptr);
-
- if (hf != INVALID_HANDLE_VALUE)
- {
- hdr.bfType = 0x4d42; // 0x42 = "B" 0x4d = "M"
- // Compute the size of the entire file.
- hdr.bfSize = (DWORD) (sizeof(BITMAPFILEHEADER) +
- pbih->biSize + pbih->biClrUsed
- * sizeof(RGBQUAD) + pbih->biSizeImage);
- hdr.bfReserved1 = 0;
- hdr.bfReserved2 = 0;
-
- // Compute the offset to the array of color indices.
- hdr.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) +
- pbih->biSize + pbih->biClrUsed
- * sizeof (RGBQUAD);
-
- // Copy the BITMAPFILEHEADER into the .BMP file.
- if (WriteFile(hf, (LPVOID) &hdr, sizeof(BITMAPFILEHEADER),
- (LPDWORD) &dwTmp, nullptr))
- {
- // Copy the BITMAPINFOHEADER and RGBQUAD array into the file.
- if (WriteFile(hf, (LPVOID) pbih, sizeof(BITMAPINFOHEADER) + pbih->biClrUsed * sizeof (RGBQUAD),(LPDWORD) &dwTmp, nullptr))
- {
- // Copy the array of color indices into the .BMP file.
- dwTotal = cb = pbih->biSizeImage;
- hp = lpBits;
- WriteFile(hf, (LPSTR) hp, (int) cb, (LPDWORD) &dwTmp, nullptr);
- }
- }
-
- // Close the .BMP file.
- CloseHandle(hf);
- }
-
- // Free memory.
- LocalFree( (HLOCAL) pbmi);
-}
-
-///Save Screen Capture to a file
-void W3DDisplay::takeScreenShot()
-{
- 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());
-}
-
/** Start/Stop capturing an AVI movie*/
void W3DDisplay::toggleMovieCapture()
{
diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
new file mode 100644
index 00000000000..c0b7b98b857
--- /dev/null
+++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
@@ -0,0 +1,159 @@
+/*
+** 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 "W3DDevice/GameClient/W3DScreenshot.h"
+#include "W3DDevice/GameClient/W3DDisplay.h"
+#include "Common/GlobalData.h"
+#include "GameClient/GameText.h"
+#include "GameClient/InGameUI.h"
+#include "WW3D2/dx8wrapper.h"
+#include "WW3D2/surfaceclass.h"
+#include
+
+struct ScreenshotThreadData
+{
+ unsigned char* imageData;
+ unsigned int width;
+ unsigned int height;
+ char pathname[_MAX_PATH];
+ 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)
+ {
+ DEBUG_LOG(("Failed to write screenshot %s", data->pathname));
+ }
+
+ delete [] data->imageData;
+ delete data;
+
+ return 0;
+}
+
+void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality)
+{
+ char leafname[_MAX_FNAME];
+ char pathname[_MAX_PATH];
+ const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png";
+
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+ sprintf(leafname, "sshot_%04d%02d%02d_%02d%02d%02d_%03d.%s",
+ st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, extension);
+ strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname));
+ strlcat(pathname, leafname, ARRAY_SIZE(pathname));
+
+ // 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;
+ 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 = nullptr;
+
+ 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;
+ strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname));
+
+ DWORD threadId;
+ HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId);
+ if (hThread)
+ {
+ CloseHandle(hThread);
+
+ UnicodeString ufileName;
+ ufileName.translate(leafname);
+ TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
+ }
+ else
+ {
+ delete [] threadData->imageData;
+ delete threadData;
+ }
+}
+
+void W3DDisplay::takeScreenShot(ScreenshotFormat format)
+{
+ W3D_TakeCompressedScreenshot(format);
+}
diff --git a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
index ee27b4cc7a0..0d8fb26369d 100644
--- a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
+++ b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
@@ -101,7 +101,7 @@ class GUIEditDisplay : public Display
virtual void drawScaledVideoBuffer( VideoBuffer *buffer, VideoStreamInterface *stream ) override { }
virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY,
Int endX, Int endY ) override { }
- virtual void takeScreenShot() override { }
+ virtual void takeScreenShot(ScreenshotFormat format) override { }
virtual void toggleMovieCapture() override {}
// 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 4c5130e1c05..844e3bf986c 100644
--- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
+++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
@@ -144,6 +144,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 594ae255b5d..ebf97de5fc4 100644
--- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h
+++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h
@@ -256,7 +256,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, ///< take JPEG screenshot (F12)
+ 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/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
index 9900029d48d..1286c06cdb2 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
@@ -654,6 +654,7 @@ GlobalData::GlobalData()
m_enableDynamicLOD = TRUE;
m_enableStaticLOD = TRUE;
m_rightMouseAlwaysScrolls = FALSE;
+ m_jpegQuality = 80;
m_useWaterPlane = FALSE;
m_useCloudPlane = FALSE;
m_downwindAngle = ( -0.785f );//Northeast!
@@ -1205,6 +1206,7 @@ void GlobalData::parseGameDataDefinition( INI* ini )
TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled();
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 f7c58e978d2..116b9016399 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp
@@ -337,6 +337,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_ALL_CHEER)
CASE_LABEL(MSG_META_TOGGLE_ATTACKMOVE)
CASE_LABEL(MSG_META_BEGIN_CAMERA_ROTATE_LEFT)
diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
index 5cae369888c..65248211714 100644
--- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
+++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
@@ -150,6 +150,7 @@ set(GAMEENGINEDEVICE_SRC
Source/W3DDevice/GameClient/W3DDebugDisplay.cpp
Source/W3DDevice/GameClient/W3DDebugIcons.cpp
Source/W3DDevice/GameClient/W3DDisplay.cpp
+ Source/W3DDevice/GameClient/W3DScreenshot.cpp
Source/W3DDevice/GameClient/W3DDisplayString.cpp
Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp
Source/W3DDevice/GameClient/W3DDynamicLight.cpp
@@ -213,6 +214,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 c43ec2691a7..19bd6c0d27f 100644
--- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
+++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
@@ -122,7 +122,7 @@ class W3DDisplay : public Display
virtual VideoBuffer* createVideoBuffer() override; ///< Create a video buffer that can be used for this display
- virtual void takeScreenShot() override; //save screenshot to file
+ virtual void takeScreenShot(ScreenshotFormat format) override; //save screenshot in specified format
virtual void toggleMovieCapture() override; //enable AVI or frame capture mode.
virtual void toggleLetterBox() override; ///
// USER INCLUDES //////////////////////////////////////////////////////////////
+#include "W3DDevice/GameClient/W3DScreenshot.h"
#include "Common/FramePacer.h"
#include "Common/ThingFactory.h"
#include "Common/GlobalData.h"
@@ -3053,221 +3054,6 @@ void W3DDisplay::setShroudLevel( Int x, Int y, CellShroudStatus setting )
}
}
-//=============================================================================
-///Utility function to dump data into a .BMP file
-static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height)
-{
- HANDLE hf; // file handle
- BITMAPFILEHEADER hdr; // bitmap file-header
- PBITMAPINFOHEADER pbih; // bitmap info-header
- LPBYTE lpBits; // memory pointer
- DWORD dwTotal; // total count of bytes
- DWORD cb; // incremental count of bytes
- BYTE *hp; // byte pointer
- DWORD dwTmp;
-
- PBITMAPINFO pbmi;
-
- pbmi = (PBITMAPINFO) LocalAlloc(LPTR,sizeof(BITMAPINFOHEADER));
- if (pbmi == nullptr)
- return;
-
- pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
- pbmi->bmiHeader.biWidth = width;
- pbmi->bmiHeader.biHeight = height;
- pbmi->bmiHeader.biPlanes = 1;
- pbmi->bmiHeader.biBitCount = 24;
- pbmi->bmiHeader.biCompression = BI_RGB;
- pbmi->bmiHeader.biSizeImage = (pbmi->bmiHeader.biWidth + 7) /8 * pbmi->bmiHeader.biHeight * 24;
- pbmi->bmiHeader.biClrImportant = 0;
-
- pbih = (PBITMAPINFOHEADER) pbmi;
- lpBits = (LPBYTE) image;
-
- // Create the .BMP file.
- hf = CreateFile(pszFile,
- GENERIC_READ | GENERIC_WRITE,
- (DWORD) 0,
- nullptr,
- CREATE_ALWAYS,
- FILE_ATTRIBUTE_NORMAL,
- (HANDLE) nullptr);
-
- if (hf != INVALID_HANDLE_VALUE)
- {
- hdr.bfType = 0x4d42; // 0x42 = "B" 0x4d = "M"
- // Compute the size of the entire file.
- hdr.bfSize = (DWORD) (sizeof(BITMAPFILEHEADER) +
- pbih->biSize + pbih->biClrUsed
- * sizeof(RGBQUAD) + pbih->biSizeImage);
- hdr.bfReserved1 = 0;
- hdr.bfReserved2 = 0;
-
- // Compute the offset to the array of color indices.
- hdr.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) +
- pbih->biSize + pbih->biClrUsed
- * sizeof (RGBQUAD);
-
- // Copy the BITMAPFILEHEADER into the .BMP file.
- if (WriteFile(hf, (LPVOID) &hdr, sizeof(BITMAPFILEHEADER),
- (LPDWORD) &dwTmp, nullptr))
- {
- // Copy the BITMAPINFOHEADER and RGBQUAD array into the file.
- if (WriteFile(hf, (LPVOID) pbih, sizeof(BITMAPINFOHEADER) + pbih->biClrUsed * sizeof (RGBQUAD),(LPDWORD) &dwTmp, nullptr))
- {
- // Copy the array of color indices into the .BMP file.
- dwTotal = cb = pbih->biSizeImage;
- hp = lpBits;
- WriteFile(hf, (LPSTR) hp, (int) cb, (LPDWORD) &dwTmp, nullptr);
- }
- }
-
- // Close the .BMP file.
- CloseHandle(hf);
- }
-
- // Free memory.
- LocalFree( (HLOCAL) pbmi);
-}
-
-///Save Screen Capture to a file
-void W3DDisplay::takeScreenShot()
-{
- 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());
-}
-
/** Start/Stop capturing an AVI movie*/
void W3DDisplay::toggleMovieCapture()
{
diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
new file mode 100644
index 00000000000..eb4e11cf106
--- /dev/null
+++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
@@ -0,0 +1,159 @@
+/*
+** 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 "W3DDevice/GameClient/W3DScreenshot.h"
+#include "W3DDevice/GameClient/W3DDisplay.h"
+#include "Common/GlobalData.h"
+#include "GameClient/GameText.h"
+#include "GameClient/InGameUI.h"
+#include "WW3D2/dx8wrapper.h"
+#include "WW3D2/surfaceclass.h"
+#include
+
+struct ScreenshotThreadData
+{
+ unsigned char* imageData;
+ unsigned int width;
+ unsigned int height;
+ char pathname[_MAX_PATH];
+ 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)
+ {
+ DEBUG_LOG(("Failed to write screenshot %s", data->pathname));
+ }
+
+ delete [] data->imageData;
+ delete data;
+
+ return 0;
+}
+
+void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality)
+{
+ char leafname[_MAX_FNAME];
+ char pathname[_MAX_PATH];
+ const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png";
+
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+ sprintf(leafname, "sshot_%04d%02d%02d_%02d%02d%02d_%03d.%s",
+ st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, extension);
+ strlcpy(pathname, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(pathname));
+ strlcat(pathname, leafname, ARRAY_SIZE(pathname));
+
+ // 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;
+ 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 = nullptr;
+
+ 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;
+ strlcpy(threadData->pathname, pathname, ARRAY_SIZE(threadData->pathname));
+
+ DWORD threadId;
+ HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId);
+ if (hThread)
+ {
+ CloseHandle(hThread);
+
+ UnicodeString ufileName;
+ ufileName.translate(leafname);
+ TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
+ }
+ else
+ {
+ delete [] threadData->imageData;
+ delete threadData;
+ }
+}
+
+void W3DDisplay::takeScreenShot(ScreenshotFormat format)
+{
+ W3D_TakeCompressedScreenshot(format);
+}
diff --git a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
index 9e432ecb827..d9a659679a1 100644
--- a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
+++ b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
@@ -101,7 +101,7 @@ class GUIEditDisplay : public Display
virtual void drawScaledVideoBuffer( VideoBuffer *buffer, VideoStreamInterface *stream ) override { }
virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY,
Int endX, Int endY ) override { }
- virtual void takeScreenShot() override { }
+ virtual void takeScreenShot(ScreenshotFormat format) override { }
virtual void toggleMovieCapture() override {}
// methods that we need to stub
diff --git a/cmake/stb.cmake b/cmake/stb.cmake
new file mode 100644
index 00000000000..42a6bc5ad90
--- /dev/null
+++ b/cmake/stb.cmake
@@ -0,0 +1,20 @@
+# TheSuperHackers @build bobtista 02/11/2025 STB single-file public domain libraries for image encoding
+# https://github.com/nothings/stb
+
+find_package(Stb CONFIG QUIET)
+
+if(NOT Stb_FOUND)
+ include(FetchContent)
+ FetchContent_Declare(
+ stb
+ GIT_REPOSITORY https://github.com/nothings/stb.git
+ GIT_TAG 5c205738c191bcb0abc65c4febfa9bd25ff35234
+ )
+
+ FetchContent_MakeAvailable(stb)
+
+ set(Stb_INCLUDE_DIR ${stb_SOURCE_DIR})
+endif()
+
+add_library(stb INTERFACE)
+target_include_directories(stb INTERFACE ${Stb_INCLUDE_DIR})
diff --git a/scripts/cpp/unify_move_files.py b/scripts/cpp/unify_move_files.py
index b95939719ec..8bb57341344 100644
--- a/scripts/cpp/unify_move_files.py
+++ b/scripts/cpp/unify_move_files.py
@@ -311,6 +311,7 @@ def main():
#unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainVisual.cpp")
#unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTreeBuffer.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/W3DTreeBuffer.cpp")
#unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp")
+ #unify_file(Game.ZEROHOUR, "GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp", Game.CORE, "GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp")
#unify_file(Game.ZEROHOUR, "GameEngine/Include/Common/UserPreferences.h", Game.CORE, "GameEngine/Include/Common/UserPreferences.h")
#unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/UserPreferences.cpp", Game.CORE, "GameEngine/Source/Common/UserPreferences.cpp")
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