Skip to content

bugfix(audiomanager): Fix dummy audio manager implementation for failing audio length script conditions in headless mode#2313

Open
Caball009 wants to merge 7 commits intoTheSuperHackers:mainfrom
Caball009:fix_dummy_audio_manager
Open

bugfix(audiomanager): Fix dummy audio manager implementation for failing audio length script conditions in headless mode#2313
Caball009 wants to merge 7 commits intoTheSuperHackers:mainfrom
Caball009:fix_dummy_audio_manager

Conversation

@Caball009
Copy link

@Caball009 Caball009 commented Feb 16, 2026

Our current implementation of dummy audio manager is incomplete, because it cannot return the actual length of audio files. Replays will mismatch if they contain scripts that rely on this.

TODO:

  • Make sure the Github CI can access the audio files.
  • Replicate in Generals.

@Caball009 Caball009 added Audio Is audio related Bug Something is not working right, typically is user facing Major Severity: Minor < Major < Critical < Blocker ThisProject The issue was introduced by this project, or this task is specific to this project Script Is related to Script Engine, SCB labels Feb 16, 2026
@Caball009 Caball009 changed the title bugfix(audiomanager): Fix audio manager implementation for failing script conditions in headless mode bugfix(audiomanager): Fix dummy audio manager implementation for failing script audio length conditions in headless mode Feb 16, 2026
@Caball009 Caball009 changed the title bugfix(audiomanager): Fix dummy audio manager implementation for failing script audio length conditions in headless mode bugfix(audiomanager): Fix dummy audio manager implementation for failing audio length script conditions in headless mode Feb 16, 2026
@Caball009 Caball009 force-pushed the fix_dummy_audio_manager branch from 7751f9e to 477d80b Compare February 28, 2026 16:19
@Caball009 Caball009 marked this pull request as ready for review February 28, 2026 16:21
@greptile-apps
Copy link

greptile-apps bot commented Feb 28, 2026

Greptile Summary

This PR replaces the simple AudioManagerDummy (which returned -1 for getFileLengthMS) with a new MilesAudioManagerDummy that inherits from MilesAudioManager, allowing the real Miles audio device initialization to run so that getFileLengthMS can query actual audio file lengths in headless mode. The createAudioManager and createRadar factory methods are refactored to accept a Bool dummy parameter, moving the headless conditional from GameEngine.cpp into the device-layer factories.

  • AudioManagerDummy is removed from the shared Core/GameAudio.h header and replaced by MilesAudioManagerDummy in MilesAudioManager.h, correctly scoping the Miles dependency to the device layer.
  • createAudioManager(Bool dummy) and createRadar(Bool dummy) signatures are updated across GameEngine.h (abstract), Win32GameEngine.h (concrete), and the call sites in GameEngine.cpp.
  • A key risk remains: MilesAudioManager::openDevice() returns early if !TheGlobalData->m_audioOn, leaving m_digitalHandle as nullptr. Since getFileLengthMS passes m_digitalHandle directly to AIL_open_stream, the fix will silently fail (returning 0.0f) on any headless environment where m_audioOn is false — exactly the scenario the PR targets.
  • A newly placed @feature comment carries a 17/05/2025 date which is prior to the current year and should be updated.

Confidence Score: 3/5

  • Safe to merge as a structural improvement, but the core fix (correct getFileLengthMS values in headless mode) may silently fail if m_audioOn is false in the target environment.
  • The refactoring of factory methods and the deletion of AudioManagerDummy from the shared header are clean and correct. However, the fundamental goal — returning real audio file lengths in headless mode — depends on m_audioOn being true so that MilesAudioManager::openDevice() populates m_digitalHandle. If that condition is not met, getFileLengthMS returns 0.0f silently, reproducing the same mismatch as before. The PR's own TODO acknowledges unresolved CI concerns, indicating this path has not been fully validated.
  • Pay close attention to Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h — specifically whether openDevice() should be overridden in MilesAudioManagerDummy to bypass the m_audioOn guard.

Important Files Changed

Filename Overview
Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h Adds MilesAudioManagerDummy class inheriting MilesAudioManager to fix getFileLengthMS in headless mode; has a logic risk where m_digitalHandle stays null if m_audioOn is false, and a style issue with a 2025-dated comment placed newly in this file.
Core/GameEngine/Include/Common/GameAudio.h Removes the AudioManagerDummy class from the shared core header; the replacement (MilesAudioManagerDummy) lives in the device-layer header as intended.
GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp Replaces inline ternary instantiation of AudioManagerDummy/RadarDummy with calls to createAudioManager(Bool) and createRadar(Bool) factory methods, correctly delegating the dummy-vs-real decision to the concrete engine subclass.
GeneralsMD/Code/GameEngineDevice/Include/Win32Device/Common/Win32GameEngine.h Updates createRadar and createAudioManager signatures and inline implementations to accept a Bool dummy parameter and conditionally return the dummy or real implementation.
GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h Pure virtual signatures for createAudioManager and createRadar updated to accept Bool dummy; straightforward, no issues.

