Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions Core/GameEngine/Include/Common/OptionPreferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
typedef UnsignedInt CursorCaptureMode;
typedef UnsignedInt ScreenEdgeScrollMode;

// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
// Upper bound for the auto-leave countdown, so a hand edited preference cannot produce a
// pathological timer. Well above the longest duration the user interface offers.
const UnsignedInt AUTO_LEAVE_ON_DEFEAT_MAX_SECONDS = 3600;

//-----------------------------------------------------------------------------
// OptionsPreferences options menu class
//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -131,6 +136,10 @@ class OptionPreferences : public UserPreferences

Bool getShowMoneyPerMinute() const;

// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
UnsignedInt getAutoLeaveOnDefeatSeconds() const;
void setAutoLeaveOnDefeatSeconds(UnsignedInt seconds);

Real getGameWindowTransitionSpeedMultiplier() const;

Int getObserverStatsFontSize(void);
Expand Down
12 changes: 12 additions & 0 deletions Core/GameEngine/Include/GameNetwork/GameInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,15 @@ class GameInfo
inline Bool oldFactionsOnly() const;
inline void setOldFactionsOnly( Bool oldFactionsOnly );

// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
// Host enforced auto-leave duration, 0 when the host does not enforce one and the local
// preference applies instead. Deliberately not part of GameInfoToAsciiString(), the slot list
// xfer, or the replay header: online lobbies carry this in the backend lobby record, so keeping
// it out of the ascii options string avoids breaking the wire format for older clients and
// avoids arming a countdown during replay playback.
inline UnsignedInt getAutoLeaveSeconds() const;
inline void setAutoLeaveSeconds( UnsignedInt seconds );

protected:
Int m_preorderMask;
Int m_crcInterval;
Expand All @@ -253,6 +262,7 @@ class GameInfo
Money m_startingCash;
UnsignedShort m_superweaponRestriction;
Bool m_oldFactionsOnly; // Only USA, China, GLA -- not USA Air Force General, GLA Toxic General, et al
UnsignedInt m_autoLeaveSeconds; // TheSuperHackers @feature JawadYzbk 16/09/2026 host enforced auto-leave on defeat, 0 = not enforced
};

extern GameInfo *TheGameInfo;
Expand All @@ -274,6 +284,8 @@ const Money&GameInfo::getStartingCash() const { return m_startingCash; }
UnsignedShort GameInfo::getSuperweaponRestriction() const { return m_superweaponRestriction; }
Bool GameInfo::oldFactionsOnly() const { return m_oldFactionsOnly; }
void GameInfo::setOldFactionsOnly( Bool oldFactionsOnly ) { m_oldFactionsOnly = oldFactionsOnly; }
UnsignedInt GameInfo::getAutoLeaveSeconds() const { return m_autoLeaveSeconds; }
void GameInfo::setAutoLeaveSeconds( UnsignedInt seconds ) { m_autoLeaveSeconds = seconds; }

AsciiString GameInfoToAsciiString( const GameInfo *game );
Bool ParseAsciiStringToGameInfo( GameInfo *game, AsciiString options );
Expand Down
29 changes: 29 additions & 0 deletions Core/GameEngine/Source/Common/OptionPreferences.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -977,3 +977,32 @@ Real OptionPreferences::getGameWindowTransitionSpeedMultiplier() const
Real speed = (Real) atof(it->second.str());
return clamp(1.0f, speed, 1000.0f);
}

// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
// Returns the number of seconds a defeated local player waits before the client returns to
// the score screen. Zero disables the feature, which is the default.
UnsignedInt OptionPreferences::getAutoLeaveOnDefeatSeconds() const
{
OptionPreferences::const_iterator it = find("AutoLeaveOnDefeatSeconds");
if (it == end())
return 0;

Int seconds = atoi(it->second.str());
if (seconds <= 0)
return 0;
if (seconds > AUTO_LEAVE_ON_DEFEAT_MAX_SECONDS)
seconds = AUTO_LEAVE_ON_DEFEAT_MAX_SECONDS;

return (UnsignedInt)seconds;
}

// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
void OptionPreferences::setAutoLeaveOnDefeatSeconds(UnsignedInt seconds)
{
if (seconds > AUTO_LEAVE_ON_DEFEAT_MAX_SECONDS)
seconds = AUTO_LEAVE_ON_DEFEAT_MAX_SECONDS;

AsciiString prefString;
prefString.format("%u", seconds);
(*this)["AutoLeaveOnDefeatSeconds"] = prefString;
}
2 changes: 2 additions & 0 deletions Core/GameEngine/Source/GameNetwork/GameInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ void GameInfo::reset()
m_useStats = TRUE;
m_surrendered = FALSE;
m_oldFactionsOnly = FALSE;
// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
m_autoLeaveSeconds = 0;
// m_localIP = 0; // BGC - actually we don't want this to be reset since the m_localIP is
// set properly in the constructor of LANGameInfo which uses this as a base class.
m_mapCRC = 0;
Expand Down
2 changes: 1 addition & 1 deletion Generals/Code/GameEngine/Include/Common/GameCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@

