Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Core/GameEngine/Include/Common/UserPreferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ class OptionPreferences : public UserPreferences
Int getObserverStatsFontSize(void);
Int getObserverNotificationFontSize(void);

Bool getObserverNotificationSpecialPowerUsage(void);
Bool getObserverNotificationSpecialPowerPurchase(void);
Bool getObserverNotificationMilestone(void);

Real getResolutionFontAdjustment(void);

Bool getShowMoneyPerMinute(void) const;
Expand Down
3 changes: 3 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,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
Expand Down
7 changes: 7 additions & 0 deletions GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,10 @@ GlobalData::GlobalData()
m_observerStatsFontSize = 7;
m_observerNotificationFontSize = 10;

m_observerNotificationSpecialPowerUsage = TRUE;
m_observerNotificationSpecialPowerPurchase = TRUE;
m_observerNotificationMilestone = TRUE;

m_showMoneyPerMinute = FALSE;
m_allowMoneyPerMinuteForPlayer = FALSE;

Expand Down Expand Up @@ -1238,6 +1242,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();

Int val=optionPref.getGammaValue();
//generate a value between 0.6 and 2.0.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,24 @@ Int OptionPreferences::getObserverNotificationFontSize(void) {
return fontSize;
}

Bool OptionPreferences::getObserverNotificationSpecialPowerUsage(void) {
OptionPreferences::const_iterator it = find("ObserverNotificationSpecialPowerUsage");
if (it == end()) return true;
return AsciiString(it->second.str()) != "no";
}

Bool OptionPreferences::getObserverNotificationSpecialPowerPurchase(void) {
OptionPreferences::const_iterator it = find("ObserverNotificationSpecialPowerPurchase");
if (it == end()) return true;
return AsciiString(it->second.str()) != "no";
}

Bool OptionPreferences::getObserverNotificationMilestone(void) {
OptionPreferences::const_iterator it = find("ObserverNotificationMilestone");
if (it == end()) return true;
return AsciiString(it->second.str()) != "no";
}

Int OptionPreferences::getObserverStatsFontSize(void)
{
OptionPreferences::const_iterator it = find("ObserverStatsFontSize");
Expand Down Expand Up @@ -1555,16 +1573,6 @@ static void saveOptions( void )
TheInGameUI->refreshRenderFpsResources();
}

//-------------------------------------------------------------------------------------------------
// Set Observer notification Font Size
val = pref->getObserverNotificationFontSize();
if (val >= 0) {
AsciiString prefString;
prefString.format("%d", val);
(*pref)["ObserverNotificationFontSize"] = prefString;
TheInGameUI->refreshObserverNotificationResources();
}

//-------------------------------------------------------------------------------------------------
// Set Observer Stats Font Size
val = pref->getObserverStatsFontSize();
Expand Down
157 changes: 67 additions & 90 deletions GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1123,16 +1123,17 @@ InGameUI::InGameUI()
m_gameTimeDropColor = GameMakeColor(0, 0, 0, 255);

