Skip to content

Fix #2452: Per-object cooldown for under-attack radar alarm#2845

Open
secprentice wants to merge 1 commit into
TheSuperHackers:mainfrom
secprentice:fix/2452-under-attack-alarm-spam
Open

Fix #2452: Per-object cooldown for under-attack radar alarm#2845
secprentice wants to merge 1 commit into
TheSuperHackers:mainfrom
secprentice:fix/2452-under-attack-alarm-spam

Conversation

@secprentice

@secprentice secprentice commented Jun 29, 2026

Copy link
Copy Markdown

Summary

Fixes #2452.

PR #2368 correctly fixed the broken 2-D squared-distance formula in Radar::tryEvent. A side effect was that the 10-second suppression window became area-local (250 world units), so simultaneous attacks on cargo planes at different map locations each triggered their own alarm — causing the reported spam.

PR #2540 worked around this by adding PRESERVE_RADAR_WARNING_SUPPRESSION, which forces map-wide 10-second suppression for all RADAR_EVENT_UNDER_ATTACK events. This restores retail noise levels but introduces a new problem: a cargo plane alarm suppresses a legitimate base under attack warning for the same 10-second window, even though the two events are in completely different locations.

This fix

Adds a m_lastUnderAttackAlarmFrame field to RadarObject (initialized to 0xFFFFFFFF so unsigned subtraction safely yields "never fired"). In Radar::tryUnderAttackEvent, the per-object frame is checked before calling tryEvent: if this specific object has already alarmed within the last 10 seconds, the call is skipped entirely.

PRESERVE_RADAR_WARNING_SUPPRESSION is set to 0. The original area-based (250-unit radius) suppression in tryEvent handles the case of multiple nearby units being attacked simultaneously.

Result:

  • A single supply plane being continuously shot at alarms at most once per 10 seconds (per object).
  • A base under attack at a different map location alarms independently — it has its own RadarObject and its own cooldown.
  • The global radar event slot is no longer used as a cross-object suppression mechanism for under-attack events.

Files changed

  • Core/GameEngine/Include/Common/GameDefines.hPRESERVE_RADAR_WARNING_SUPPRESSION → 0
  • Core/GameEngine/Include/Common/Radar.h — add m_lastUnderAttackAlarmFrame to RadarObject
  • Core/GameEngine/Source/Common/System/Radar.cpp — initialize field; add per-object cooldown check in tryUnderAttackEvent

Test plan

  • Supply plane is shot down — "unit under attack" alarm fires once, not repeatedly during the plane's destruction
  • Multiple supply planes attacked simultaneously — each fires at most one alarm (not N alarms in rapid succession)
  • Base under attack at the same time as a supply plane — both locations report an alarm, not just the first one
  • Normal unit or structure under attack — alarm still fires as expected
  • 10 seconds after an alarm, attacking the same unit again — alarm fires again (cooldown expired)

…alarm

Each object now tracks the last logic frame its under-attack alarm fired.
A new alarm from the same object is suppressed until 10 seconds have elapsed,
regardless of what other objects are doing.