// ----------------------------------------------------------------------------------------------
#include "Lib/BaseType.h"
#include "WWCommon.h"
#include "WWLib/WWCommon.h"
#include "Common/GameDefines.h"

// ----------------------------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions Generals/Code/GameEngine/Include/Common/GameEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class ParticleSystemManager;
*/
class GameEngine : public SubsystemInterface
{
public:

GameEngine();
virtual ~GameEngine() override;
Expand Down
4 changes: 2 additions & 2 deletions Generals/Code/GameEngine/Include/GameLogic/GameLogic.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ enum BuildableStatus CPP_11(: Int);
typedef const CommandButton* ConstCommandButtonPtr;

// What kind of game we're in.
enum
enum GameMode CPP_11(: Int)
{
GAME_SINGLE_PLAYER,
GAME_LAN,
Expand Down Expand Up @@ -435,7 +435,7 @@ class GameLogic : public SubsystemInterface, public Snapshot
virtual TerrainLogic *createTerrainLogic();
virtual GhostObjectManager *createGhostObjectManager(bool dummy = false);

Int m_gameMode;
GameMode m_gameMode;
Int m_rankLevelLimit;

LoadScreen *getLoadScreen( Bool saveGame );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ struct LobbyEntry
bool track_stats = false;
bool allow_observers = false;
uint16_t max_cam_height = 0;
// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
// Host enforced auto-leave on defeat, in seconds. 0 means the host does not enforce one.
uint16_t auto_leave_seconds = 0;

uint32_t exe_crc = 0;
uint32_t ini_crc = 0;
Expand Down Expand Up @@ -210,6 +213,8 @@ class NGMP_OnlineServices_LobbyInterface
void UpdateCurrentLobby_Map(AsciiString strMap, AsciiString strMapPath, bool bIsOfficial, int newMaxPlayers);
void UpdateCurrentLobby_LimitSuperweapons(bool bLimitSuperweapons);
void UpdateCurrentLobby_StartingCash(UnsignedInt startingCashValue);
// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
void UpdateCurrentLobby_AutoLeave(UnsignedInt autoLeaveSeconds);

void UpdateCurrentLobby_HasMap();

Expand Down
10 changes: 10 additions & 0 deletions GeneralsMD/Code/GameEngine/Source/Common/StatsExporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,17 @@
#include "GameLogic/Module/BattlePlanUpdate.h"

#include <stdio.h>

// TheSuperHackers @build JawadYzbk 16/09/2026 The engine typedefs Byte as char in
// BaseTypeCore.h, while zlib's zconf.h typedefs it as unsigned char, so any translation unit
// that pulls in both fails to compile. zconf.h skips its own typedef when __MACTYPES__ is
// defined, which is its documented hook for hosts that already provide a Byte. Only the gz*
// file API is used below and none of it mentions Byte or Bytef; note that Bytef does resolve
// to the engine's Byte here, so the byte oriented zlib calls (compress, inflate, ...) must not
// be used in this file. Use core_compression for those instead.
#define __MACTYPES__
#include <zlib.h>
#undef __MACTYPES__

#include "GameNetwork/GeneralsOnline/json.hpp"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,33 @@ static GameWindow * checkRetaliation = nullptr;
static NameKeyType checkDoubleClickAttackMoveID = NAMEKEY_INVALID;
static GameWindow * checkDoubleClickAttackMove = nullptr;

// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
static NameKeyType comboBoxAutoLeaveOnDefeatID = NAMEKEY_INVALID;
static GameWindow * comboBoxAutoLeaveOnDefeat = nullptr;

// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
// The durations offered in the options menu. Off is first so the feature reads as disabled by
// default. The seconds value is stored as the combo box item data, so the order of this table can
// change without invalidating anything already saved to Options.ini.
struct AutoLeaveOnDefeatOption
{
UnsignedInt seconds;
const char *label;
};

static const AutoLeaveOnDefeatOption autoLeaveOnDefeatOptions[] =
{
{ 0, "GUI:AutoLeaveOnDefeatOff" },
{ 30, "GUI:AutoLeaveOnDefeat30" },
{ 60, "GUI:AutoLeaveOnDefeat60" },
{ 120, "GUI:AutoLeaveOnDefeat120" },
{ 180, "GUI:AutoLeaveOnDefeat180" },
{ 300, "GUI:AutoLeaveOnDefeat300" },
};

static const Int autoLeaveOnDefeatOptionCount =
sizeof(autoLeaveOnDefeatOptions) / sizeof(autoLeaveOnDefeatOptions[0]);

static NameKeyType sliderScrollSpeedID = NAMEKEY_INVALID;
static GameWindow * sliderScrollSpeed = nullptr;

Expand Down Expand Up @@ -615,6 +642,20 @@ static void saveOptions()
TheWritableGlobalData->m_doubleClickAttackMove = GadgetCheckBoxIsChecked( checkDoubleClickAttackMove );
(*pref)["UseDoubleClickAttackMove"] = TheWritableGlobalData->m_doubleClickAttackMove ? "yes" : "no";

// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
// Only written when the control is present, so an unmodified window asset leaves any existing
// preference untouched rather than resetting it to Off.
if (comboBoxAutoLeaveOnDefeat)
{
Int autoLeaveIndex = -1;
GadgetComboBoxGetSelectedPos( comboBoxAutoLeaveOnDefeat, &autoLeaveIndex );
if (autoLeaveIndex >= 0)
{
UnsignedInt seconds = (UnsignedInt)GadgetComboBoxGetItemData( comboBoxAutoLeaveOnDefeat, autoLeaveIndex );
pref->setAutoLeaveOnDefeatSeconds( seconds );
}
}

// TheSuperHackers @todo Add combo box ?
{
CursorCaptureMode mode = pref->getCursorCaptureMode();
Expand Down Expand Up @@ -1002,6 +1043,10 @@ void OptionsMenuInit( WindowLayout *layout, void *userData )
checkRetaliation = TheWindowManager->winGetWindowFromId( nullptr, checkRetaliationID);
checkDoubleClickAttackMoveID = TheNameKeyGenerator->nameToKey( "OptionsMenu.wnd:CheckDoubleClickAttackMove" );
checkDoubleClickAttackMove = TheWindowManager->winGetWindowFromId( nullptr, checkDoubleClickAttackMoveID );
// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
// Stays null when running against unmodified window assets, so every use below is guarded.
comboBoxAutoLeaveOnDefeatID = TheNameKeyGenerator->nameToKey( "OptionsMenu.wnd:ComboBoxAutoLeaveOnDefeat" );
comboBoxAutoLeaveOnDefeat = TheWindowManager->winGetWindowFromId( nullptr, comboBoxAutoLeaveOnDefeatID );
sliderScrollSpeedID = TheNameKeyGenerator->nameToKey( "OptionsMenu.wnd:SliderScrollSpeed" );
sliderScrollSpeed = TheWindowManager->winGetWindowFromId( nullptr, sliderScrollSpeedID);
comboBoxAntiAliasingID = TheNameKeyGenerator->nameToKey( "OptionsMenu.wnd:ComboBoxAntiAliasing" );
Expand Down Expand Up @@ -1235,6 +1280,40 @@ void OptionsMenuInit( WindowLayout *layout, void *userData )
}
GadgetComboBoxSetSelectedPos(comboBoxAntiAliasing, pos);

// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
// Populate the auto-leave durations. The seconds are carried as item data rather than inferred
// from the entry position, so a hand edited Options.ini value that is not one of the offered
// durations can be preserved as an extra entry instead of being silently rewritten on save.
if (comboBoxAutoLeaveOnDefeat)
{
const UnsignedInt savedSeconds = pref->getAutoLeaveOnDefeatSeconds();
Int selectedPos = 0;

GadgetComboBoxReset(comboBoxAutoLeaveOnDefeat);

for (Int opt = 0; opt < autoLeaveOnDefeatOptionCount; ++opt)
{
const AutoLeaveOnDefeatOption &option = autoLeaveOnDefeatOptions[opt];
UnicodeString optionText = TheGameText->fetch( option.label );
Int optionIndex = GadgetComboBoxAddEntry( comboBoxAutoLeaveOnDefeat, optionText, color );
GadgetComboBoxSetItemData( comboBoxAutoLeaveOnDefeat, optionIndex, (void *)option.seconds );

if (option.seconds == savedSeconds)
selectedPos = optionIndex;
}

if (savedSeconds != 0 && selectedPos == 0)
{
// Not one of the offered durations, so show it rather than lose it.
UnicodeString customText;
customText.format( TheGameText->fetch("GUI:AutoLeaveOnDefeatCustom").str(), savedSeconds );
selectedPos = GadgetComboBoxAddEntry( comboBoxAutoLeaveOnDefeat, customText, color );
GadgetComboBoxSetItemData( comboBoxAutoLeaveOnDefeat, selectedPos, (void *)savedSeconds );
}

GadgetComboBoxSetSelectedPos( comboBoxAutoLeaveOnDefeat, selectedPos );
}

// get resolution from saved preferences file
AsciiString selectedResolution = (*pref) ["Resolution"];
Int selectedXRes=DEFAULT_DISPLAY_WIDTH;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3514,6 +3514,8 @@ Bool handleGameSetupSlashCommands(UnicodeString uText)
GadgetListBoxAddEntryText(listboxGameSetupChat, UnicodeString(L"/setpassword <password> - Set a lobby password (host only)."), helpColor, -1, -1);
GadgetListBoxAddEntryText(listboxGameSetupChat, UnicodeString(L"/removepassword - Remove the lobby password (host only)."), helpColor, -1, -1);
GadgetListBoxAddEntryText(listboxGameSetupChat, UnicodeString(L"/maxcameraheight <value> - Set the camera height limit (host only)."), helpColor, -1, -1);
// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
GadgetListBoxAddEntryText(listboxGameSetupChat, UnicodeString(L"/autoleave <seconds> - Defeated players return to the score screen after this long. 0 is off (host only)."), helpColor, -1, -1);
// GadgetListBoxAddEntryText(listboxGameSetupChat, UnicodeString(L"/leave - Return to the main lobby."), helpColor, -1, -1);
// GadgetListBoxAddEntryText(listboxGameSetupChat, UnicodeString(L"/quit - Exit the game."), helpColor, -1, -1);
GadgetListBoxAddEntryText(listboxGameSetupChat, UnicodeString(L"/support - Open the GeneralsOnline Discord."), helpColor, -1, -1);
Expand Down Expand Up @@ -3633,6 +3635,75 @@ Bool handleGameSetupSlashCommands(UnicodeString uText)
}
}