Sequence Diagram

sequenceDiagram
    participant GE as GameEngine::init()
    participant W32 as Win32GameEngine
    participant MAMD as MilesAudioManagerDummy
    participant MAM as MilesAudioManager
    participant Miles as Miles Sound System

    GE->>W32: createAudioManager(m_headless=true)
    W32-->>GE: NEW MilesAudioManagerDummy

    GE->>MAMD: initSubsystem / init()
    MAMD->>MAM: MilesAudioManager::init()
    MAM->>MAMD: openDevice() [inherited, not overridden]
    MAMD->>MAM: MilesAudioManager::openDevice()
    alt m_audioOn == true
        MAM->>Miles: AIL_startup() + AIL_quick_startup()
        Miles-->>MAM: m_digitalHandle (valid)
        MAM-->>MAMD: device ready
    else m_audioOn == false
        MAM-->>MAMD: early return, m_digitalHandle stays nullptr
    end

    Note over GE,Miles: Script condition queries audio length

    GE->>MAMD: getFileLengthMS(filename)
    MAMD->>MAM: MilesAudioManager::getFileLengthMS() [inherited]
    alt m_digitalHandle != nullptr
        MAM->>Miles: AIL_open_stream(m_digitalHandle, filename)
        Miles-->>MAM: valid stream
        MAM-->>GE: actual length in ms ✓
    else m_digitalHandle == nullptr
        MAM->>Miles: AIL_open_stream(nullptr, filename)
        Miles-->>MAM: null stream
        MAM-->>GE: 0.0f (fix silently fails) ✗
    end
Loading

Last reviewed commit: 9432bea

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

@Caball009
Copy link
Author

Addressed feedback.

Comment on lines +333 to +334
// TheSuperHackers @feature helmutbuhler 17/05/2025 AudioManager that does almost nothing. Useful for headless mode.
// @bugfix Caball009 16/02/2026 Scripts may require the actual audio file length to function properly, which is important for the CRC computation.
Copy link

Choose a reason for hiding this comment

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

Stale date in newly added comment

The @feature comment referencing 17/05/2025 is newly placed in this file (it was moved from GameAudio.h). Per the project convention, newly created comments should not reference dates prior to the current year (2026). Consider updating the year in this line to 2026, or consolidating the two-line header into a single comment block with a 2026 date.

Suggested change
// TheSuperHackers @feature helmutbuhler 17/05/2025 AudioManager that does almost nothing. Useful for headless mode.
// @bugfix Caball009 16/02/2026 Scripts may require the actual audio file length to function properly, which is important for the CRC computation.
// TheSuperHackers @bugfix helmutbuhler/Caball009 2026 AudioManager that does almost nothing. Useful for headless mode.
// Scripts may require the actual audio file length to function properly, which is important for the CRC computation.
// The Miles AudioManager handles the device opening / closure, so that getFileLengthMS can function as intended.

Context Used: Rule from dashboard - What: Flag newly created code comments that reference dates prior to the current year (2026).

Why: ... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h
Line: 333-334

Comment:
**Stale date in newly added comment**

The `@feature` comment referencing `17/05/2025` is newly placed in this file (it was moved from `GameAudio.h`). Per the project convention, newly created comments should not reference dates prior to the current year (2026). Consider updating the year in this line to 2026, or consolidating the two-line header into a single comment block with a 2026 date.

```suggestion
// TheSuperHackers @bugfix helmutbuhler/Caball009 2026 AudioManager that does almost nothing. Useful for headless mode.
// Scripts may require the actual audio file length to function properly, which is important for the CRC computation.
// The Miles AudioManager handles the device opening / closure, so that getFileLengthMS can function as intended.
```

**Context Used:** Rule from `dashboard` - What: Flag newly created code comments that reference dates prior to the current year (2026).

