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..30fae8e7acd 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() const;
Real getScrollFactor();
Bool getDrawScrollAnchor();
Bool getMoveScrollAnchor();
diff --git a/Core/GameEngine/Include/GameClient/Display.h b/Core/GameEngine/Include/GameClient/Display.h
index 8c022244206..50e14a28453 100644
--- a/Core/GameEngine/Include/GameClient/Display.h
+++ b/Core/GameEngine/Include/GameClient/Display.h
@@ -33,6 +33,14 @@
#include "GameClient/GameFont.h"
#include "GameClient/View.h"
+enum ScreenshotFormat
+{
+ SCREENSHOT_JPEG,
+ SCREENSHOT_PNG,
+
+ SCREENSHOT_FORMAT_COUNT
+};
+
struct ShroudLevel
{
Short m_currentShroud; ///< A Value of 1 means shrouded. 0 is not. Negative is the count of people looking.
@@ -172,7 +180,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, Int jpegQuality = 80) = 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..72039cedf76 100644
--- a/Core/GameEngine/Source/Common/OptionPreferences.cpp
+++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp
@@ -239,6 +239,18 @@ Bool OptionPreferences::getDoubleClickAttackMoveEnabled()
return FALSE;
}
+Int OptionPreferences::getJPEGQuality() const
+{
+ OptionPreferences::const_iterator it = find("JPEGQuality");
+ if (it == end())
+ return 80;
+
+ // TheSuperHackers @info bobtista 08/07/2026 Cap the quality at 95, because JPEG quality
+ // above that increases the file size significantly with no visible benefit.
+ Int quality = atoi(it->second.str());
+ return clamp(1, quality, 95);
+}
+
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..42a32679803 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, TheGlobalData->m_jpegQuality);
+ 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..1b0791a2104 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..e6edec10cd8
--- /dev/null
+++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h
@@ -0,0 +1,28 @@
+/*
+** 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 jpegQuality);
+
+// Called once per frame on the main thread to show messages for screenshots
+// that the screenshot thread has finished writing.
+void W3D_UpdateScreenshotMessages();
+
diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
new file mode 100644
index 00000000000..fc29477c734
--- /dev/null
+++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScreenshot.cpp
@@ -0,0 +1,238 @@
+/*
+** 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 "Common/GlobalData.h"
+#include "GameClient/GameText.h"
+#include "GameClient/InGameUI.h"
+#include "WW3D2/dx8wrapper.h"
+#include "WW3D2/surfaceclass.h"
+#include
+#include
+
+struct ScreenshotThreadData
+{
+ ScreenshotThreadData()
+ : pixelData(nullptr)
+ {
+ }
+
+ ~ScreenshotThreadData()
+ {
+ delete [] pixelData;
+ }
+
+ unsigned char* pixelData;
+ unsigned int width;
+ unsigned int height;
+ bool is16Bit;
+ char userDataDirectory[_MAX_PATH];
+ char leafname[_MAX_FNAME];
+ int quality;
+ ScreenshotFormat format;
+};
+
+// TheInGameUI is not thread safe, so the screenshot thread cannot show the success message
+// itself. It posts the written filename here and the main thread shows the message in
+// W3D_UpdateScreenshotMessages.
+static void* volatile s_screenshotWrittenLeafname = nullptr;
+
+static DWORD WINAPI screenshotThreadFunc(LPVOID param)
+{
+ ScreenshotThreadData* data = static_cast(param);
+
+ // TheSuperHackers @feature bobtista 08/07/2026 Save screenshots into a Screenshots subfolder
+ // to keep the user data root folder tidy.
+ char pathname[_MAX_PATH];
+ strlcpy(pathname, data->userDataDirectory, ARRAY_SIZE(pathname));
+ strlcat(pathname, "Screenshots\\", ARRAY_SIZE(pathname));
+ CreateDirectory(pathname, nullptr);
+ strlcat(pathname, data->leafname, ARRAY_SIZE(pathname));
+
+ const unsigned int width = data->width;
+ const unsigned int height = data->height;
+ unsigned int x, y, index;
+
+ // Convert to R8G8B8 for stb_image_write.
+ unsigned char* image = new unsigned char[3 * width * height];
+
+ if (!data->is16Bit)
+ {
+ // Convert A8R8G8B8/X8R8G8B8 to R8G8B8
+ for (y = 0; y < height; ++y)
+ {
+ const unsigned int* srcLine = reinterpret_cast(data->pixelData + y * width * 4);
+ for (x = 0; x < width; ++x)
+ {
+ const unsigned int argb = srcLine[x];
+ index = 3 * (x + y * width);
+ image[index + 0] = (unsigned char)(argb >> 16); // r
+ image[index + 1] = (unsigned char)(argb >> 8); // g
+ image[index + 2] = (unsigned char)(argb >> 0); // b
+ }
+ }
+ }
+ else
+ {
+ // Convert R5G6B5 to R8G8B8
+ for (y = 0; y < height; ++y)
+ {
+ const unsigned short* srcLine = reinterpret_cast(data->pixelData + y * width * 2);
+ for (x = 0; x < width; ++x)
+ {
+ const unsigned short rgb = srcLine[x];
+ index = 3 * (x + y * width);
+ image[index + 0] = (unsigned char)((rgb & 0xF800) >> 8); // r
+ image[index + 1] = (unsigned char)((rgb & 0x07E0) >> 3); // g
+ image[index + 2] = (unsigned char)((rgb & 0x001F) << 3); // b
+ }
+ }
+ }
+
+ int success = 0;
+ switch (data->format)
+ {
+ case SCREENSHOT_JPEG:
+ success = stbi_write_jpg(pathname, width, height, 3, image, data->quality);
+ break;
+ case SCREENSHOT_PNG:
+ success = stbi_write_png(pathname, width, height, 3, image, width * 3);
+ break;
+ }
+
+ if (success)
+ {
+ char* leafname = new char[_MAX_FNAME];
+ strlcpy(leafname, data->leafname, _MAX_FNAME);
+ char* oldLeafname = static_cast(InterlockedExchangePointer(&s_screenshotWrittenLeafname, leafname));
+ delete [] oldLeafname;
+ }
+ else
+ {
+ DEBUG_LOG(("Failed to write screenshot %s", pathname));
+ }
+
+ delete [] image;
+ delete data;
+
+ return success;
+}
+
+void W3D_UpdateScreenshotMessages()
+{
+ char* leafname = static_cast(InterlockedExchangePointer(&s_screenshotWrittenLeafname, nullptr));
+ if (leafname != nullptr)
+ {
+ UnicodeString ufileName;
+ ufileName.translate(leafname);
+ TheInGameUI->message(TheGameText->fetch("GUI:ScreenCapture"), ufileName.str());
+ delete [] leafname;
+ }
+}
+
+void W3D_TakeCompressedScreenshot(ScreenshotFormat format, Int jpegQuality)
+{
+ static const char* const ScreenshotFormatExtensions[] = { "jpg", "png" };
+ static_assert(ARRAY_SIZE(ScreenshotFormatExtensions) == SCREENSHOT_FORMAT_COUNT, "Incorrect array size");
+
+ // The filename is created here so the timestamp matches the capture time.
+ char leafname[_MAX_FNAME];
+ const char* extension = ScreenshotFormatExtensions[format];
+
+ 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);
+
+ // 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);
+
+ // TheSuperHackers @bugfix bobtista 08/07/2026 Support the 16 bit back buffer format that the
+ // game uses when running in 16 bit color mode. Reading it with the 32 bit stride read garbage.
+ const bool is32Bit = surfaceDesc.Format == WW3D_FORMAT_A8R8G8B8 || surfaceDesc.Format == WW3D_FORMAT_X8R8G8B8;
+ const bool is16Bit = surfaceDesc.Format == WW3D_FORMAT_R5G6B5;
+
+ if (!is32Bit && !is16Bit)
+ {
+ DEBUG_LOG(("Screenshot does not support back buffer format %d", (int)surfaceDesc.Format));
+ surface->Release_Ref();
+ return;
+ }
+
+ 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 y;
+ unsigned int width = surfaceDesc.Width;
+ unsigned int height = surfaceDesc.Height;
+
+ // Copy the surface rows into a tightly packed buffer. The pixel conversion and all file
+ // operations are done on the screenshot thread to keep the main thread cheap.
+ const unsigned int bytesPerPixel = is32Bit ? 4 : 2;
+ unsigned char* pixels = new unsigned char[bytesPerPixel * width * height];
+
+ for (y = 0; y < height; ++y)
+ {
+ memcpy(pixels + y * width * bytesPerPixel, reinterpret_cast(lrect.pBits) + y * lrect.Pitch, width * bytesPerPixel);
+ }
+
+ surfaceCopy->Unlock();
+ surfaceCopy->Release_Ref();
+ surfaceCopy = nullptr;
+
+ ScreenshotThreadData* threadData = new ScreenshotThreadData();
+ threadData->pixelData = pixels;
+ threadData->width = width;
+ threadData->height = height;
+ threadData->is16Bit = is16Bit;
+ threadData->quality = jpegQuality;
+ threadData->format = format;
+ strlcpy(threadData->userDataDirectory, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(threadData->userDataDirectory));
+ strlcpy(threadData->leafname, leafname, ARRAY_SIZE(threadData->leafname));
+
+ DWORD threadId;
+ HANDLE hThread = CreateThread(nullptr, 0, screenshotThreadFunc, threadData, 0, &threadId);
+ if (hThread)
+ {
+ CloseHandle(hThread);
+ }
+ else
+ {
+ delete threadData;
+ }
+}
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/Dependencies/Utility/Utility/interlocked_adapter.h b/Dependencies/Utility/Utility/interlocked_adapter.h
index 45da6a2f9cb..5405f6ccb8c 100644
--- a/Dependencies/Utility/Utility/interlocked_adapter.h
+++ b/Dependencies/Utility/Utility/interlocked_adapter.h
@@ -25,4 +25,9 @@ inline long InterlockedCompareExchange(long volatile *Destination, long Exchange
return (long)InterlockedCompareExchange((PVOID*)Destination, (PVOID)Exchange, (PVOID)Comparand);
}
+inline PVOID InterlockedExchangePointer(PVOID volatile *Target, PVOID Value)
+{
+ return (PVOID)InterlockedExchange((LPLONG)Target, (LONG)Value);
+}
+
#endif
diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h
index 67264cbea62..94357cde728 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; // TheSuperHackers @feature Quality for JPEG screenshots.
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..17b141778f2 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
+ MSG_META_TAKE_SCREENSHOT_PNG, ///< TheSuperHackers @feature Take lossless PNG screenshot
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..adc64c5d3c3 100644
--- a/Generals/Code/GameEngineDevice/CMakeLists.txt
+++ b/Generals/Code/GameEngineDevice/CMakeLists.txt
@@ -200,6 +200,7 @@ target_link_libraries(g_gameenginedevice PRIVATE
corei_gameenginedevice_private
gi_always
gi_main
+ stb
)
target_link_libraries(g_gameenginedevice PUBLIC
diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
index 11e480e210c..aa3f0b79d2c 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, Int jpegQuality) 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"
@@ -1734,6 +1735,9 @@ void W3DDisplay::draw()
if (TheGlobalData->m_headless)
return;
+ // TheSuperHackers @feature bobtista 10/07/2026 Show messages for screenshots finished by the screenshot thread.
+ W3D_UpdateScreenshotMessages();
+
updateAverageFPS();
if (TheGlobalData->m_enableDynamicLOD && TheGameLogic->getShowDynamicLOD())
{
@@ -2941,227 +2945,17 @@ 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()
{
WW3D::Toggle_Movie_Capture("Movie",30);
}
+void W3DDisplay::takeScreenShot(ScreenshotFormat format, Int jpegQuality)
+{
+ W3D_TakeCompressedScreenshot(format, jpegQuality);
+}
+
#if defined(RTS_DEBUG)
diff --git a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
index ee27b4cc7a0..c2219ec7d2e 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, Int jpegQuality) 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..a3a54b96f8c 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; // TheSuperHackers @feature Quality for JPEG screenshots.
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..5e27ea5fb0c 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
+ MSG_META_TAKE_SCREENSHOT_PNG, ///< TheSuperHackers @feature Take lossless PNG screenshot
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..27352e4f00d 100644
--- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
+++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt
@@ -213,6 +213,7 @@ target_link_libraries(z_gameenginedevice PRIVATE
corei_gameenginedevice_private
zi_always
zi_main
+ stb
)
target_link_libraries(z_gameenginedevice PUBLIC
diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h
index c43ec2691a7..ade19e0ef83 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, Int jpegQuality) 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"
@@ -1804,6 +1805,9 @@ void W3DDisplay::draw()
if (TheGlobalData->m_headless)
return;
+ // TheSuperHackers @feature bobtista 10/07/2026 Show messages for screenshots finished by the screenshot thread.
+ W3D_UpdateScreenshotMessages();
+
updateAverageFPS();
if (TheGlobalData->m_enableDynamicLOD && TheGameLogic->getShowDynamicLOD())
{
@@ -3053,227 +3057,17 @@ 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()
{
WW3D::Toggle_Movie_Capture("Movie",30);
}
+void W3DDisplay::takeScreenShot(ScreenshotFormat format, Int jpegQuality)
+{
+ W3D_TakeCompressedScreenshot(format, jpegQuality);
+}
+
#if defined(RTS_DEBUG)
diff --git a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
index 9e432ecb827..ddd7b84bafb 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, Int jpegQuality) 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..26481b8357e
--- /dev/null
+++ b/cmake/stb.cmake
@@ -0,0 +1,19 @@
+# Fetch the stb library for writing JPEG and PNG screenshots.
+
+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/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