Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2e0fee7
Add stb library dependency via FetchContent
bobtista Nov 2, 2025
6f720c4
Add MSG_META_TAKE_SCREENSHOT_COMPRESSED message and F11 keybinding
bobtista Nov 2, 2025
dc9a753
Add takeScreenShotCompressed() to Display interface
bobtista Nov 2, 2025
d4f6ebc
Implement threaded JPEG screenshot for GeneralsMD
bobtista Nov 2, 2025
e42eb10
Implement threaded JPEG screenshot for Generals
bobtista Nov 2, 2025
59d53b6
Link stb library to GameEngineDevice targets
bobtista Nov 3, 2025
5e4d22a
Remove excessive comments from screenshot implementation
bobtista Nov 3, 2025
37bd840
Use Win32 CreateThread for VC6 compatibility and add GUIEditDisplay stub
bobtista Nov 3, 2025
66ac4b1
Move screenshot logic to Core to eliminate code duplication
bobtista Nov 3, 2025
2e9a56f
Add shared screenshot implementation in Core
bobtista Nov 3, 2025
a7a4b4e
Change F12 to JPEG and add CTRL+F12 for PNG screenshots
bobtista Nov 3, 2025
1c23940
Remove old BMP screenshot code
bobtista Nov 3, 2025
5d269f7
Add JPEGQuality option to Options.ini (default 80)
bobtista Nov 3, 2025
e2a07c4
Move W3DScreenshot implementation to game-specific directories
bobtista Nov 3, 2025
2e119cc
Fix include order for VC6 precompiled headers
bobtista Nov 3, 2025
8733c83
Remove default parameter from function definition
bobtista Nov 3, 2025
4e9c510
Move STB implementation to separate file to avoid PCH issues
bobtista Nov 3, 2025
efc773f
Include screenshot implementation directly in W3DDisplay.cpp to avoid…
bobtista Nov 3, 2025
f8162f3
Use Windows constants and switch statement in screenshot code
bobtista Nov 3, 2025
d197bdd
Use vcpkg for stb dependency with FetchContent fallback
bobtista Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions Core/GameEngineDevice/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScreenshot.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include "GameClient/Display.h"

void W3D_TakeCompressedScreenshot(ScreenshotFormat format, int quality = 80);

1 change: 1 addition & 0 deletions Generals/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion Generals/Code/GameEngine/Include/Common/MessageStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions Generals/Code/GameEngine/Include/Common/UserPreferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 7 additions & 1 deletion Generals/Code/GameEngine/Include/GameClient/Display.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Generals/Code/GameEngine/Source/Common/GlobalData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,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();
Expand Down
1 change: 1 addition & 0 deletions Generals/Code/GameEngine/Source/Common/MessageStream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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)
{
Expand Down
13 changes: 12 additions & 1 deletion Generals/Code/GameEngineDevice/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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); ///<enabled letter-boxed display
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ static void drawFramerateBar(void);
#include <time.h>

// USER INCLUDES //////////////////////////////////////////////////////////////
#include "W3DDevice/GameClient/W3DScreenshot.h"
#include "Common/FramePacer.h"
#include "Common/ThingFactory.h"
#include "Common/GlobalData.h"
Expand Down Expand Up @@ -2885,142 +2886,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
strcpy(pathname, TheGlobalData->getPath_UserData().str());
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; y<height; y++)
{
for (x=0; x<width; x++)
{
// index for image
index=3*(x+y*width);
// index for fb
index2=y*lrect.Pitch+4*x;

image[index]=*((char *) lrect.pBits + index2+2);
image[index+1]=*((char *) lrect.pBits + index2+1);
image[index+2]=*((char *) lrect.pBits + index2+0);
}
}

surfaceCopy->Unlock();
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; y<height; y++)
{
for (x=0; x<width; x++)
{
// index for image
index=3*(x+y*width);
// index for fb
index2=y*lrect.Pitch+4*x;

image[index]=*((char *) lrect.pBits + index2+0);
image[index+1]=*((char *) lrect.pBits + index2+1);
image[index+2]=*((char *) lrect.pBits + index2+2);
}
}

surfaceCopy->Unlock();
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)
Expand Down
Loading
Loading