Why: ... ([source](https://app.greptile.com/review/custom-context?memory=fd72a556-4fd8-4db4-8b08-8e51516a64ad))

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +336 to +382
class MilesAudioManagerDummy : public MilesAudioManager
{
#if defined(RTS_DEBUG)
virtual void audioDebugDisplay(DebugDisplayInterface* dd, void* userData, FILE* fp) {}
#endif
virtual void stopAudio(AudioAffect which) {}
virtual void pauseAudio(AudioAffect which) {}
virtual void resumeAudio(AudioAffect which) {}
virtual void pauseAmbient(Bool shouldPause) {}
virtual void killAudioEventImmediately(AudioHandle audioEvent) {}
virtual void nextMusicTrack() {}
virtual void prevMusicTrack() {}
virtual Bool isMusicPlaying() const { return false; }
virtual Bool hasMusicTrackCompleted(const AsciiString& trackName, Int numberOfTimes) const { return false; }
virtual AsciiString getMusicTrackName() const { return ""; }
//virtual void openDevice() {}
//virtual void closeDevice() {}
//virtual void* getDevice() { return nullptr; }
virtual void notifyOfAudioCompletion(UnsignedInt audioCompleted, UnsignedInt flags) {}
virtual UnsignedInt getProviderCount() const { return 0; };
virtual AsciiString getProviderName(UnsignedInt providerNum) const { return ""; }
virtual UnsignedInt getProviderIndex(AsciiString providerName) const { return 0; }
virtual void selectProvider(UnsignedInt providerNdx) {}
virtual void unselectProvider() {}
virtual UnsignedInt getSelectedProvider() const { return 0; }
virtual void setSpeakerType(UnsignedInt speakerType) {}
virtual UnsignedInt getSpeakerType() { return 0; }
virtual UnsignedInt getNum2DSamples() const { return 0; }
virtual UnsignedInt getNum3DSamples() const { return 0; }
virtual UnsignedInt getNumStreams() const { return 0; }
virtual Bool doesViolateLimit(AudioEventRTS* event) const { return false; }
virtual Bool isPlayingLowerPriority(AudioEventRTS* event) const { return false; }
virtual Bool isPlayingAlready(AudioEventRTS* event) const { return false; }
virtual Bool isObjectPlayingVoice(UnsignedInt objID) const { return false; }
virtual void adjustVolumeOfPlayingAudio(AsciiString eventName, Real newVolume) {}
virtual void removePlayingAudio(AsciiString eventName) {}
virtual void removeAllDisabledAudio() {}
virtual Bool has3DSensitiveStreamsPlaying() const { return false; }
virtual void* getHandleForBink() { return nullptr; }
virtual void releaseHandleForBink() {}
virtual void friend_forcePlayAudioEventRTS(const AudioEventRTS* eventToPlay) {}
virtual void setPreferredProvider(AsciiString providerNdx) {}
virtual void setPreferredSpeaker(AsciiString speakerType) {}
//virtual Real getFileLengthMS(AsciiString strToLoad) const { return 0.0f; }
virtual void closeAnySamplesUsingFile(const void* fileToClose) {}
virtual void setDeviceListenerPosition() {}
};
Copy link

Choose a reason for hiding this comment

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

getFileLengthMS may silently return 0 if m_audioOn is false

MilesAudioManagerDummy intentionally does not override openDevice() so that MilesAudioManager::openDevice() runs and populates m_digitalHandle. However, MilesAudioManager::openDevice() contains an early return guard:

if (!TheGlobalData->m_audioOn) {
    return;
}

m_digitalHandle is initialized to nullptr in the constructor and remains nullptr if that guard fires. When getFileLengthMS is subsequently called it reaches:

HSTREAM stream = AIL_open_stream(m_digitalHandle, strToLoad.str(), 0);
if (!stream) {
    return 0.0f;
}

Because AIL_open_stream with a null HDIGDRIVER will return a null stream, getFileLengthMS silently returns 0.0f — the same value as before this fix. If m_audioOn is false in headless environments (e.g. CI runners without audio hardware), the CRC mismatch that motivated this PR will persist.

Consider either:

  • Overriding openDevice() in MilesAudioManagerDummy to call AIL_startup() + AIL_quick_handles() unconditionally (bypassing the m_audioOn guard), or
  • Documenting explicitly that this fix only works when m_audioOn is true and ensuring headless mode forces m_audioOn = true.
Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngineDevice/Include/MilesAudioDevice/MilesAudioManager.h
Line: 336-382

Comment:
**`getFileLengthMS` may silently return `0` if `m_audioOn` is false**

`MilesAudioManagerDummy` intentionally does not override `openDevice()` so that `MilesAudioManager::openDevice()` runs and populates `m_digitalHandle`. However, `MilesAudioManager::openDevice()` contains an early return guard:

```cpp
if (!TheGlobalData->m_audioOn) {
    return;
}
```

`m_digitalHandle` is initialized to `nullptr` in the constructor and remains `nullptr` if that guard fires. When `getFileLengthMS` is subsequently called it reaches:

```cpp
HSTREAM stream = AIL_open_stream(m_digitalHandle, strToLoad.str(), 0);
if (!stream) {
    return 0.0f;
}
```

Because `AIL_open_stream` with a null `HDIGDRIVER` will return a null stream, `getFileLengthMS` silently returns `0.0f` — the same value as before this fix. If `m_audioOn` is `false` in headless environments (e.g. CI runners without audio hardware), the CRC mismatch that motivated this PR will persist.

Consider either:
- Overriding `openDevice()` in `MilesAudioManagerDummy` to call `AIL_startup()` + `AIL_quick_handles()` unconditionally (bypassing the `m_audioOn` guard), or
- Documenting explicitly that this fix only works when `m_audioOn` is `true` and ensuring headless mode forces `m_audioOn = true`.

How can I resolve this? If you propose a fix, please make it concise.

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

Labels

Audio Is audio related Bug Something is not working right, typically is user facing Major Severity: Minor < Major < Critical < Blocker Script Is related to Script Engine, SCB ThisProject The issue was introduced by this project, or this task is specific to this project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incomplete implementation of dummy audio manager can cause mismatches in headless mode

2 participants