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/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/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/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/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 ); 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/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" 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/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/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(); + } + } } //------------------------------------------------------------------------------------------------- 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();