return TRUE; // was a slash command
}
// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown.
// Mirrors /maxcameraheight: host only, numeric argument, validated before it is sent.
else if (token == "autoleave" && uText.getLength() > 11)
{
NGMP_OnlineServicesManager* pOnlineServicesMgr = NGMP_OnlineServicesManager::GetInstance();
if (pOnlineServicesMgr != nullptr)
{
NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface<NGMP_OnlineServices_LobbyInterface>();

if (pLobbyInterface != nullptr)
{
if (pLobbyInterface->IsInLobby())
{
if (pLobbyInterface->IsHost())
{
UnicodeString val = UnicodeString(uText.str() + 11); // skip the command

AsciiString asciiVal;
asciiVal.translate(val);

bool bIsNumber = asciiVal.getLength() > 0;

for (int i = 0; i < asciiVal.getLength(); ++i)
{
char thisChar = asciiVal.getCharAt(i);
if (!std::isdigit((unsigned char)thisChar))
{
bIsNumber = false;
break;
}
}

if (!bIsNumber)
{
GadgetListBoxAddEntryText(listboxGameSetupChat, UnicodeString(L"Auto-leave: Enter a number of seconds, or 0 to turn it off."), GameMakeColor(255, 0, 0, 255), -1, -1);
return TRUE; // was a slash command
}

const UnsignedInt newAutoLeave = (UnsignedInt)atoi(asciiVal.str());

if (newAutoLeave > AUTO_LEAVE_ON_DEFEAT_MAX_SECONDS)
{
UnicodeString msg;
msg.format(L"Auto-leave: Enter a value from 0 to %d seconds.", AUTO_LEAVE_ON_DEFEAT_MAX_SECONDS);
GadgetListBoxAddEntryText(listboxGameSetupChat, msg, GameMakeColor(255, 0, 0, 255), -1, -1);
return TRUE; // was a slash command
}

// update lobby
pLobbyInterface->UpdateCurrentLobby_AutoLeave(newAutoLeave);

UnicodeString msg;
if (newAutoLeave == 0)
msg = UnicodeString(L"Auto-leave: Off. Each player's own setting applies.");
else
msg.format(L"Auto-leave: Defeated players return to the score screen after %d seconds.", newAutoLeave);
GadgetListBoxAddEntryText(listboxGameSetupChat, msg, GameMakeColor(0, 255, 0, 255), -1, -1);
}
else
{
GadgetListBoxAddEntryText(listboxGameSetupChat, UnicodeString(L"Auto-leave: Only the host can change it."), GameMakeColor(255, 0, 0, 255), -1, -1);
return TRUE; // was a slash command
}
}
}
}

return TRUE; // was a slash command
}
#endif
Expand Down
Loading
Loading