Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
13430b4
Add stb library dependency via FetchContent
bobtista Nov 2, 2025
d53fafd
Add MSG_META_TAKE_SCREENSHOT_COMPRESSED message and F11 keybinding
bobtista Nov 2, 2025
50ed01e
Implement threaded JPEG screenshot for GeneralsMD
bobtista Nov 2, 2025
c8a27bc
Use Win32 CreateThread for VC6 compatibility and add GUIEditDisplay stub
bobtista Nov 3, 2025
c346f1b
Move screenshot logic to Core to eliminate code duplication
bobtista Nov 3, 2025
aa9dd09
Change F12 to JPEG and add CTRL+F12 for PNG screenshots
bobtista Nov 3, 2025
7fcbb01
Remove old BMP screenshot code
bobtista Nov 3, 2025
0eb7821
Add JPEGQuality option to Options.ini (default 80)
bobtista Nov 3, 2025
1c45436
Remove trailing whitespace from empty lines
bobtista Nov 22, 2025
12858a9
Fix copyright headers: Change Electronic Arts Inc. to TheSuperHackers
bobtista Nov 22, 2025
7ac1a95
Add file headers to W3DScreenshot.cpp files
bobtista Nov 22, 2025
98ae29f
Rename MSG_META_TAKE_SCREENSHOT_JPEG to MSG_META_TAKE_SCREENSHOT_PNG
bobtista Nov 22, 2025
13108c2
fix(screenshot): Fix W3DScreenshot.cpp references in CMakeLists after…
bobtista Dec 3, 2025
1f46d55
fix(screenshot): Add missing W3DScreenshot.h include for ScreenshotFo…
bobtista Dec 3, 2025
8eb0036
fix(screenshot): Use full include path for W3DScreenshot.h from Core
bobtista Dec 3, 2025
f0e1a03
fix(screenshot): Remove W3DScreenshot.cpp from separate compilation s…
bobtista Dec 3, 2025
14ca7a0
refactor(screenshot): Consolidate stb_image_write_impl.cpp to Core
bobtista Dec 3, 2025
df506b5
docs(unify): Document stb_image_write_impl.cpp unification in script
bobtista Dec 3, 2025
dd65a74
fix(screenshot): Restore DESTROY_MESSAGE, fix quality default, NULL t…
bobtista Feb 16, 2026
516acb2
fix(screenshot): Replicate screenshot fixes to Generals
bobtista Feb 16, 2026
7e3c765
refactor(screenshot): Compile W3DScreenshot.cpp as separate translati…
bobtista Feb 16, 2026
65867f4
fix(screenshot): Replicate m_jpegQuality init and CMake cleanup to Ge…
bobtista Feb 16, 2026
45c0339
refactor(screenshot): Replicate separate compilation fix to Generals
bobtista Feb 16, 2026
a6f02b9
fix(screenshot): Initialize m_jpegQuality in constructor and remove e…
bobtista Feb 16, 2026
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 @@ -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.
Expand Down
1 change: 1 addition & 0 deletions Core/GameEngine/Include/Common/UserPreferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions Core/GameEngineDevice/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,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
Expand Down Expand Up @@ -173,6 +174,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
Expand Down Expand Up @@ -218,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 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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include "GameClient/Display.h"

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

Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>

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 @@ -142,6 +142,7 @@ class GlobalData : public SubsystemInterface
Bool m_clientRetaliationModeEnabled;
Bool m_doubleClickAttackMove;
Bool m_rightMouseAlwaysScrolls;
Int m_jpegQuality;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_jpegQuality is never initialized in the constructor

Every neighboring member in this struct (m_rightMouseAlwaysScrolls at GlobalData.cpp:647, m_useWaterPlane at GlobalData.cpp:648, etc.) is explicitly initialized in the GlobalData::GlobalData() constructor, but m_jpegQuality is not. It only gets set later in parseGameDataDefinition via optionPref.getJPEGQuality().