// Observer Stats Overlay
m_observerStatsString = NULL;
m_observerStatsString = nullptr;
m_observerStatsFont = "Tahoma";
m_observerStatsPointSize = 10;
m_observerStatsBold = TRUE;
m_observerStatsPosition.x = kHudAnchorX;
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);
Expand Down Expand Up @@ -6094,7 +6095,7 @@ void InGameUI::recreateControlBar(void)
// Observer Notification
// ======================================================================================
namespace {
const Int MAX_NOTIFICATIONS = 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;
Expand Down Expand Up @@ -6134,63 +6135,64 @@ 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!!!"},
{"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 CIA Intelligence"},
{"SuperweaponSneakAttack", L"opened a Sneak Attack"},

{"SuperweaponDaisyCutter", L"CALLED IN THE MOAB!!!"},
{"AirF_SuperweaponDaisyCutter", L"CALLED IN THE MOAB!!!"},
{"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!!"},
{"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_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_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!!"},
{"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!"},
{"SuperweaponCashHack", L"used Cash Hack"},
{"SuperweaponEmergencyRepair", L"used Emergency Repair"},

{"Slth_SuperweaponGPSScrambler", L"ACTIVATED A GPS SCRAMBLER!"},
{"SuperweaponGPSScrambler", L"ACTIVATED A GPS SCRAMBLER!"},
{"SuperweaponFrenzy", L"activated a Frenzy"},
{"Early_SuperweaponFrenzy", L"activated a Frenzy"},

{"Infa_SuperweaponInfantryParadrop", L"DEPLOYED A CHINA INFANTRY PARADROP!"},
{"Tank_SuperweaponTankParadrop", L"DEPLOYED A TANK PARADROP!"},
{"SuperweaponParadropAmerica", L"DEPLOYED A USA INFANTRY PARADROP!"},
{"Slth_SuperweaponGPSScrambler", L"activated a GPS Scrambler"},
{"SuperweaponGPSScrambler", L"activated a GPS Scrambler"},

{"SuperweaponLeafletDrop", L"CALLED IN A LEAFLET DROP!!"},
{"Early_SuperweaponLeafletDrop", L"CALLED IN A LEAFLET DROP!!"},
{"Infa_SuperweaponInfantryParadrop", L"deployed a China Infantry Paradrop"},
{"Tank_SuperweaponTankParadrop", L"deployed a China Tank Paradrop"},
{"SuperweaponParadropAmerica", L"deployed a USA Infantry Paradrop"},

{"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);
UnicodeString result;
result.format(L"used %hs", powerNameAscii.str()); // Fallback for unmapped support powers
return result;
}

void InGameUI::drawObserverNotifications(Int& x, Int& y)
{
if (!TheInGameUI->getInputEnabled() || TheGameLogic->isIntroMoviePlaying() ||
if (!TheGameLogic || !TheInGameUI->getInputEnabled() || TheGameLogic->isIntroMoviePlaying() ||
TheGameLogic->isLoadingMap() || TheInGameUI->isQuitMenuVisible() ||
!TheGameLogic || TheGameLogic->getFrame() <= 1 || m_observerNotificationsHidden)
TheGameLogic->getFrame() <= 1 || m_observerNotificationsHidden)
return;

Player* localPlayer = ThePlayerList->getLocalPlayer();
Expand Down Expand Up @@ -6277,51 +6279,41 @@ void InGameUI::drawObserverNotifications(Int& x, Int& y)
// Handle milestone initialization and triggers milestone checks once per second.
void InGameUI::updateObserverNotifications(UnsignedInt currentFrame)
{
if (m_observerMilestones.empty()) {
m_observerMilestones.resize(MAX_SLOTS);
}

static UnsignedInt lastCheckFrame = 0;
if (currentFrame - lastCheckFrame >= LOGICFRAMES_PER_SECOND) {
lastCheckFrame = currentFrame;
if ((currentFrame % LOGICFRAMES_PER_SECOND) == 0) {
checkObserverMilestones(currentFrame);
}
}

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())
continue;

AsciiString nameKeyStr;
nameKeyStr.format("player%d", slotIndex);

if (!ThePlayerList || !TheNameKeyGenerator)
continue;
if (!TheGlobalData->m_observerNotificationMilestone)
return;

Player* p = ThePlayerList->findPlayerWithNameKey(TheNameKeyGenerator->nameToKey(nameKeyStr));
Int playerCount = ThePlayerList ? ThePlayerList->getPlayerCount() : 0;
if (m_observerMilestones.size() < (size_t)playerCount)
m_observerMilestones.resize(playerCount);

if (!p || !p->isPlayerActive() || p->isPlayerObserver())
continue;
for (Int i = 0; i < playerCount; ++i) {
Player* p = ThePlayerList->getNthPlayer(i);
if (!p || !p->isPlayerActive() || p->isPlayerObserver())
continue;

UnicodeString name = p->getPlayerDisplayName();
if (name.isEmpty())
continue;
UnicodeString name = p->getPlayerDisplayName();
if (name.isEmpty())
continue;

ObserverMilestone& milestone = m_observerMilestones[slotIndex];
Color playerColor = p->getPlayerColor();
ObserverMilestone& milestone = m_observerMilestones[i];
Color playerColor = p->getPlayerColor();

// Check rank milestones
Int rank = p->getRankLevel();
if (rank >= 3 && !milestone.reachedLevel3) {
milestone.reachedLevel3 = true;
addObserverNotification(name, L" reached Rank 3!", playerColor);
addObserverNotification(name, L" reached Rank 3", playerColor);
}
if (rank >= 5 && !milestone.reachedLevel5) {
milestone.reachedLevel5 = true;
addObserverNotification(name, L" reached Rank 5!", playerColor);
addObserverNotification(name, L" reached Rank 5", playerColor);
}

// Check economy milestones
Expand All @@ -6334,16 +6326,16 @@ void InGameUI::checkObserverMilestones(UnsignedInt currentFrame)

if (cash >= 100000 && !milestone.warnedFloating100k) {
milestone.warnedFloating100k = true;
addObserverNotification(name, L" is floating $100k!", playerColor);
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!!!!" }
{ 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) {
Expand Down Expand Up @@ -6395,6 +6387,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;
Expand All @@ -6409,22 +6404,8 @@ 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 msg;
msg.format(L"%ls %ls", player->getPlayerDisplayName().str(), formatPowerAction(powerTemplate->getName()).str());
Expand Down Expand Up @@ -7160,7 +7141,3 @@ void InGameUI::drawGameTime()
m_gameTimeString->draw(horizontalTimerOffset, m_gameTimePosition.y, m_gameTimeColor, m_gameTimeDropColor);
m_gameTimeFrameString->draw(horizontalFrameOffset, m_gameTimePosition.y, GameMakeColor(180, 180, 180, 255), m_gameTimeDropColor);
}




Loading