This replaces the map-wide PRESERVE_RADAR_WARNING_SUPPRESSION workaround
(which could suppress a legitimate base-under-attack warning because a cargo
plane's alarm had fired moments earlier). With per-object cooldown each unit
is silenced independently, so simultaneous attacks at different locations each
report correctly while a single continuously-hit unit (e.g. supply plane)
cannot spam the alarm every 10 seconds.

PRESERVE_RADAR_WARNING_SUPPRESSION is set to 0; the area-based 250-unit radius
suppression in tryEvent handles the case of multiple units in the same locale.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the blunt map-wide PRESERVE_RADAR_WARNING_SUPPRESSION workaround with a targeted per-RadarObject cooldown (m_lastUnderAttackAlarmFrame), so that objects at different map locations can each raise an independent under-attack alarm without any one alarm suppressing others globally.

  • PRESERVE_RADAR_WARNING_SUPPRESSION is set to 0, re-enabling the original 250-unit area-based deduplication in tryEvent.
  • RadarObject gains m_lastUnderAttackAlarmFrame (initialized to 0xFFFFFFFF) and tryUnderAttackEvent checks it before calling tryEvent, updating it only when an alarm actually fires.
  • The new field is not serialized in RadarObject::xfer, meaning the per-object cooldown resets to "never fired" on every save/load; whether this is intentional transient state or an oversight is worth a clarifying note in the xfer version comment.

Confidence Score: 4/5

The core logic is sound and the cooldown correctly resets only on a successful alarm; the only open question is whether the missing xfer serialization for the new field is intentional.

The per-object cooldown approach is logically correct and handles all the described scenarios (simultaneous attacks, independent base/plane alarms, 10-second repeat). The one observation is that m_lastUnderAttackAlarmFrame is not serialized in RadarObject::xfer, so the cooldown silently resets on every save/load. For purely transient notification state this is probably fine, but it is undocumented and deviates from the pattern of other persisted fields in the same class.

Core/GameEngine/Source/Common/System/Radar.cpp — specifically the xfer method and whether the new field should be serialized or explicitly noted as transient.

Important Files Changed

Filename Overview
Core/GameEngine/Include/Common/GameDefines.h Sets PRESERVE_RADAR_WARNING_SUPPRESSION to 0, disabling the old map-wide suppression hack in favour of the new per-object cooldown; accompanied by an explanatory comment.
Core/GameEngine/Include/Common/Radar.h Adds m_lastUnderAttackAlarmFrame (UnsignedInt) to RadarObject; field placement and comment are consistent with surrounding members.
Core/GameEngine/Source/Common/System/Radar.cpp Initializes m_lastUnderAttackAlarmFrame to 0xFFFFFFFF in the constructor and gates tryUnderAttackEvent on a per-object cooldown; the new field is not serialized in xfer, so the cooldown silently resets on save/load.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant tryUnderAttackEvent
    participant RadarObject
    participant tryEvent

    Caller->>tryUnderAttackEvent: obj attacked
    tryUnderAttackEvent->>RadarObject: friend_getRadarData()
    RadarObject-->>tryUnderAttackEvent: radarData (or null)

    alt radarData exists AND cooldown not expired
        tryUnderAttackEvent-->>Caller: return (silenced)
    else cooldown expired or no radarData
        tryUnderAttackEvent->>tryEvent: RADAR_EVENT_UNDER_ATTACK, pos
        alt area suppression (within 250 units, within 10s)
            tryEvent-->>tryUnderAttackEvent: false
        else no recent nearby event
            tryEvent->>tryEvent: createEvent()
            tryEvent-->>tryUnderAttackEvent: true
            tryUnderAttackEvent->>RadarObject: "m_lastUnderAttackAlarmFrame = currentFrame"
            tryUnderAttackEvent->>Caller: UI feedback (glow, audio, message)
        end
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant tryUnderAttackEvent
    participant RadarObject
    participant tryEvent

    Caller->>tryUnderAttackEvent: obj attacked
    tryUnderAttackEvent->>RadarObject: friend_getRadarData()
    RadarObject-->>tryUnderAttackEvent: radarData (or null)

    alt radarData exists AND cooldown not expired
        tryUnderAttackEvent-->>Caller: return (silenced)
    else cooldown expired or no radarData
        tryUnderAttackEvent->>tryEvent: RADAR_EVENT_UNDER_ATTACK, pos
        alt area suppression (within 250 units, within 10s)
            tryEvent-->>tryUnderAttackEvent: false
        else no recent nearby event
            tryEvent->>tryEvent: createEvent()
            tryEvent-->>tryUnderAttackEvent: true
            tryUnderAttackEvent->>RadarObject: "m_lastUnderAttackAlarmFrame = currentFrame"
            tryUnderAttackEvent->>Caller: UI feedback (glow, audio, message)
        end
    end
Loading

Comments Outside Diff (1)

  1. Core/GameEngine/Source/Common/System/Radar.cpp, line 139-171 (link)

    P2 Per-object cooldown not persisted across save/load

    m_lastUnderAttackAlarmFrame is initialized to 0xFFFFFFFF in the constructor but is never written or read in RadarObject::xferm_color and the object ID are xferred but this new field is omitted. On every save/load the cooldown silently resets, so an object that fired an alarm immediately before a save will be able to fire it again immediately after load. For a purely transient UI notification this may be intentional, but if so the xfer version comment is worth updating to document the deliberate omission, otherwise a future maintainer may assume it was overlooked.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: Core/GameEngine/Source/Common/System/Radar.cpp
    Line: 139-171
    
    Comment:
    **Per-object cooldown not persisted across save/load**
    
    `m_lastUnderAttackAlarmFrame` is initialized to `0xFFFFFFFF` in the constructor but is never written or read in `RadarObject::xfer``m_color` and the object ID are xferred but this new field is omitted. On every save/load the cooldown silently resets, so an object that fired an alarm immediately before a save will be able to fire it again immediately after load. For a purely transient UI notification this may be intentional, but if so the `xfer` version comment is worth updating to document the deliberate omission, otherwise a future maintainer may assume it was overlooked.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
Core/GameEngine/Source/Common/System/Radar.cpp:139-171
**Per-object cooldown not persisted across save/load**

`m_lastUnderAttackAlarmFrame` is initialized to `0xFFFFFFFF` in the constructor but is never written or read in `RadarObject::xfer``m_color` and the object ID are xferred but this new field is omitted. On every save/load the cooldown silently resets, so an object that fired an alarm immediately before a save will be able to fire it again immediately after load. For a purely transient UI notification this may be intentional, but if so the `xfer` version comment is worth updating to document the deliberate omission, otherwise a future maintainer may assume it was overlooked.

Reviews (1): Last reviewed commit: "Fix #2452: Per-object cooldown for under..." | Re-trigger Greptile

@stephanmeesters

Copy link
Copy Markdown

See guidelines for AI contributions https://github.com/TheSuperHackers/GeneralsGameCode/blob/main/CONTRIBUTING.md

@Skyaero42 Skyaero42 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not look like this implementation has been tested.


#ifndef PRESERVE_RADAR_WARNING_SUPPRESSION
#define PRESERVE_RADAR_WARNING_SUPPRESSION (1)
#define PRESERVE_RADAR_WARNING_SUPPRESSION (0) // Per-object cooldown in tryUnderAttackEvent supersedes this map-wide suppression.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabling another fix is not a proper solution. If this fix superseeds the previous fix and not backwards compatibility is needed, than the old fix needs to be removed. If it needs to be backwards compatible, than this macro should not be changed and your fix should be in the same macro tags (#if !PRESERVE_RADAR_WARNING_SUPPRESSION)

const UnsignedInt framesBetweenAlarms = LOGICFRAMES_PER_SECOND * 10;
UnsignedInt currentFrame = TheGameLogic->getFrame();
RadarObject *radarData = obj->friend_getRadarData();
if( radarData && currentFrame - radarData->m_lastUnderAttackAlarmFrame < framesBetweenAlarms )

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_lastUnderAttackAlarmFrame has the maximum value assigned (line 103). This if statement is therefore always true. Therefore line 1063 will never fire and m_lastUnderAttackAlarmFrame will never be reset.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Unit under attack" alarm fires too often

3 participants