This means m_jpegQuality holds an indeterminate value between construction and the parseGameDataDefinition call. While screenshots are unlikely to be taken in that window during normal gameplay, this is still a correctness defect that should be fixed for consistency and safety. The same issue exists in the GeneralsMD mirror file.

Add initialization in both GlobalData::GlobalData() constructors, for example after m_rightMouseAlwaysScrolls = FALSE;:

m_jpegQuality = 80;
Prompt To Fix With AI
This is a comment left during a code review.
Path: Generals/Code/GameEngine/Include/Common/GlobalData.h
Line: 145:145

Comment:
**`m_jpegQuality` is never initialized in the constructor**

Every neighboring member in this struct (`m_rightMouseAlwaysScrolls` at `GlobalData.cpp:647`, `m_useWaterPlane` at `GlobalData.cpp:648`, etc.) is explicitly initialized in the `GlobalData::GlobalData()` constructor, but `m_jpegQuality` is not. It only gets set later in `parseGameDataDefinition` via `optionPref.getJPEGQuality()`.

This means `m_jpegQuality` holds an indeterminate value between construction and the `parseGameDataDefinition` call. While screenshots are unlikely to be taken in that window during normal gameplay, this is still a correctness defect that should be fixed for consistency and safety. The same issue exists in the GeneralsMD mirror file.

Add initialization in both `GlobalData::GlobalData()` constructors, for example after `m_rightMouseAlwaysScrolls = FALSE;`:
```cpp
m_jpegQuality = 80;
```

How can I resolve this? If you propose a fix, please make it concise.

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_PNG, ///< take PNG screenshot (CTRL+F12, lossless)
MSG_META_ALL_CHEER, ///< Yay! :)
MSG_META_TOGGLE_ATTACKMOVE, ///< enter attack-move mode

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 @@ -33,6 +33,12 @@
#include "GameClient/GameFont.h"
#include "GameClient/View.h"

enum ScreenshotFormat
{
SCREENSHOT_JPEG,
SCREENSHOT_PNG
};

struct ShroudLevel
{
Short m_currentShroud; ///< A Value of 1 means shrouded. 0 is not. Negative is the count of people looking.
Expand Down Expand Up @@ -166,7 +172,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
2 changes: 2 additions & 0 deletions Generals/Code/GameEngine/Source/Common/GlobalData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,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!
Expand Down Expand Up @@ -1189,6 +1190,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();
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_PNG)
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 @@ -354,6 +354,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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3418,7 +3418,15 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage
case GameMessage::MSG_META_TAKE_SCREENSHOT:
{
if (TheDisplay)
TheDisplay->takeScreenShot();
TheDisplay->takeScreenShot(SCREENSHOT_JPEG);
disp = DESTROY_MESSAGE;
break;
}

case GameMessage::MSG_META_TAKE_SCREENSHOT_PNG:
{
if (TheDisplay)
TheDisplay->takeScreenShot(SCREENSHOT_PNG);
disp = DESTROY_MESSAGE;
break;
}
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_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 },
Expand Down Expand Up @@ -813,6 +814,26 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t)
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)
{
Expand Down
2 changes: 2 additions & 0 deletions Generals/Code/GameEngineDevice/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ set(GAMEENGINEDEVICE_SRC
Source/W3DDevice/GameClient/W3DDebugDisplay.cpp
Source/W3DDevice/GameClient/W3DDebugIcons.cpp
Source/W3DDevice/GameClient/W3DDisplay.cpp
Source/W3DDevice/GameClient/W3DScreenshot.cpp
Source/W3DDevice/GameClient/W3DDisplayString.cpp
Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp
Source/W3DDevice/GameClient/W3DDynamicLight.cpp
Expand Down Expand Up @@ -200,6 +201,7 @@ target_link_libraries(g_gameenginedevice PRIVATE
corei_gameenginedevice_private
gi_always
gi_main
stb
)

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 @@ -2879,142 +2880,6 @@ 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(), 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; 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 = 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; 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 = 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(void)
Expand Down
Loading
Loading