From 7efe1cdf2dcf67bf1423dbaff2e911b7a0f00405 Mon Sep 17 00:00:00 2001 From: JawadYzbk Date: Wed, 16 Sep 2026 15:29:15 +0300 Subject: [PATCH 1/4] build: Fix Generals game engine compile errors The Generals target does not compile. Each of these is a partially applied upstream change that left Generals inconsistent with itself, while GeneralsMD received the complete change. - Common/GameCommon.h includes "WWCommon.h" unqualified, which cannot resolve because only the WWVegas roots are on the include path. Qualifies it as "WWLib/WWCommon.h", matching what Core/GameEngine/Include/Common/GameCommon.h already does. - Common/GameEngine.h is missing the public access specifier, so the constructor, destructor and virtual overrides are all private and GameMain() cannot call init() or execute(). - GameLogic/GameLogic.h declares the game mode enum anonymously while the same header declares toString(GameMode) and getGameMode() returns GameMode, and m_gameMode is declared Int while GameLogic.cpp assigns it to a GameMode. Names the enum and corrects the member type to match GeneralsMD. Together these clear 452 of the 687 errors the target produced. The remainder are separate gaps and are not addressed here. --- Generals/Code/GameEngine/Include/Common/GameCommon.h | 2 +- Generals/Code/GameEngine/Include/Common/GameEngine.h | 1 + Generals/Code/GameEngine/Include/GameLogic/GameLogic.h | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/GameCommon.h b/Generals/Code/GameEngine/Include/Common/GameCommon.h index 1d4a1c309d0..51d38e56ab3 100644 --- a/Generals/Code/GameEngine/Include/Common/GameCommon.h +++ b/Generals/Code/GameEngine/Include/Common/GameCommon.h @@ -49,7 +49,7 @@ // ---------------------------------------------------------------------------------------------- #include "Lib/BaseType.h" -#include "WWCommon.h" +#include "WWLib/WWCommon.h" #include "Common/GameDefines.h" // ---------------------------------------------------------------------------------------------- diff --git a/Generals/Code/GameEngine/Include/Common/GameEngine.h b/Generals/Code/GameEngine/Include/Common/GameEngine.h index 97d8f1c25c5..e2ee030a7a3 100644 --- a/Generals/Code/GameEngine/Include/Common/GameEngine.h +++ b/Generals/Code/GameEngine/Include/Common/GameEngine.h @@ -57,6 +57,7 @@ class ParticleSystemManager; */ class GameEngine : public SubsystemInterface { +public: GameEngine(); virtual ~GameEngine() override; diff --git a/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h b/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h index b156c3206fb..33410e14eb3 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -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, @@ -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 ); From 2a95f9f405069c2fde5a688fa24d1e5560d26731 Mon Sep 17 00:00:00 2001 From: JawadYzbk Date: Wed, 16 Sep 2026 15:29:24 +0300 Subject: [PATCH 2/4] build: Fix Byte redefinition between zlib and the engine in StatsExporter StatsExporter.cpp is the only translation unit that includes both the engine headers and . BaseTypeCore.h typedefs Byte as char while zlib's zconf.h typedefs it as unsigned char, so the file fails to compile whenever it is rebuilt. CompressionManager.cpp includes the same header without trouble because it never pulls in the engine types. zconf.h skips its own Byte typedef when __MACTYPES__ is defined, which is its documented hook for hosts that already provide the type. Only the gz* file API is used here and none of it mentions Byte or Bytef. Note that Bytef resolves to the engine's Byte as a result, so the byte oriented zlib calls must not be used in this file. Use core_compression for those. The comment at the include says so. --- .../Code/GameEngine/Source/Common/StatsExporter.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/StatsExporter.cpp b/GeneralsMD/Code/GameEngine/Source/Common/StatsExporter.cpp index 48631a04660..45b4e5d3dc3 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/StatsExporter.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/StatsExporter.cpp @@ -33,7 +33,17 @@ #include "GameLogic/Module/BattlePlanUpdate.h" #include + +// 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 +#undef __MACTYPES__ #include "GameNetwork/GeneralsOnline/json.hpp" From 5149e0a14fe694063746f74b28c23aeef2f33ec9 Mon Sep 17 00:00:00 2001 From: JawadYzbk Date: Wed, 16 Sep 2026 15:29:43 +0300 Subject: [PATCH 3/4] feat: Add optional auto-leave on defeat countdown A defeated player in a LAN or online match is left sitting in a passive spectating state until they quit by hand. This adds an opt-in countdown that returns their client to the score screen on its own. Disabled by default. The player chooses a duration in the options menu: Off, 30 seconds, 1, 2, 3 or 5 minutes. The behaviour is purely local to the defeated player's own client. It changes no simulation state and is observable only by the player it happens to, so it carries no desync surface, needs no agreement between peers, and only automates the quit that player can already perform by hand. It is therefore driven by a local preference rather than the game options wire format, which leaves GameInfoToAsciiString, the replay header and the skirmish save version untouched. Countdown behaviour: - Runs in LAN and online games only. Skirmish is excluded because a defeated skirmish player restarts rather than sitting out a match. Campaign and replay playback are excluded by the same predicate. - Never arms while an ally is still alive. VictoryConditions marks a defeated player victorious if a surviving ally goes on to win, so leaving early would discard a win the player is still entitled to. Re-tested every frame rather than once at defeat, because with three or more alliances the local alliance can be wiped out while the match carries on between the others. - Never arms for observers. - Cancels once a single alliance remains, so it cannot race the normal end of match path into the score screen. - Frame based, so it freezes while the game is paused and follows the game speed setting rather than the wall clock. - Calls exitGame() rather than quit(), which would either open the quit menu or self destruct the player in a multiplayer game. - Latches after firing, because exitGame() only posts a deferred MSG_CLEAR_GAME_DATA and update() keeps running for several frames afterwards. The preference is resolved once during map load rather than at defeat time so the file read never lands mid match. Values are clamped on read, so a hand edited Options.ini cannot produce a pathological timer. The options menu control is null guarded on every use and the save path only writes when the control is present, so a client running against unmodified window assets degrades to the feature being off instead of crashing or silently resetting an existing preference. A duration in Options.ini that is not one of the offered ones is shown as an extra entry rather than being rewritten on save. Requires new window and string assets, see the pull request description. --- .../Include/Common/OptionPreferences.h | 9 ++ .../Source/Common/OptionPreferences.cpp | 29 ++++ .../GUI/GUICallbacks/Menus/OptionsMenu.cpp | 79 +++++++++ .../ScriptEngine/VictoryConditions.cpp | 152 ++++++++++++++++++ 4 files changed, 269 insertions(+) diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index d41945256a2..bd0c607dec6 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -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 //----------------------------------------------------------------------------- @@ -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); diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index 40b417abb4b..abb99098732 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -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; +} 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 9f33223decf..66c951a2307 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp @@ -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; @@ -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(); @@ -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" ); @@ -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; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp index 76fc18d6e41..529f724a9a0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/VictoryConditions.cpp @@ -34,6 +34,7 @@ #include "Common/GameEngine.h" #include "Common/GameUtility.h" #include "Common/KindOf.h" +#include "Common/OptionPreferences.h" #include "Common/PlayerList.h" #include "Common/Player.h" #include "Common/PlayerTemplate.h" @@ -97,6 +98,10 @@ class VictoryConditions : public VictoryConditionsInterface void markAllianceVictorious(Player* victoriousPlayer); ///< Mark the victorious player and his allies as victorious. Bool multipleAlliancesExist(); ///< Are there multiple alliances still alive? + // TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. + Bool hasUndefeatedAlly(Player* player); ///< Does this player still have a living ally? + void updateAutoLeaveOnDefeat(); ///< Arm, announce and fire the auto-leave countdown. + Player* m_players[MAX_PLAYER_COUNT]; Int m_localSlotNum; UnsignedInt m_endFrame; @@ -105,6 +110,12 @@ class VictoryConditions : public VictoryConditionsInterface Bool m_localPlayerDefeated; ///< prevents condition from being signaled each frame Bool m_singleAllianceRemaining; ///< prevents condition from being signaled each frame Bool m_isObserver; + + // TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. + UnsignedInt m_autoLeaveSeconds; ///< configured duration, 0 when the feature is off + UnsignedInt m_autoLeaveFrame; ///< frame the client leaves on, 0 when not counting down + UnsignedInt m_autoLeaveAnnounced; ///< last announced seconds remaining, so we only say it once + Bool m_autoLeaveFired; ///< prevents re-arming while the deferred exit is pending }; //------------------------------------------------------------------------------------------------- @@ -142,6 +153,12 @@ void VictoryConditions::reset() m_isObserver = false; m_endFrame = 0; + // TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. + m_autoLeaveSeconds = 0; + m_autoLeaveFrame = 0; + m_autoLeaveAnnounced = 0; + m_autoLeaveFired = false; + m_victoryConditions = VICTORY_NOBUILDINGS | VICTORY_NOUNITS; } @@ -249,6 +266,114 @@ void VictoryConditions::update() SetInGameChatType( INGAME_CHAT_EVERYONE ); // can't chat to allies after death. Only to other observers. } } + + // TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. + updateAutoLeaveOnDefeat(); +} + +// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. +// Announce sparsely while there is plenty of time left, then every second at the end, so a long +// countdown does not flood the message area. +//------------------------------------------------------------------------------------------------- +inline static Bool shouldAnnounceAutoLeave(UnsignedInt secondsLeft) +{ + if (secondsLeft > 60) + return (secondsLeft % 60) == 0; + + if (secondsLeft > 10) + return secondsLeft == 60 || secondsLeft == 30; + + return secondsLeft == 10 || secondsLeft <= 5; +} + +// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. +/** Does this player still have an ally that has not been defeated? areAllies() is false when a + * player is compared against itself, so only other players are considered. */ +//------------------------------------------------------------------------------------------------- +Bool VictoryConditions::hasUndefeatedAlly(Player *player) +{ + if (!player) + return false; + + for (Int i = 0; i < MAX_PLAYER_COUNT; ++i) + { + Player *other = m_players[i]; + if (other && areAllies(other, player) && !hasSinglePlayerBeenDefeated(other)) + return true; + } + + return false; +} + +// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. +/** Returns a defeated local player to the score screen after a configurable delay, so losing does + * not leave the client sitting in a spectating state. This is purely local: it automates the + * quit the player can already perform by hand, and changes no simulation state, so it is safe to + * drive from a local preference and cannot desync. Disabled unless the preference is set. */ +//------------------------------------------------------------------------------------------------- +void VictoryConditions::updateAutoLeaveOnDefeat() +{ + // exitGame() only posts a deferred MSG_CLEAR_GAME_DATA, so update() keeps running for a few + // frames afterwards. Without this the countdown would re-arm and re-announce on the way out. + if (m_autoLeaveSeconds == 0 || m_autoLeaveFired) + return; + + // Observers must never be counted down. cachePlayerPtrs() sets m_localPlayerDefeated for an + // observer, so testing m_isObserver here is load bearing and must not be removed. + if (m_isObserver || m_localSlotNum < 0) + return; + + const UnsignedInt now = TheGameLogic->getFrame(); + + // Defeat handling is suppressed on the opening frames of a match; match that. + if (now <= 1) + return; + + // Once a single alliance remains the match is already resolving and the score screen is coming + // on its own. Firing exitGame() into that transition would race the normal end of match path. + if (m_singleAllianceRemaining) + { + m_autoLeaveFrame = 0; + return; + } + + if (m_autoLeaveFrame == 0) + { + if (!m_localPlayerDefeated) + return; + + // A defeated player whose ally is still alive is still marked victorious if that ally wins, + // so leaving now would throw away a win they are still entitled to. This is re-tested every + // frame rather than once at defeat, because with three or more alliances the local alliance + // can be wiped out while the match carries on between the others. + if (hasUndefeatedAlly(m_players[m_localSlotNum])) + return; + + m_autoLeaveFrame = now + m_autoLeaveSeconds * LOGICFRAMES_PER_SECOND; + m_autoLeaveAnnounced = 0; + } + + if (now >= m_autoLeaveFrame) + { + m_autoLeaveFrame = 0; + m_autoLeaveFired = true; + // exitGame() returns to the score screen. Deliberately not quit(), which would either open + // the quit menu or self destruct the player in a multiplayer game. + TheGameLogic->exitGame(); + return; + } + + // Frame based, so the countdown follows game time: it freezes while the game is paused and + // tracks the game speed setting rather than the wall clock. + const UnsignedInt framesLeft = m_autoLeaveFrame - now; + const UnsignedInt secondsLeft = (framesLeft + LOGICFRAMES_PER_SECOND - 1) / LOGICFRAMES_PER_SECOND; + + const Bool firstAnnouncement = (m_autoLeaveAnnounced == 0); + if (secondsLeft != m_autoLeaveAnnounced && (firstAnnouncement || shouldAnnounceAutoLeave(secondsLeft))) + { + m_autoLeaveAnnounced = secondsLeft; + TheInGameUI->message("GUI:AutoLeaveOnDefeatCountdown", (Int)secondsLeft); + } } //------------------------------------------------------------------------------------------------- @@ -389,6 +514,33 @@ void VictoryConditions::cachePlayerPtrs() m_localPlayerDefeated = true; // if we have no local player, don't check for defeat m_isObserver = true; } + + // TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. + // Resolve the preference once per match, here rather than at defeat time, so the file read + // happens during map load instead of mid game. Never arm during replay playback: TheGameLogic + // points TheGameInfo at the recorded game, and a replay of a match the watched player lost + // would otherwise exit itself partway through. + // Only LAN and online games. Skirmish is excluded deliberately: a defeated skirmish player + // restarts rather than sitting out a match, so there is nothing to leave. isInMultiplayerGame() + // is exactly GAME_LAN or GAME_INTERNET, which also keeps campaign and replay out. + m_autoLeaveSeconds = 0; + if (TheGameLogic && TheGameLogic->isInMultiplayerGame() + && !m_isObserver && !(TheRecorder && TheRecorder->isPlaybackMode())) + { + // A duration enforced by the host wins over the local preference. Zero means the host does + // not enforce one, in which case the player's own setting applies. TheGameInfo is null in + // single player, so it is checked rather than assumed. + const UnsignedInt hostSeconds = TheGameInfo ? TheGameInfo->getAutoLeaveSeconds() : 0; + if (hostSeconds > 0) + { + m_autoLeaveSeconds = hostSeconds; + } + else + { + OptionPreferences optionPref; + m_autoLeaveSeconds = optionPref.getAutoLeaveOnDefeatSeconds(); + } + } } //------------------------------------------------------------------------------------------------- From e0b435497ac5777788ad1adad9db3ab27f91dcaf Mon Sep 17 00:00:00 2001 From: JawadYzbk Date: Wed, 16 Sep 2026 15:30:03 +0300 Subject: [PATCH 4/4] feat: Let the online lobby host enforce the auto-leave duration Adds a host enforced auto-leave duration to the online lobby, set with /autoleave in the staging room. The host value wins when non zero. Zero means the host does not enforce one and each player's own preference applies, which is the default for a new lobby. The value travels in the backend lobby record alongside the other host set rules, not in GameInfoToAsciiString, so there is no wire format change. This matters: ParseAsciiStringToGameInfo rejects the whole options string on an unrecognised key, so adding a tag there would make a patched host's lobby unjoinable and invisible to unpatched clients, and would change the replay header. None of that applies here. Client plumbing mirrors MaximumCameraHeight, which is the closest existing precedent for a host set numeric lobby rule: - ELobbyUpdateField::LOBBY_AUTO_LEAVE = 20 - NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_AutoLeave() - LobbyEntry::auto_leave_seconds, read at both lobby parse sites - NGMPGame applies it to the GameInfo the gameplay code reads - GameInfo carries it, deliberately outside the ascii options string and the slot list xfer Note that this client's copy of ELobbyUpdateField had drifted behind the service, stopping at JOINABILITY = 18 while the service also defines HOST_ACTION_BULK_SLOT_UPDATE = 19. Claiming 19 here would have collided with that handler, so auto-leave takes 20. The comment at the enum records this. The lobby parse sites use value("AutoLeaveSeconds", 0) rather than get_to(), which throws on a missing key. This is deliberate and load bearing: it lets the client ship before the service does, and a client talking to a service that does not yet return the field simply sees zero and leaves the feature off. Do not replace it with get_to() for consistency with the neighbouring lines. Input is a chat command rather than a staging room control because the online options row has roughly 50px of free width against the 200px a labelled combo box needs, so a control would require shrinking the chat box. The fork already uses this pattern for host settings, see /maxcameraheight and /friendsonly. This is inert until the Generals Online service stores and echoes the field. The contract is in the pull request description. Note that auto-leave is advisory rather than enforceable, since the behaviour is client side. A modified client can ignore it. --- .../GameEngine/Include/GameNetwork/GameInfo.h | 12 ++++ .../Source/GameNetwork/GameInfo.cpp | 2 + .../OnlineServices_LobbyInterface.h | 5 ++ .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 71 +++++++++++++++++++ .../GameNetwork/GeneralsOnline/NGMPGame.cpp | 3 + .../OnlineServices_LobbyInterface.cpp | 39 +++++++++- 6 files changed, 131 insertions(+), 1 deletion(-) diff --git a/Core/GameEngine/Include/GameNetwork/GameInfo.h b/Core/GameEngine/Include/GameNetwork/GameInfo.h index d65087b95ae..9604692ec9d 100644 --- a/Core/GameEngine/Include/GameNetwork/GameInfo.h +++ b/Core/GameEngine/Include/GameNetwork/GameInfo.h @@ -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; @@ -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; @@ -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 ); diff --git a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp index 731953f831e..f217eb8e1ba 100644 --- a/Core/GameEngine/Source/GameNetwork/GameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameInfo.cpp @@ -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; diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h index a98243c4fa6..63d7e20b449 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h @@ -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; @@ -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(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index d821e1070d3..c597177881b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -3514,6 +3514,8 @@ Bool handleGameSetupSlashCommands(UnicodeString uText) GadgetListBoxAddEntryText(listboxGameSetupChat, UnicodeString(L"/setpassword - 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 - 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 - 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); @@ -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(); + + 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 diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp index 388e95997c3..b0c0dffd14e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp @@ -104,6 +104,9 @@ void NGMPGame::SyncWithLobby(LobbyEntry& lobby) // superweapon setSuperweaponRestriction(lobby.limit_superweapons); + // TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. + setAutoLeaveSeconds(lobby.auto_leave_seconds); + // vanilla teams setOldFactionsOnly(lobby.vanilla_teams); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index a3593c06918..404ca7b91a0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -74,7 +74,12 @@ enum class ELobbyUpdateField AI_TEAM = 15, AI_START_POS = 16, MAX_CAMERA_HEIGHT = 17, - JOINABILITY = 18 + JOINABILITY = 18, + // TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. + // NOTE: these values are a wire contract with the lobby service and must match it. The service + // also defines HOST_ACTION_BULK_SLOT_UPDATE = 19, which this client does not send, so the next + // free value is 20. + LOBBY_AUTO_LEAVE = 20 }; void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_Map(AsciiString strMap, AsciiString strMapPath, bool bIsOfficial, int newMaxPlayers) @@ -162,6 +167,31 @@ void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_StartingCash(Unsigne }); } +// TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. +void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_AutoLeave(UnsignedInt autoLeaveSeconds) +{ + // reset autostart if host changes anything (because ready flag will reset too) +#if !defined(GENERALS_ONLINE_DISABLE_AUTO_ACCEPT) + ClearAutoReadyCountdown(); +#endif + if (TheNGMPGame && TheNGMPGame->IsCountdownStarted()) + TheNGMPGame->StopCountdown(); + + std::string strURI = std::format("{}/{}", NGMP_OnlineServicesManager::GetAPIEndpoint("Lobby"), m_CurrentLobby.lobbyID); + std::map mapHeaders; + + nlohmann::json j; + j["field"] = ELobbyUpdateField::LOBBY_AUTO_LEAVE; + j["auto_leave_seconds"] = autoLeaveSeconds; + std::string strPostData = j.dump(); + + // convert + NGMP_OnlineServicesManager::GetInstance()->GetHTTPManager()->SendPOSTRequest(strURI.c_str(), EIPProtocolVersion::DONT_CARE, mapHeaders, strPostData.c_str(), [=](bool bSuccess, int statusCode, std::string strBody, HTTPRequest* pReq) + { + + }); +} + void NGMP_OnlineServices_LobbyInterface::UpdateCurrentLobby_HasMap() { // do we have the map? @@ -588,6 +618,8 @@ void NGMP_OnlineServices_LobbyInterface::SearchForLobbies(std::function lobbyEntryIter["IsPassworded"].get_to(lobbyEntry.passworded); lobbyEntryIter["AllowObservers"].get_to(lobbyEntry.allow_observers); lobbyEntryIter["MaximumCameraHeight"].get_to(lobbyEntry.max_cam_height); + // TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. + lobbyEntry.auto_leave_seconds = lobbyEntryIter.value("AutoLeaveSeconds", 0); lobbyEntryIter["ExeCRC"].get_to(lobbyEntry.exe_crc); lobbyEntryIter["IniCRC"].get_to(lobbyEntry.ini_crc); lobbyEntryIter["MatchID"].get_to(lobbyEntry.match_id); @@ -866,6 +898,8 @@ void NGMP_OnlineServices_LobbyInterface::UpdateRoomDataCache(std::functionm_exeCRC; j["ini_crc"] = TheGlobalData->m_iniCRC; j["max_cam_height"] = NGMP_OnlineServicesManager::Settings.Camera_GetMaxHeight_WhenLobbyHost(); + // TheSuperHackers @feature JawadYzbk 16/09/2026 Add optional auto-leave on defeat countdown. + // New lobbies never enforce auto-leave; the host opts in with /autoleave. + j["auto_leave_seconds"] = 0; j["anticheat_id"] = AnticheatPlugInterface::GetAnticheatIdentifier(); std::string strPostData = j.dump();