diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4160c918c7..7256433de2 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/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt
index 404c1eb60e..43d57498f5 100644
--- a/Core/GameEngineDevice/CMakeLists.txt
+++ b/Core/GameEngineDevice/CMakeLists.txt
@@ -71,6 +71,7 @@ set(GAMEENGINEDEVICE_SRC
# Include/W3DDevice/GameClient/W3DTerrainVisual.h
# Include/W3DDevice/GameClient/W3DTreeBuffer.h
Include/W3DDevice/GameClient/W3DVideoBuffer.h
+ Include/W3DDevice/GameClient/W3DScreenshot.h
# Include/W3DDevice/GameClient/W3DView.h
# Include/W3DDevice/GameClient/W3DVolumetricShadow.h
# Include/W3DDevice/GameClient/W3DWater.h
@@ -220,6 +221,7 @@ target_include_directories(corei_gameenginedevice_public INTERFACE
target_link_libraries(corei_gameenginedevice_private INTERFACE
corei_always
corei_main
+ stb
)
target_link_libraries(corei_gameenginedevice_public INTERFACE
diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h
new file mode 100644
index 0000000000..fe43aa8032
--- /dev/null
+++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h
@@ -0,0 +1,24 @@
+/*
+** 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
+
+#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 e10943f2e4..c55bbadbaf 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 a36aeed7ad..1a24b12e6e 100644
--- a/Generals/Code/GameEngine/Include/Common/MessageStream.h
+++ b/Generals/Code/GameEngine/Include/Common/MessageStream.h
@@ -257,7 +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, ///< 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 aef49361d3..175ce1eddd 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 4d12e246e3..00b235121a 100644
--- a/Generals/Code/GameEngine/Include/GameClient/Display.h
+++ b/Generals/Code/GameEngine/Include/GameClient/Display.h
@@ -35,6 +35,12 @@
class View;
+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.
@@ -168,7 +174,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(void) = 0; ///< saves screenshot to a file
+ 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 4b72223877..c1dae082af 100644
--- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
+++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
@@ -1180,6 +1180,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 0af149735e..5aeefea00d 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_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 60ff6cbcee..2d759dbc65 100644
--- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp
+++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp
@@ -335,6 +335,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 f71498c242..27647194fc 100644
--- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
+++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
@@ -3410,7 +3410,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage
case GameMessage::MSG_META_TAKE_SCREENSHOT:
{
if (TheDisplay)
- TheDisplay->takeScreenShot();
+ TheDisplay->takeScreenShot(SCREENSHOT_JPEG);
+ break;
+ }
+
+ case GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG:
+ {
+ if (TheDisplay)
+ 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 2b8b0c18a0..7ae1ea3fde 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_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 },
@@ -793,6 +794,26 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t)
map->m_displayName = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SelectNextIdleWorker", L"Next Idle Worker");
}
}
+ {
+ 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;
+ map->m_transition = DOWN;
+ map->m_modState = CTRL;
+ map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
+ }
+ }
#if defined(RTS_DEBUG)
{
diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt
index 63a78b15a4..47bf0b5229 100644
--- a/Generals/Code/GameEngineDevice/CMakeLists.txt
+++ b/Generals/Code/GameEngineDevice/CMakeLists.txt
@@ -200,7 +200,18 @@ target_precompile_headers(g_gameenginedevice PRIVATE
target_link_libraries(g_gameenginedevice PRIVATE
corei_gameenginedevice_private
gi_always
- gi_main
+ gi_main
+ 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
diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
index 018e27ef81..8fa09a4e36 100644
--- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
+++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
@@ -120,7 +120,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 takeScreenShot(ScreenshotFormat format); //save screenshot in specified format
virtual void toggleMovieCapture(void); //enable AVI or frame capture mode.
virtual void toggleLetterBox(void); ///
// USER INCLUDES //////////////////////////////////////////////////////////////
+#include "W3DDevice/GameClient/W3DScreenshot.h"
#include "Common/FramePacer.h"
#include "Common/ThingFactory.h"
#include "Common/GlobalData.h"
@@ -2879,142 +2880,7 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height)
}
///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(), 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,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 = NULL;
-
- 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 = NULL;
-
- //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());
-}
+#include "W3DScreenshot.cpp"
/** Start/Stop capturing an AVI movie*/
void W3DDisplay::toggleMovieCapture(void)
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 0000000000..9a601d1232
--- /dev/null
+++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
@@ -0,0 +1,130 @@
+#include
+
+struct ScreenshotThreadData
+{
+ unsigned char* imageData;
+ unsigned int width;
+ unsigned int height;
+ char pathname[_MAX_PATH];
+ char leafname[_MAX_FNAME];
+ int quality;
+ ScreenshotFormat format;
+};
+
+static DWORD WINAPI screenshotThreadFunc(LPVOID param)
+{
+ ScreenshotThreadData* data = (ScreenshotThreadData*)param;
+
+ int result = 0;
+ switch (data->format)
+ {
+ case SCREENSHOT_JPEG:
+ result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality);
+ break;
+ case SCREENSHOT_PNG:
+ result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3);
+ break;
+ }
+
+ if (!result) {
+ OutputDebugStringA("Failed to write screenshot\n");
+ }
+
+ delete [] data->imageData;
+ delete data;
+
+ return 0;
+}
+
+void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality)
+{
+ char leafname[_MAX_FNAME];
+ char pathname[_MAX_PATH];
+ static int jpegFrameNumber = 1;
+ static int pngFrameNumber = 1;
+
+ int* frameNumber = (format == SCREENSHOT_JPEG) ? &jpegFrameNumber : &pngFrameNumber;
+ const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png";
+
+ Bool done = false;
+ while (!done) {
+ sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension);
+ strcpy(pathname, TheGlobalData->getPath_UserData().str());
+ strlcat(pathname, leafname, ARRAY_SIZE(pathname));
+ if (_access(pathname, 0) == -1)
+ done = true;
+ }
+
+ SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer();
+ SurfaceClass::SurfaceDescription surfaceDesc;
+ surface->Get_Description(surfaceDesc);
+
+ SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)));
+ DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL);
+
+ surface->Release_Ref();
+ surface = NULL;
+
+ struct Rect
+ {
+ int Pitch;
+ void* pBits;
+ } lrect;
+
+ lrect.pBits = surfaceCopy->Lock(&lrect.Pitch);
+ if (lrect.pBits == NULL)
+ {
+ surfaceCopy->Release_Ref();
+ return;
+ }
+
+ unsigned int x, y, index, index2;
+ unsigned int width = surfaceDesc.Width;
+ unsigned int height = surfaceDesc.Height;
+
+ unsigned char* image = new unsigned char[3 * width * height];
+
+ for (y = 0; y < height; y++)
+ {
+ for (x = 0; x < width; x++)
+ {
+ index = 3 * (x + y * width);
+ index2 = y * lrect.Pitch + 4 * x;
+
+ image[index] = *((unsigned char*)lrect.pBits + index2 + 2);
+ image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1);
+ image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0);
+ }
+ }
+
+ surfaceCopy->Unlock();
+ surfaceCopy->Release_Ref();
+ surfaceCopy = NULL;
+
+ if (quality <= 0 && format == SCREENSHOT_JPEG)
+ quality = TheGlobalData->m_jpegQuality;
+
+ ScreenshotThreadData* threadData = new ScreenshotThreadData();
+ threadData->imageData = image;
+ threadData->width = width;
+ threadData->height = height;
+ threadData->quality = quality;
+ threadData->format = format;
+ strcpy(threadData->pathname, pathname);
+ strcpy(threadData->leafname, leafname);
+
+ DWORD threadId;
+ HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId);
+ if (hThread) {
+ CloseHandle(hThread);
+ }
+
+ UnicodeString ufileName;
+ ufileName.translate(leafname);
+ TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
+}
+
+void W3DDisplay::takeScreenShot(ScreenshotFormat format)
+{
+ W3D_TakeCompressedScreenshot(format);
+}
diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp
new file mode 100644
index 0000000000..364368901a
--- /dev/null
+++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp
@@ -0,0 +1,21 @@
+/*
+** Command & Conquer Generals(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#define STB_IMAGE_WRITE_IMPLEMENTATION
+#include
+
diff --git a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
index dda18bf3ae..8d36e555b9 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 ) { }
virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY,
Int endX, Int endY ) { }
- virtual void takeScreenShot(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 5ff1ed4039..609c9ea955 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 324f28cfdc..9a653c5d24 100644
--- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h
+++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h
@@ -257,7 +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, ///< 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 7936cfd8ee..324918566c 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 3f3a783946..cebeefce4c 100644
--- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h
+++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h
@@ -35,6 +35,12 @@
class View;
+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.
@@ -168,7 +174,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(void) = 0; ///< saves screenshot to a file
+ 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 071c5debeb..917e851e65 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 1acee2d16f..bde560b6e3 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_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 180dad5cfb..c8dd6c49fb 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp
@@ -368,6 +368,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 633474ef7f..7e50853dad 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
@@ -3743,7 +3743,14 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage
case GameMessage::MSG_META_TAKE_SCREENSHOT:
{
if (TheDisplay)
- TheDisplay->takeScreenShot();
+ TheDisplay->takeScreenShot(SCREENSHOT_JPEG);
+ break;
+ }
+
+ case GameMessage::MSG_META_TAKE_SCREENSHOT_JPEG:
+ {
+ if (TheDisplay)
+ 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 2a7f4097fc..9f6a19e6c8 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_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 },
@@ -851,6 +852,26 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t)
map->m_displayName = TheGameText->FETCH_OR_SUBSTITUTE("GUI:SelectNextIdleWorker", L"Next Idle Worker");
}
}
+ {
+ 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;
+ map->m_transition = DOWN;
+ map->m_modState = CTRL;
+ map->m_usableIn = COMMANDUSABLE_EVERYWHERE;
+ }
+ }
#if defined(RTS_DEBUG)
{
diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
index 82c1bb4259..114cfacc31 100644
--- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
+++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
@@ -214,6 +214,17 @@ target_link_libraries(z_gameenginedevice PRIVATE
corei_gameenginedevice_private
zi_always
zi_main
+ 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
diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
index 7f5ad9f174..df14b0480a 100644
--- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
+++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
@@ -120,7 +120,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 takeScreenShot(ScreenshotFormat format); //save screenshot in specified format
virtual void toggleMovieCapture(void); //enable AVI or frame capture mode.
virtual void toggleLetterBox(void); ///
// USER INCLUDES //////////////////////////////////////////////////////////////
+#include "W3DDevice/GameClient/W3DScreenshot.h"
#include "Common/FramePacer.h"
#include "Common/ThingFactory.h"
#include "Common/GlobalData.h"
@@ -2998,142 +2999,7 @@ static void CreateBMPFile(LPTSTR pszFile, char *image, Int width, Int height)
}
///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(), 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,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 = NULL;
-
- 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 = NULL;
-
- //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());
-}
+#include "W3DScreenshot.cpp"
/** Start/Stop capturing an AVI movie*/
void W3DDisplay::toggleMovieCapture(void)
diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
new file mode 100644
index 0000000000..9a601d1232
--- /dev/null
+++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
@@ -0,0 +1,130 @@
+#include
+
+struct ScreenshotThreadData
+{
+ unsigned char* imageData;
+ unsigned int width;
+ unsigned int height;
+ char pathname[_MAX_PATH];
+ char leafname[_MAX_FNAME];
+ int quality;
+ ScreenshotFormat format;
+};
+
+static DWORD WINAPI screenshotThreadFunc(LPVOID param)
+{
+ ScreenshotThreadData* data = (ScreenshotThreadData*)param;
+
+ int result = 0;
+ switch (data->format)
+ {
+ case SCREENSHOT_JPEG:
+ result = stbi_write_jpg(data->pathname, data->width, data->height, 3, data->imageData, data->quality);
+ break;
+ case SCREENSHOT_PNG:
+ result = stbi_write_png(data->pathname, data->width, data->height, 3, data->imageData, data->width * 3);
+ break;
+ }
+
+ if (!result) {
+ OutputDebugStringA("Failed to write screenshot\n");
+ }
+
+ delete [] data->imageData;
+ delete data;
+
+ return 0;
+}
+
+void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality)
+{
+ char leafname[_MAX_FNAME];
+ char pathname[_MAX_PATH];
+ static int jpegFrameNumber = 1;
+ static int pngFrameNumber = 1;
+
+ int* frameNumber = (format == SCREENSHOT_JPEG) ? &jpegFrameNumber : &pngFrameNumber;
+ const char* extension = (format == SCREENSHOT_JPEG) ? "jpg" : "png";
+
+ Bool done = false;
+ while (!done) {
+ sprintf(leafname, "sshot%.3d.%s", (*frameNumber)++, extension);
+ strcpy(pathname, TheGlobalData->getPath_UserData().str());
+ strlcat(pathname, leafname, ARRAY_SIZE(pathname));
+ if (_access(pathname, 0) == -1)
+ done = true;
+ }
+
+ SurfaceClass* surface = DX8Wrapper::_Get_DX8_Back_Buffer();
+ SurfaceClass::SurfaceDescription surfaceDesc;
+ surface->Get_Description(surfaceDesc);
+
+ SurfaceClass* surfaceCopy = NEW_REF(SurfaceClass, (DX8Wrapper::_Create_DX8_Surface(surfaceDesc.Width, surfaceDesc.Height, surfaceDesc.Format)));
+ DX8Wrapper::_Copy_DX8_Rects(surface->Peek_D3D_Surface(), NULL, 0, surfaceCopy->Peek_D3D_Surface(), NULL);
+
+ surface->Release_Ref();
+ surface = NULL;
+
+ struct Rect
+ {
+ int Pitch;
+ void* pBits;
+ } lrect;
+
+ lrect.pBits = surfaceCopy->Lock(&lrect.Pitch);
+ if (lrect.pBits == NULL)
+ {
+ surfaceCopy->Release_Ref();
+ return;
+ }
+
+ unsigned int x, y, index, index2;
+ unsigned int width = surfaceDesc.Width;
+ unsigned int height = surfaceDesc.Height;
+
+ unsigned char* image = new unsigned char[3 * width * height];
+
+ for (y = 0; y < height; y++)
+ {
+ for (x = 0; x < width; x++)
+ {
+ index = 3 * (x + y * width);
+ index2 = y * lrect.Pitch + 4 * x;
+
+ image[index] = *((unsigned char*)lrect.pBits + index2 + 2);
+ image[index + 1] = *((unsigned char*)lrect.pBits + index2 + 1);
+ image[index + 2] = *((unsigned char*)lrect.pBits + index2 + 0);
+ }
+ }
+
+ surfaceCopy->Unlock();
+ surfaceCopy->Release_Ref();
+ surfaceCopy = NULL;
+
+ if (quality <= 0 && format == SCREENSHOT_JPEG)
+ quality = TheGlobalData->m_jpegQuality;
+
+ ScreenshotThreadData* threadData = new ScreenshotThreadData();
+ threadData->imageData = image;
+ threadData->width = width;
+ threadData->height = height;
+ threadData->quality = quality;
+ threadData->format = format;
+ strcpy(threadData->pathname, pathname);
+ strcpy(threadData->leafname, leafname);
+
+ DWORD threadId;
+ HANDLE hThread = CreateThread(NULL, 0, screenshotThreadFunc, threadData, 0, &threadId);
+ if (hThread) {
+ CloseHandle(hThread);
+ }
+
+ UnicodeString ufileName;
+ ufileName.translate(leafname);
+ TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
+}
+
+void W3DDisplay::takeScreenShot(ScreenshotFormat format)
+{
+ W3D_TakeCompressedScreenshot(format);
+}
diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp
new file mode 100644
index 0000000000..364368901a
--- /dev/null
+++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/stb_image_write_impl.cpp
@@ -0,0 +1,21 @@
+/*
+** Command & Conquer Generals(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#define STB_IMAGE_WRITE_IMPLEMENTATION
+#include
+
diff --git a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
index e7741c4016..44f625229b 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 ) { }
virtual void drawVideoBuffer( VideoBuffer *buffer, Int startX, Int startY,
Int endX, Int endY ) { }
- virtual void takeScreenShot(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
new file mode 100644
index 0000000000..8f2078a810
--- /dev/null
+++ b/cmake/stb.cmake
@@ -0,0 +1,19 @@
+# TheSuperHackers @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)
+
+ add_library(stb INTERFACE)
+ target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
+endif()
diff --git a/vcpkg.json b/vcpkg.json
index 011b913c8a..9ce3c6667c 100644
--- a/vcpkg.json
+++ b/vcpkg.json
@@ -3,6 +3,7 @@
"builtin-baseline": "b02e341c927f16d991edbd915d8ea43eac52096c",
"dependencies": [
"zlib",
- "ffmpeg"
+ "ffmpeg",
+ "stb"
]
}
\ No newline at end of file