From 38b1a258792121b713e5ebf35562f2e4aedf89e4 Mon Sep 17 00:00:00 2001 From: Ibra Date: Sat, 30 May 2026 17:35:42 +0300 Subject: [PATCH] bugfix: Fix obs overlay font adjustment after merge, normalize notification content, fix spectre multi notifs, add per type toggles and observer notification keybinds --- .../Include/Common/OptionPreferences.h | 3 + .../Source/Common/OptionPreferences.cpp | 64 ++++- .../GameEngine/Include/Common/GlobalData.h | 3 + .../GameEngine/Include/Common/MessageStream.h | 2 + .../GameEngine/Include/GameClient/InGameUI.h | 9 +- .../GameEngine/Source/Common/GlobalData.cpp | 6 + .../Source/Common/MessageStream.cpp | 2 + .../GUI/GUICallbacks/Menus/OptionsMenu.cpp | 11 + .../GameEngine/Source/GameClient/InGameUI.cpp | 267 ++++++++---------- .../GameClient/MessageStream/CommandXlat.cpp | 124 ++++++++ .../GameClient/MessageStream/MetaEvent.cpp | 23 ++ .../SpecialPower/SpecialPowerModule.cpp | 3 +- 12 files changed, 356 insertions(+), 161 deletions(-) diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index d7c0359ca5a..e2ccb2bcc82 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -131,4 +131,7 @@ class OptionPreferences : public UserPreferences Int getObserverStatsFontSize(void); Int getObserverNotificationFontSize(void); + Bool getObserverNotificationSpecialPowerUsage(void); + Bool getObserverNotificationSpecialPowerPurchase(void); + Bool getObserverNotificationMilestone(void); }; diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index 5731b79d39d..b526963de58 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -116,14 +116,57 @@ Int OptionPreferences::getCampaignDifficulty() return factor; } -Int OptionPreferences::getObserverNotificationFontSize(void) { - OptionPreferences::const_iterator it = find("ObserverNotificationFontSize"); - if (it == end()) - return 10; - Int fontSize = atoi(it->second.str()); - if (fontSize < 0) - fontSize = 0; - return fontSize; +Int OptionPreferences::getObserverNotificationFontSize(void) +{ + OptionPreferences::const_iterator it = find("ObserverNotificationFontSize"); + if (it == end()) + return 10; + Int fontSize = atoi(it->second.str()); + if (fontSize < 0) + { + fontSize = 0; + } + if (fontSize > 15) + { + fontSize = 15; + } + return fontSize; +} + +Bool OptionPreferences::getObserverNotificationSpecialPowerUsage(void) +{ + OptionPreferences::const_iterator it = find("ObserverNotificationSpecialPowerUsage"); + if (it == end()) + return TheGlobalData->m_observerNotificationSpecialPowerUsage; + if (stricmp(it->second.str(), "yes") == 0) + { + return TRUE; + } + return FALSE; +} + +Bool OptionPreferences::getObserverNotificationSpecialPowerPurchase(void) +{ + OptionPreferences::const_iterator it = find("ObserverNotificationSpecialPowerPurchase"); + if (it == end()) + return TheGlobalData->m_observerNotificationSpecialPowerPurchase; + if (stricmp(it->second.str(), "yes") == 0) + { + return TRUE; + } + return FALSE; +} + +Bool OptionPreferences::getObserverNotificationMilestone(void) +{ + OptionPreferences::const_iterator it = find("ObserverNotificationMilestone"); + if (it == end()) + return TheGlobalData->m_observerNotificationMilestone; + if (stricmp(it->second.str(), "yes") == 0) + { + return TRUE; + } + return FALSE; } Int OptionPreferences::getObserverStatsFontSize(void) @@ -131,12 +174,15 @@ Int OptionPreferences::getObserverStatsFontSize(void) OptionPreferences::const_iterator it = find("ObserverStatsFontSize"); if (it == end()) return 7; - Int fontSize = atoi(it->second.str()); if (fontSize < 0) { fontSize = 0; } + if (fontSize > 15) + { + fontSize = 15; + } return fontSize; } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index eea9af3317b..6e3aace7aa4 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -437,6 +437,9 @@ class GlobalData : public SubsystemInterface // Generals Online @feature 16/1/2025 allow the observer notification font size to be set, a size of zero disables it Int m_observerNotificationFontSize; + Bool m_observerNotificationSpecialPowerUsage; + Bool m_observerNotificationSpecialPowerPurchase; + Bool m_observerNotificationMilestone; Real m_shakeSubtleIntensity; ///< Intensity for shaking a camera with SHAKE_SUBTLE Real m_shakeNormalIntensity; ///< Intensity for shaking a camera with SHAKE_NORMAL diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index 9cf278e44a7..0e552c0e364 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -248,6 +248,8 @@ class GameMessage : public MemoryPoolObject MSG_META_INCREASE_OBSERVER_STATS_FONT, ///< Generals Online @feature Increase observer overlay size MSG_META_DECREASE_OBSERVER_STATS_FONT, ///< Generals Online @feature Decrease observer overlay size + MSG_META_INCREASE_OBSERVER_NOTIFICATION_FONT, ///< Generals Online @feature Increase observer notification size + MSG_META_DECREASE_OBSERVER_NOTIFICATION_FONT, ///< Generals Online @feature Decrease observer notification size MSG_META_BEGIN_PATH_BUILD, ///< enter path-building mode MSG_META_END_PATH_BUILD, ///< exit path-building mode diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h index 10946c011d3..7b7dbe2c310 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h @@ -622,7 +622,6 @@ class InGameUI : public SubsystemInterface, public Snapshot void triggerDoubleClickAttackMoveGuardHint(); void drawObserverNotifications(Int& x, Int& y); - void updateObserverNotifications(UnsignedInt currentFrame); void checkObserverMilestones(UnsignedInt currentFrame); void addObserverNotification(const UnicodeString& playerName, const wchar_t* message, Color playerColor); void addObserverNotificationRaw(const UnicodeString& message, Color color); @@ -685,10 +684,8 @@ class InGameUI : public SubsystemInterface, public Snapshot Bool reachedLevel3; Bool reachedLevel5; Bool reached10kCPM; - Bool reached20kCPM; - Bool reached50kCPM; - Bool reached100kCPM; - Bool warnedFloating100k; + Bool stolenPower; + Bool gotHunted; }; struct PlayerData { @@ -1051,7 +1048,7 @@ class InGameUI : public SubsystemInterface, public Snapshot // Obs overlay static const Int numCols = 8; const wchar_t* headers[numCols] = { - L"(T) Name", L"Army", L"Cash", L"Cash/m", L"(R) XP", L"SP", L"K/D", L"Power" + L"(T) Player", L"Army", L"Cash", L"Cash/m", L"(R) XP", L"SP", L"K/D", L"Power" }; struct ObsOverlayPlayerData diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index ab1ecb6603b..9fbfec97060 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -963,6 +963,9 @@ GlobalData::GlobalData() m_observerStatsFontSize = 7; m_observerNotificationFontSize = 10; + m_observerNotificationSpecialPowerUsage = TRUE; + m_observerNotificationSpecialPowerPurchase = TRUE; + m_observerNotificationMilestone = TRUE; m_showMoneyPerMinute = FALSE; m_allowMoneyPerMinuteForPlayer = FALSE; @@ -1258,6 +1261,9 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_showMoneyPerMinute = optionPref.getShowMoneyPerMinute(); TheWritableGlobalData->m_observerStatsFontSize = optionPref.getObserverStatsFontSize(); TheWritableGlobalData->m_observerNotificationFontSize = optionPref.getObserverNotificationFontSize(); + TheWritableGlobalData->m_observerNotificationSpecialPowerUsage = optionPref.getObserverNotificationSpecialPowerUsage(); + TheWritableGlobalData->m_observerNotificationSpecialPowerPurchase = optionPref.getObserverNotificationSpecialPowerPurchase(); + TheWritableGlobalData->m_observerNotificationMilestone = optionPref.getObserverNotificationMilestone(); TheWritableGlobalData->m_antiAliasLevel = optionPref.getAntiAliasing(); TheWritableGlobalData->m_textureFilteringMode = optionPref.getTextureFilterMode(); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp index c2104da912a..6fc81157eff 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp @@ -348,6 +348,8 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_INCREASE_OBSERVER_STATS_FONT) CASE_LABEL(MSG_META_DECREASE_OBSERVER_STATS_FONT) + CASE_LABEL(MSG_META_INCREASE_OBSERVER_NOTIFICATION_FONT) + CASE_LABEL(MSG_META_DECREASE_OBSERVER_NOTIFICATION_FONT) CASE_LABEL(MSG_META_INCREASE_MAX_RENDER_FPS) CASE_LABEL(MSG_META_DECREASE_MAX_RENDER_FPS) CASE_LABEL(MSG_META_INCREASE_LOGIC_TIME_SCALE) 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 f20df4aae07..d220e909850 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/OptionsMenu.cpp @@ -837,6 +837,17 @@ static void saveOptions() TheWritableGlobalData->m_showMoneyPerMinute = show; } + //------------------------------------------------------------------------------------------------- + // Set Observer Stats Font Size + val = pref->getObserverStatsFontSize(); + if (val >= 0) + { + AsciiString prefString; + prefString.format("%d", val); + (*pref)["ObserverStatsFontSize"] = prefString; + TheInGameUI->initObserverOverlay(); + } + //------------------------------------------------------------------------------------------------- // Resolution // diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index ca65821d6e7..948756c41e6 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -1219,7 +1219,7 @@ InGameUI::InGameUI() m_playerInfoListBackgroundAlpha = 170; // Observer Stats Overlay - m_observerStatsString = NULL; + m_observerStatsString = nullptr; m_observerStatsFont = "Tahoma"; m_observerStatsPointSize = 10; m_observerStatsBold = TRUE; @@ -1227,8 +1227,9 @@ InGameUI::InGameUI() m_observerStatsPosition.y = kHudAnchorY; // Observer notification overlay - m_observerNotificationString = NULL; + m_observerNotificationString = nullptr; m_observerNotificationPointSize = TheGlobalData->m_observerNotificationFontSize; + m_observerNotificationsHidden = false; #if defined(GENERALS_ONLINE) m_colorGood = GameMakeColor(0, 255, 0, 150); @@ -2339,7 +2340,7 @@ void InGameUI::freeCustomUiResources() m_playerInfoList.clear(); TheDisplayStringManager->freeDisplayString(m_observerStatsString); - m_observerStatsString = NULL; + m_observerStatsString = nullptr; } //------------------------------------------------------------------------------------------------- @@ -6189,21 +6190,19 @@ void InGameUI::recreateControlBar() // ====================================================================================== // Observer Notification // ====================================================================================== -namespace { - const Int MAX_NOTIFICATIONS = 8; - const UnsignedInt SLIDE_IN_MS = 300; - const UnsignedInt VISIBLE_MS = 3000; - const UnsignedInt SLIDE_OUT_MS = 300; - const UnsignedInt TOTAL_LIFETIME_MS = SLIDE_IN_MS + VISIBLE_MS + SLIDE_OUT_MS; - const Real BRIGHTNESS_BOOST = 0.3f; // Apply a slight brightness to make darker colors more visible - - // Layout for notifications - const Int NOTIF_LEFT_MARGIN = 20; - const Int NOTIF_VERTICAL_OFFSET = 300; // Offset from center of screen - const Int NOTIF_PADDING_X = 12; - const Int NOTIF_PADDING_Y = 10; - const Int NOTIF_BOX_SPACING = 8; -} +const Int MAX_NOTIFICATIONS = 8; // Maximum number of notifs to show at once on screen +const UnsignedInt SLIDE_IN_MS = 300; +const UnsignedInt VISIBLE_MS = 3000; +const UnsignedInt SLIDE_OUT_MS = 300; +const UnsignedInt TOTAL_LIFETIME_MS = SLIDE_IN_MS + VISIBLE_MS + SLIDE_OUT_MS; +const Real BRIGHTNESS_BOOST = 0.4f; // Apply a slight brightness to make darker colors more visible + +// Layout for notifications +const Int NOTIF_LEFT_MARGIN = 7; +const Int NOTIF_VERTICAL_OFFSET = 300; // Offset from center of screen +const Int NOTIF_PADDING_X = 15; +const Int NOTIF_PADDING_Y = 10; +const Int NOTIF_BOX_SPACING = 5; // Vertical spacing between notification boxes // Compute animation progress from elapsed render time (0 = sliding in, 1 = visible, 2 = expired) static Real computeSlideProgress(UnsignedInt ageMs) @@ -6230,70 +6229,55 @@ static UnicodeString formatPowerAction(const AsciiString& powerNameAscii) }; static const Entry table[] = { - {"SuperweaponScudStorm", L"LAUNCHED A SCUD STORM!!!"}, - {"SuperweaponNeutronMissile", L"LAUNCHED A NUKE MISSILE!!!"}, - {"SuperweaponParticleUplinkCannon", L"FIRED A PARTICLE CANNON!!!"}, - {"SuperweaponAnthraxBomb", L"DROPPED AN ANTHRAX BOMB!!!"}, - {"SuperweaponRebelAmbush", L"CALLED IN THE REBEL AMBUSH!!"}, - {"SuperweaponArtilleryBarrage", L"CALLED IN THE ARTILLERY BARRAGE!!"}, - {"SuperweaponEMPPulse", L"CALLED IN AN EMP PULSE!!!"}, - {"SuperweaponCIAIntelligence", L"JUST ACTIVATED THE CIA INTELLIGENCE!"}, - {"SuperweaponSneakAttack", L"OPENED A SNEAK ATTACK!!!"}, - - {"SuperweaponDaisyCutter", L"CALLED IN THE MOAB!!!"}, - {"AirF_SuperweaponDaisyCutter", L"CALLED IN THE MOAB!!!"}, - - {"SuperweaponClusterMines", L"CALLED IN A MINE DROP!!"}, - {"Nuke_SuperweaponClusterMines", L"CALLED IN A MINE DROP!!"}, - - {"AirF_SuperweaponA10ThunderboltMissileStrike", L"CALLED IN AN A10 STRIKE!!"}, - {"SuperweaponA10ThunderboltMissileStrike", L"CALLED IN AN A10 STRIKE!!"}, - - {"AirF_SuperweaponSpectreGunship", L"CALLED IN A SPECTRE GUNSHIP!!"}, - {"SuperweaponSpectreGunship", L"CALLED IN A SPECTRE GUNSHIP!!"}, - - {"AirF_SuperweaponCarpetBomb", L"CALLED IN A CARPET BOMB!!"}, - {"Nuke_SuperweaponChinaCarpetBomb", L"CALLED IN A CARPET BOMB!!"}, - {"Early_SuperweaponChinaCarpetBomb", L"CALLED IN A CARPET BOMB!!"}, - {"SuperweaponChinaCarpetBomb", L"CALLED IN A CARPET BOMB!!"}, - - {"SuperweaponFrenzy", L"ACTIVATED THE FRENZY!"}, - {"Early_SuperweaponFrenzy", L"ACTIVATED THE FRENZY!"}, - - {"Slth_SuperweaponGPSScrambler", L"ACTIVATED A GPS SCRAMBLER!"}, - {"SuperweaponGPSScrambler", L"ACTIVATED A GPS SCRAMBLER!"}, - - {"Infa_SuperweaponInfantryParadrop", L"DEPLOYED A CHINA INFANTRY PARADROP!"}, - {"Tank_SuperweaponTankParadrop", L"DEPLOYED A TANK PARADROP!"}, - {"SuperweaponParadropAmerica", L"DEPLOYED A USA INFANTRY PARADROP!"}, - - {"SuperweaponLeafletDrop", L"CALLED IN A LEAFLET DROP!!"}, - {"Early_SuperweaponLeafletDrop", L"CALLED IN A LEAFLET DROP!!"}, + {"SuperweaponScudStorm", L"launched a Scud Storm"}, + {"SuperweaponNeutronMissile", L"launched a Nuke Missile"}, + {"SuperweaponParticleUplinkCannon", L"fired a Particle Cannon"}, + {"SuperweaponAnthraxBomb", L"dropped an Anthrax Bomb"}, + {"SuperweaponRebelAmbush", L"called in a Rebel Ambush"}, + {"SuperweaponArtilleryBarrage", L"called in an Artillery Barrage"}, + {"SuperweaponEMPPulse", L"called in an EMP Bomb"}, + {"SuperweaponCIAIntelligence", L"activated the Intelligence"}, + {"SuperweaponSneakAttack", L"opened a Sneak Attack"}, + + {"SuperweaponDaisyCutter", L"called in a MOAB"}, + {"AirF_SuperweaponDaisyCutter", L"called in a MOAB"}, + + {"SuperweaponClusterMines", L"called in a Mine Drop"}, + {"Nuke_SuperweaponClusterMines", L"called in a Mine Drop"}, + + {"AirF_SuperweaponA10ThunderboltMissileStrike", L"called in an A10 Strike"}, + {"SuperweaponA10ThunderboltMissileStrike", L"called in an A10 Strike"}, + + {"AirF_SuperweaponSpectreGunship", L"called in a Spectre Gunship"}, + {"SuperweaponSpectreGunship", L"called in a Spectre Gunship"}, + + {"AirF_SuperweaponCarpetBomb", L"called in a Carpet Bomb"}, + {"Nuke_SuperweaponChinaCarpetBomb", L"called in a Carpet Bomb"}, + {"Early_SuperweaponChinaCarpetBomb", L"called in a Carpet Bomb"}, + {"SuperweaponChinaCarpetBomb", L"called in a Carpet Bomb"}, + + {"SuperweaponLeafletDrop", L"called in a Leaflet Drop"}, + {"Early_SuperweaponLeafletDrop", L"called in a Leaflet Drop"}, }; for (const Entry& entry : table) if (powerNameAscii == entry.key) - return entry.value; - - UnicodeString result = L"USED "; // Fallback for unmapped support powers - UnicodeString temp; - temp.translate(powerNameAscii); - result.concat(temp); - return result; + return UnicodeString(entry.value); + return UnicodeString(); // not in table, suppress notification } void InGameUI::drawObserverNotifications(Int& x, Int& y) { - if (!TheInGameUI->getInputEnabled() || TheGameLogic->isIntroMoviePlaying() || - TheGameLogic->isLoadingMap() || TheInGameUI->isQuitMenuVisible() || - !TheGameLogic || TheGameLogic->getFrame() <= 1 || m_observerNotificationsHidden) + if (!TheGameLogic || !TheInGameUI->getInputEnabled() || TheGameLogic->isIntroMoviePlaying() || TheGameLogic->isLoadingMap() || + TheInGameUI->isQuitMenuVisible() || TheGameLogic->getFrame() <= 1 || m_observerNotificationsHidden) return; Player* localPlayer = ThePlayerList->getLocalPlayer(); if (!localPlayer || !localPlayer->isPlayerObserver()) return; - updateObserverNotifications(TheGameLogic->getFrame()); + if ((TheGameLogic->getFrame() % LOGICFRAMES_PER_SECOND) == 0) + checkObserverMilestones(TheGameLogic->getFrame()); if (m_observerNotifications.empty()) return; @@ -6320,8 +6304,8 @@ void InGameUI::drawObserverNotifications(Int& x, Int& y) Int padY = Int(NOTIF_PADDING_Y * scale); Int boxSpacing = Int(NOTIF_BOX_SPACING * scale); - Color bgColor = TheWindowManager->winMakeColor(0, 0, 0, 180); - Color borderColor = TheWindowManager->winMakeColor(255, 255, 255, 255); + Color bgColor = TheWindowManager->winMakeColor(0, 0, 0, 90); + Color borderColor = TheWindowManager->winMakeColor(255, 255, 255, 90); UnsignedInt nowMs = timeGetTime(); @@ -6370,22 +6354,17 @@ void InGameUI::drawObserverNotifications(Int& x, Int& y) } } -// Handle milestone initialization and triggers milestone checks once per second. -void InGameUI::updateObserverNotifications(UnsignedInt currentFrame) +void InGameUI::checkObserverMilestones(UnsignedInt currentFrame) { - if (m_observerMilestones.empty()) { + if (!TheGlobalData->m_observerNotificationMilestone) + return; + + if (m_observerMilestones.size() < (size_t)MAX_SLOTS) m_observerMilestones.resize(MAX_SLOTS); - } - static UnsignedInt lastCheckFrame = 0; - if (currentFrame - lastCheckFrame >= LOGICFRAMES_PER_SECOND) { - lastCheckFrame = currentFrame; - checkObserverMilestones(currentFrame); - } -} + if (!ThePlayerList || !TheNameKeyGenerator) + return; -void InGameUI::checkObserverMilestones(UnsignedInt currentFrame) -{ for (Int slotIndex = 0; slotIndex < MAX_SLOTS; ++slotIndex) { const GameSlot* slot = TheGameInfo ? TheGameInfo->getConstSlot(slotIndex) : nullptr; if (!slot || !slot->isOccupied()) @@ -6394,9 +6373,6 @@ void InGameUI::checkObserverMilestones(UnsignedInt currentFrame) AsciiString nameKeyStr; nameKeyStr.format("player%d", slotIndex); - if (!ThePlayerList || !TheNameKeyGenerator) - continue; - Player* p = ThePlayerList->findPlayerWithNameKey(TheNameKeyGenerator->nameToKey(nameKeyStr)); if (!p || !p->isPlayerActive() || p->isPlayerObserver()) @@ -6410,43 +6386,53 @@ void InGameUI::checkObserverMilestones(UnsignedInt currentFrame) Color playerColor = p->getPlayerColor(); // Check rank milestones + Bool earlyGame = currentFrame < LOGICFRAMES_PER_SECOND * 10; // skip milestone notifications early game for modded maps that start with high rank or income Int rank = p->getRankLevel(); if (rank >= 3 && !milestone.reachedLevel3) { milestone.reachedLevel3 = true; - addObserverNotification(name, L" reached Rank 3!", playerColor); + if (!earlyGame) addObserverNotification(name, L" reached Rank 3", playerColor); } if (rank >= 5 && !milestone.reachedLevel5) { milestone.reachedLevel5 = true; - addObserverNotification(name, L" reached Rank 5!", playerColor); + if (!earlyGame) addObserverNotification(name, L" reached Rank 5", playerColor); } - // Check economy milestones Money* money = p->getMoney(); if (!money) continue; - UnsignedInt cash = money->countMoney(); UnsignedInt cpm = money->getCashPerMinute(); + if (cpm >= 10000 && !milestone.reached10kCPM) { + milestone.reached10kCPM = true; + if (!earlyGame) addObserverNotification(name, L" reached 10k/min income", playerColor); + } + + const Energy* energy = p->getEnergy(); + AsciiString side = p->getSide(); + Bool isGLA = side == "GLA" || side == "GLAStealthGeneral" || side == "GLAToxinGeneral" || side == "GLADemolitionGeneral"; + Bool hasPower = energy && energy->getProduction() > 0; + if (isGLA && hasPower && !milestone.stolenPower) { + milestone.stolenPower = true; + if (!earlyGame) addObserverNotification(name, L" now has power", playerColor); + } - if (cash >= 100000 && !milestone.warnedFloating100k) { - milestone.warnedFloating100k = true; - addObserverNotification(name, L" is floating $100k!", playerColor); - } - - // Check income milestones in ascending order - struct IncomeThreshold { UnsignedInt amount; Bool& reached; const wchar_t* msg; }; - IncomeThreshold thresholds[] = { - { 10000, milestone.reached10kCPM, L" reached 10k/min income!" }, - { 20000, milestone.reached20kCPM, L" reached 20k/min income!!" }, - { 50000, milestone.reached50kCPM, L" reached 50k/min income!!!" }, - { 100000, milestone.reached100kCPM, L" reached 100k/min income!!!!" } - }; - - for (auto& threshold : thresholds) { - if (cpm >= threshold.amount && !threshold.reached) { - threshold.reached = true; - addObserverNotification(name, threshold.msg, playerColor); - break; // Only trigger one income milestone per check + if (!milestone.gotHunted) { + Bool hasBuilder = false; + Bool hasBuilderSource = false; + for (Object* obj = TheGameLogic->getFirstObject(); obj && !(hasBuilder && hasBuilderSource); + obj = obj->getNextObject()) { + + if (obj->getControllingPlayer() != p || obj->isEffectivelyDead()) + continue; + + if (obj->isKindOf(KINDOF_DOZER)) + hasBuilder = true; + if (isGLA ? obj->isKindOf(KINDOF_FS_SUPPLY_CENTER) || obj->isKindOf(KINDOF_COMMANDCENTER) : obj->isKindOf(KINDOF_COMMANDCENTER)) + hasBuilderSource = true; + } + if (!hasBuilder && !hasBuilderSource) { + milestone.gotHunted = true; + if (!earlyGame) addObserverNotification(name, isGLA ? L" got worker hunted" : L" got dozer hunted", playerColor); } } } @@ -6464,26 +6450,16 @@ void InGameUI::addObserverNotificationRaw(const UnicodeString& message, Color co UnsignedInt nowMs = timeGetTime(); // Reuse first inactive slot - for (auto& n : m_observerNotifications) - if (!n.active) - return n = { message, color, nowMs, true }, void(); + for (ObserverNotification& notif : m_observerNotifications) + if (!notif.active) { + notif = { message, color, nowMs, true }; + return; + } - // Expand if under limit if (m_observerNotifications.size() < MAX_NOTIFICATIONS) { m_observerNotifications.push_back({ message, color, nowMs, true }); return; } - - // Replace oldest active notification - auto* oldest = &m_observerNotifications[0]; - for (auto& n : m_observerNotifications) - if (n.active && n.createdRenderMs < oldest->createdRenderMs) - oldest = &n; - - oldest->message = message; - oldest->color = color; - oldest->createdRenderMs = nowMs; - oldest->active = true; } void InGameUI::notifyGeneralPromotion(Player* player, ScienceType science) @@ -6491,6 +6467,9 @@ void InGameUI::notifyGeneralPromotion(Player* player, ScienceType science) if (!player || !player->isPlayerActive() || player->isPlayerObserver()) return; + if (!TheGlobalData->m_observerNotificationSpecialPowerPurchase) + return; + UnicodeString scienceName, description; if (!TheScienceStore->getNameAndDescription(science, scienceName, description)) return; @@ -6505,25 +6484,15 @@ void InGameUI::notifySpecialPowerUsed(Player* player, const SpecialPowerTemplate if (!player || !player->isPlayerActive() || !powerTemplate || player->isPlayerObserver()) return; - // Only notify for these support powers - switch (powerTemplate->getSpecialPowerType()) { - case SPECIAL_DAISY_CUTTER: case SPECIAL_CARPET_BOMB: case AIRF_SPECIAL_DAISY_CUTTER: - case SPECIAL_PARTICLE_UPLINK_CANNON: case SPECIAL_SCUD_STORM: case SPECIAL_NEUTRON_MISSILE: - case SPECIAL_AMBUSH: case EARLY_SPECIAL_LEAFLET_DROP: case EARLY_SPECIAL_FRENZY: - case SPECIAL_CLUSTER_MINES: case SPECIAL_EMP_PULSE: case SPECIAL_ANTHRAX_BOMB: - case SPECIAL_A10_THUNDERBOLT_STRIKE: case SPECIAL_ARTILLERY_BARRAGE: case SPECIAL_SPECTRE_GUNSHIP: - case SPECIAL_FRENZY: case SPECIAL_SNEAK_ATTACK: case SPECIAL_CHINA_CARPET_BOMB: case SPECIAL_CIA_INTELLIGENCE: - case SPECIAL_LEAFLET_DROP: case SPECIAL_TANK_PARADROP: case SPECIAL_PARADROP_AMERICA: - case NUKE_SPECIAL_CLUSTER_MINES: case AIRF_SPECIAL_A10_THUNDERBOLT_STRIKE: case AIRF_SPECIAL_SPECTRE_GUNSHIP: - case INFA_SPECIAL_PARADROP_AMERICA: case SLTH_SPECIAL_GPS_SCRAMBLER: case AIRF_SPECIAL_CARPET_BOMB: - case SPECIAL_GPS_SCRAMBLER: case EARLY_SPECIAL_CHINA_CARPET_BOMB: - break; - default: + if (!TheGlobalData->m_observerNotificationSpecialPowerUsage) + return; + + UnicodeString action = formatPowerAction(powerTemplate->getName()); + if (action.isEmpty()) return; - } UnicodeString msg; - msg.format(L"%ls %ls", player->getPlayerDisplayName().str(), formatPowerAction(powerTemplate->getName()).str()); + msg.format(L"%ls %ls", player->getPlayerDisplayName().str(), action.str()); addObserverNotificationRaw(msg, player->getPlayerColor()); } @@ -6807,10 +6776,10 @@ void InGameUI::drawObserverStats(Int & x, Int & y) Int contentY = baseY + padY; // Draw background - TheWindowManager->winFillRect(TheWindowManager->winMakeColor(0, 0, 0, 180), 1, baseX, baseY, baseX + bgW, baseY + bgH); + TheWindowManager->winFillRect(TheWindowManager->winMakeColor(0, 0, 0, 150), 1, baseX, baseY, baseX + bgW, baseY + bgH); // Draw border - Color border = TheWindowManager->winMakeColor(255, 255, 255, 225); + Color border = TheWindowManager->winMakeColor(255, 255, 255, 110); TheWindowManager->winFillRect(border, 1, baseX, baseY, baseX + bgW, baseY + 1); TheWindowManager->winFillRect(border, 1, baseX, baseY + bgH - 1, baseX + bgW, baseY + bgH); TheWindowManager->winFillRect(border, 1, baseX, baseY, baseX + 1, baseY + bgH); @@ -6839,16 +6808,24 @@ void InGameUI::drawObserverStats(Int & x, Int & y) } drawY += totalRowHeight; - //for (size_t row = 0; row < actualNumPlayers; ++row) + Real brightnessBoost = 0.3f; for (int i = 0; i < MAX_SLOTS; ++i) { if (m_mapOverlayPlayerData[i].isPresent) { + Color raw = m_mapOverlayPlayerData[i].playerData.color; + UnsignedInt r = (raw >> 16) & 0xFF; + UnsignedInt g = (raw >> 8) & 0xFF; + UnsignedInt b = raw & 0xFF; + UnsignedInt lr = r + UnsignedInt((255 - r) * brightnessBoost); + UnsignedInt lg = g + UnsignedInt((255 - g) * brightnessBoost); + UnsignedInt lb = b + UnsignedInt((255 - b) * brightnessBoost); + + Color boostedColor = TheWindowManager->winMakeColor(lr, lg, lb, 255); drawX = contentX; - for (Int col = 0; col < numCols; ++col) - { - m_mapOverlayPlayerData[i].playerCellStrings[col]->draw(drawX, drawY, m_mapOverlayPlayerData[i].playerData.color, dropShadow); + for (Int col = 0; col < numCols; ++col) { + m_mapOverlayPlayerData[i].playerCellStrings[col]->draw(drawX, drawY, boostedColor, dropShadow); drawX += colWidths[col]; } drawY += totalRowHeight; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 2be7ea57128..951fc115213 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -51,6 +51,7 @@ #include "Common/StatsCollector.h" #include "Common/ThingTemplate.h" #include "Common/GameLOD.h" +#include "Common/OptionPreferences.h" #include "GameClient/InGameUI.h" #include "GameClient/CommandXlat.h" @@ -180,6 +181,89 @@ Bool hasThingsInProduction(PlayerType playerType) #endif // defined(RTS_DEBUG) || defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE) +enum ObserverStatsFontChange +{ + ObserverStatsFontChange_Increase, + ObserverStatsFontChange_Decrease, +}; +enum ObserverNotificationFontChange +{ + ObserverNotificationFontChange_Increase, + ObserverNotificationFontChange_Decrease, +}; + +bool changeObserverNotificationFontSize(ObserverNotificationFontChange change) +{ + Int fontSize = TheWritableGlobalData->m_observerNotificationFontSize; + + const Int minSize = 0; + const Int maxSize = 15; + + switch (change) + { + case ObserverNotificationFontChange_Increase: + if (fontSize < maxSize) ++fontSize; + break; + case ObserverNotificationFontChange_Decrease: + if (fontSize > minSize) --fontSize; + break; + } + + if (fontSize == TheWritableGlobalData->m_observerNotificationFontSize) + return false; + + TheWritableGlobalData->m_observerNotificationFontSize = fontSize; + + if (TheInGameUI) + TheInGameUI->refreshObserverNotificationResources(); + + OptionPreferences optPref; + AsciiString prefString; + prefString.format("%d", fontSize); + optPref["ObserverNotificationFontSize"] = prefString; + optPref.write(); + + return true; +} + +bool changeObserverStatsFontSize(ObserverStatsFontChange change) +{ + Int fontSize = TheWritableGlobalData->m_observerStatsFontSize; + + const Int minSize = 0; + const Int maxSize = 15; + + switch (change) + { + case ObserverStatsFontChange_Increase: + if (fontSize < maxSize) + ++fontSize; + break; + + case ObserverStatsFontChange_Decrease: + if (fontSize > minSize) + --fontSize; + break; + } + + if (fontSize == TheWritableGlobalData->m_observerStatsFontSize) + return false; + + TheWritableGlobalData->m_observerStatsFontSize = fontSize; + + if (TheInGameUI) + { + TheInGameUI->initObserverOverlay(); + } + + OptionPreferences optPref; + AsciiString prefString; + prefString.format("%d", fontSize); + optPref["ObserverStatsFontSize"] = prefString; + optPref.write(); + + return true; +} bool changeMaxRenderFps(FpsValueChange change) { @@ -3285,6 +3369,46 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage break; } + //----------------------------------------------------------------------------------------- + + case GameMessage::MSG_META_INCREASE_OBSERVER_NOTIFICATION_FONT: + { + if (changeObserverNotificationFontSize(ObserverNotificationFontChange_Increase)) + disp = DESTROY_MESSAGE; + break; + } + + //----------------------------------------------------------------------------------------- + + case GameMessage::MSG_META_DECREASE_OBSERVER_NOTIFICATION_FONT: + { + if (changeObserverNotificationFontSize(ObserverNotificationFontChange_Decrease)) + disp = DESTROY_MESSAGE; + break; + } + + //----------------------------------------------------------------------------------------- + + case GameMessage::MSG_META_INCREASE_OBSERVER_STATS_FONT: + { + if (changeObserverStatsFontSize(ObserverStatsFontChange_Increase)) + { + disp = DESTROY_MESSAGE; + } + break; + } + + //----------------------------------------------------------------------------------------- + + case GameMessage::MSG_META_DECREASE_OBSERVER_STATS_FONT: + { + if (changeObserverStatsFontSize(ObserverStatsFontChange_Decrease)) + { + disp = DESTROY_MESSAGE; + } + break; + } + //----------------------------------------------------------------------------------------- case GameMessage::MSG_META_INCREASE_LOGIC_TIME_SCALE: { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index 241c21716dc..9eac277dec8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -161,6 +161,8 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "TOGGLE_PLAYER_OBSERVER", GameMessage::MSG_META_TOGGLE_PLAYER_OBSERVER }, { "INCREASE_OBSERVER_STATS_FONT", GameMessage::MSG_META_INCREASE_OBSERVER_STATS_FONT }, { "DECREASE_OBSERVER_STATS_FONT", GameMessage::MSG_META_DECREASE_OBSERVER_STATS_FONT }, + { "INCREASE_OBSERVER_NOTIFICATION_FONT", GameMessage::MSG_META_INCREASE_OBSERVER_NOTIFICATION_FONT }, + { "DECREASE_OBSERVER_NOTIFICATION_FONT", GameMessage::MSG_META_DECREASE_OBSERVER_NOTIFICATION_FONT }, { "BEGIN_PATH_BUILD", GameMessage::MSG_META_BEGIN_PATH_BUILD }, { "END_PATH_BUILD", GameMessage::MSG_META_END_PATH_BUILD }, { "BEGIN_FORCEATTACK", GameMessage::MSG_META_BEGIN_FORCEATTACK }, @@ -731,6 +733,27 @@ void MetaMap::generateMetaMap() // TheSuperHackers @info A default mapping for MSG_META_SELECT_ALL_AIRCRAFT would be useful for Generals // but is not recommended, because it will cause key mapping conflicts with original game languages. + { + MetaMapRec* map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_INCREASE_OBSERVER_NOTIFICATION_FONT); + if (map->m_key == MK_NONE) + { + map->m_key = MK_RIGHT; + map->m_transition = DOWN; + map->m_modState = SHIFT; + map->m_usableIn = COMMANDUSABLE_GAME; + } + } + { + MetaMapRec* map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_DECREASE_OBSERVER_NOTIFICATION_FONT); + if (map->m_key == MK_NONE) + { + map->m_key = MK_LEFT; + map->m_transition = DOWN; + map->m_modState = SHIFT; + map->m_usableIn = COMMANDUSABLE_GAME; + } + } + { MetaMapRec* map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_INCREASE_OBSERVER_STATS_FONT); if (map->m_key == MK_NONE) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp index 9420ad32a48..34d3ba6a126 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp @@ -648,7 +648,8 @@ void SpecialPowerModule::aboutToDoSpecialPower( const Coord3D *location ) const Object* obj = getObject(); const SpecialPowerTemplate* tpl = getSpecialPowerTemplate(); if (obj && tpl && TheInGameUI) { - if (Player* p = obj->getControllingPlayer()) + Player* p = obj->getControllingPlayer(); + if (p && !obj->isKindOf(KINDOF_AIRCRAFT)) // exclude aircraft as they are spawned by the command center which already fires its notification TheInGameUI->notifySpecialPowerUsed(p, tpl); }