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