Skip to content

feat: sound Qt#1678

Draft
kokekanon wants to merge 2 commits intoopentibiabr:mainfrom
kokekanon:sound15
Draft

feat: sound Qt#1678
kokekanon wants to merge 2 commits intoopentibiabr:mainfrom
kokekanon:sound15

Conversation

@kokekanon
Copy link
Copy Markdown
Contributor

@kokekanon kokekanon commented Mar 29, 2026

Make sure the video isn't muted

2026-03-28.19-00-44.mp4

#1098
#1074

Summary by CodeRabbit

  • New Features

    • Introduced granular audio controls with master volume, music, ambience, and event volume sliders.
    • Added Battle Sounds panel with per-subchannel toggles (spells, attacks, healing, weapons).
    • Added UI Sounds panel with notification settings (party, guild, chat, NPC announcements).
    • Added audio feedback for buttons and windows.
  • Chores

    • Refactored audio options structure for improved sound channel management.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive sound management system overhaul, adding granular audio channel controls, item ambience tracking, protocol-based sound playback, a dedicated debug UI, and UI sound integration. New sound manager implementations handle debug snapshots, item-based ambient sounds, and protocol-driven audio routing alongside expanded sound channels and per-widget sound capabilities.

Changes

Cohort / File(s) Summary
Sound Framework Core
src/framework/sound/soundmanager.cpp, soundmanager.h, soundmanager_types.h, soundmanager_debug.cpp, soundmanager_itemambience.cpp, soundmanager_protocol.cpp
Introduces comprehensive sound management system with item ambience lifecycle tracking, protocol-based sound playback with eligibility checks and cooldowns, debug snapshot generation, event recording, channel-specific playback helpers, and audio path routing. Adds ClientSoundType/ClientMusicType enums, debug state structures, and consolidated type definitions in new soundmanager_types.h.
Client Game Integration
src/client/creature.cpp, game.cpp, map.cpp, protocolgameparse.cpp, uimap.cpp, uimap.h
Integrates sound manager with game systems: listener position updates on creature movement, channel stopping on game end, item ambience tile change tracking, protocol sound playback on magic effects, and new UIMap tile query methods (getTilePoint, getTileRect) for positional sound visualization.
UIWidget Sound Support
src/framework/ui/uiwidget.cpp, uiwidget.h, uiwidgetbasestyle.cpp
Adds UI sound mapping via setClickSound, addSound, removeSound methods; plays click sound on interaction and show sound on visibility; parses click-sound and display-sound OTML style tags.
Button & Window Sound Integration
data/styles/10-buttons.otui, 10-windows.otui, modules/corelib/ui/uibutton.lua, uiminiwindow.lua
Assigns default click sounds to buttons (2774) and display sounds to windows (2785); updates button and miniwindow creation to configure sound playback.
Sound Channel Constants
modules/corelib/const.lua
Extends SoundChannels enum with new channel types (Spells, Item, Event, OwnBattles, OthersPlayers, Creature, SoundUI, Bot, secundaryChannel) and introduces ESoundUI enum for UI sound types.
Client Options Overhaul
modules/client_options/data_options.lua, options.lua
Replaces simple audio toggles with granular per-channel volume controls and sub-channel enablement; adds protocol sound setting synchronization; implements deferred music channel enable/disable with threshold detection; introduces battle and UI sound panel routing.
Sound Options UI
modules/client_options/styles/sound/audio.otui (removed), sound.otui, battleSounds.otui, uiSounds.otui
Removes legacy audio panel; adds new master/music/ambience/items volume controls; adds battle sound sub-channel toggles with per-category volume sliders; adds UI notification sound toggles with granular category options.
Sound Debug Module
modules/game_soundDebug/sounddebug.lua, sounddebug.otmod, sounddebug.otui
Introduces toggleable in-game sound debug overlay with channel meters, active source listings, ambient item cards, positional event history, map-based audio visualization, and field-grid preview with listener/player/event markers; includes keybind (Ctrl+Alt+S) and auto-update scheduling.
Build Configuration
src/CMakeLists.txt, vc18/otclient.vcxproj, modules/game_interface/interface.otmod
Adds new sound manager implementation files to build system; registers game_soundDebug module in game interface load-later list.
Lua Bindings
src/client/luafunctions.cpp, luavaluecasts_client.cpp, luavaluecasts_client.h, src/framework/luafunctions.cpp
Exposes new sound manager methods (getRandomSoundIds, getSoundEffectType, refreshProtocolSoundSettings, toggleDebugMode, isDebugMode, getDebugSnapshot), UIWidget sound controls, UIMap tile queries, and sound debug type serialization to Lua.
Sound State Persistence
src/framework/sound/soundsource.cpp, soundsource.h, combinedsoundsource.cpp, streamsoundsource.cpp, streamsoundsource.h, soundchannel.cpp
Enhances sound sources to persist property state (pitch, position, velocity, looping, relative) in member variables; adds source kind tracking; implements streaming-specific looping override; routes channel assignments and fade-source handling.
Miscellaneous
modules/client/client.lua, modules/game_screenshot/game_screenshot.lua, modules/game_things/things.lua
Refactors music channel event handling to connect during init rather than startup; adds guard against re-initialization in screenshot module; adds nil check for sound manager availability in things loader.

Sequence Diagram(s)

sequenceDiagram
    participant Game
    participant SoundManager
    participant ProtocolParser
    participant SoundChannel
    participant AudioDevice
    participant Lua/UI

    Note over Game,Lua/UI: Protocol-Based Sound Playback Flow

    Game->>ProtocolParser: onMagicEffect(MAGIC_EFFECTS_CREATE_SOUND_MAIN_EFFECT)
    ProtocolParser->>SoundManager: playProtocolSoundMain(soundSource, soundEffectId, pos)
    SoundManager->>SoundManager: shouldPlayProtocolSound(checks: enabled, volume, cooldown)
    alt Playback Eligible
        SoundManager->>SoundManager: chooseProtocolAudioFileId(soundEffectId)
        SoundManager->>SoundManager: playProtocolAudioFileId(audioFileId, pos, channelId)
        SoundManager->>SoundManager: buildProtocolSoundPath(fileName)
        SoundManager->>SoundChannel: playChannelSound(filename, gain, pitch)
        SoundChannel->>AudioDevice: Play audio with spatial positioning
        SoundManager->>SoundManager: recordProtocolDebugEvent(if debug enabled)
        SoundManager->>Lua/UI: Update debug snapshot on poll
    else Ineligible
        SoundManager->>SoundManager: Skip playback (log reason if debug)
    end
Loading
sequenceDiagram
    participant Game
    participant Map
    participant SoundManager
    participant ItemAmbienceTracker
    participant SoundChannel
    participant AudioDevice

    Note over Game,AudioDevice: Item Ambience Lifecycle

    Map->>SoundManager: onItemTileChanged(position)
    SoundManager->>ItemAmbienceTracker: Mark tile dirty
    
    Game->>Map: setCentralPosition(newPos)
    Map->>SoundManager: onListenerPositionChanged(newPos, oldPos)
    SoundManager->>ItemAmbienceTracker: Rescan tiles if distance/z changed
    
    SoundManager->>SoundManager: poll() - updateItemAmbienceSources()
    SoundManager->>ItemAmbienceTracker: Rebuild/update tracked tiles if dirty
    
    loop Per Active Effect
        ItemAmbienceTracker->>SoundManager: Get current itemCount from tiles
        SoundManager->>SoundManager: resolveItemAmbientAudioFileId(itemCount)
        SoundManager->>SoundManager: Compute gain from nearest distance
        SoundManager->>SoundChannel: playChannelSound(looping, gain, pan)
        SoundChannel->>AudioDevice: Stream ambient audio with spatial audio
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • majestyotbr
  • andreoam

Poem

🐰 Beep boop! Hark, the soundscape grows,
With channels deep and ambience that flows,
Debug UI shows each source with glee,
From battle roars to UI clicks, let listeners see!
A symphony of code, precisely arranged,
The audio framework is beautifully changed! 🎵

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: sound Qt' is vague and does not clearly convey the scope of changes. The PR makes extensive audio system improvements including sound channels, UI sound integration, debug tooling, and protocol-based sound playback. Consider a more descriptive title like 'feat: add comprehensive sound system with UI integration and debugging tools' or 'feat: implement Qt-based audio channel management and sound UI controls'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/framework/sound/combinedsoundsource.cpp (1)

62-108: ⚠️ Potential issue | 🟡 Minor

Fix state persistence in setFading() for consistency.

The changes correctly persist values to member variables before propagating to child sources. However, setFading() does not persist to the inherited member variables (m_fadeState, m_fadeTime, m_fadeStartTime, m_fadeGain), unlike other setters in this class. To maintain consistency with the established pattern (setLooping, setGain, setPitch, etc.), setFading() should set these inherited members in addition to propagating to child sources.

(Note: setReferenceDistance() has no corresponding member variable to persist, so it correctly only propagates to children.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/sound/combinedsoundsource.cpp` around lines 62 - 108,
CombinedSoundSource::setFading currently propagates fading to child sources but
doesn't persist the values to the inherited member variables; update setFading
to assign the incoming values to m_fadeState, m_fadeTime, m_fadeStartTime, and
m_fadeGain before iterating m_sources so the object's fade state is consistent
with other setters (e.g., setLooping, setGain) and then continue propagating to
each child source.
src/framework/sound/soundmanager.h (1)

45-50: ⚠️ Potential issue | 🟠 Major

Default-initialize maxSoundDistance.

The new field is read as a sentinel in the item-ambience path, so leaving it uninitialized makes that branch undefined if any parse path or default construction misses the assignment.

🧯 Proposed fix
 struct ClientItemAmbient
 {
     uint32_t id;
     std::vector<uint32_t> clientIds;
-    uint32_t maxSoundDistance;
+    uint32_t maxSoundDistance{ 0 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/sound/soundmanager.h` around lines 45 - 50, The new uint32_t
field maxSoundDistance in struct ClientItemAmbient is left uninitialized; make
it default-initialized to a safe sentinel (e.g., 0 or other agreed sentinel) in
the member declaration so default-constructed or partially-parsed
ClientItemAmbient instances have a defined value; also update any constructors
or aggregate initializations that might rely on an implicit value to ensure they
don't overwrite the default unintentionally (refer to ClientItemAmbient and
maxSoundDistance).
🧹 Nitpick comments (5)
modules/corelib/ui/uiminiwindow.lua (1)

7-7: Verify sound ID selection.

Sound ID 2785 is used here for click sounds, which is the same ID used for display-sound in 10-windows.otui. Meanwhile, the base Button style uses 2774 for click sounds.

Is using the same sound for miniwindow clicks and window display intentional? Different sounds typically help users distinguish between clicking/interacting vs. windows opening.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/corelib/ui/uiminiwindow.lua` at line 7, The miniwindow currently
registers the click sound with miniwindow:addSound(ESoundUI.SoundTypeClick,
2785), which duplicates the display-sound ID used elsewhere and differs from the
base Button click ID 2774; decide whether miniwindow clicks should use the
distinct Button click sound (2774) or a unique new ID, then update the call to
miniwindow:addSound(ESoundUI.SoundTypeClick, <chosen-id>) accordingly and ensure
any corresponding OTUI/10-windows.otui entries are adjusted to keep click vs
display sounds distinct (reference miniwindow:addSound and
ESoundUI.SoundTypeClick).
modules/corelib/ui/uibutton.lua (1)

7-7: Prefer style-driven default for click sound instead of hardcoding it here.

This duplicates theme/style responsibility and makes future sound-theme overrides harder. Consider keeping the default in OTUI only (or via a shared constant).

♻️ Suggested adjustment
 function UIButton.create()
     local button = UIButton.internalCreate()
     button:setFocusable(false)
-    button:setClickSound(2774)
     button.cursorPushed = false
     return button
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/corelib/ui/uibutton.lua` at line 7, Remove the hardcoded sound id set
by button:setClickSound(2774) and instead apply the theme/style-driven default:
read the click sound from the button's style/theme (or from a shared constant
like DEFAULT_CLICK_SOUND exposed by the OTUI theme module) and call
button:setClickSound(...) with that value; update any initialization around the
UI button creation in uibutton.lua to fall back to the shared/default value when
the style does not specify one so the click sound can be overridden by theme
changes.
src/framework/ui/uiwidget.h (1)

449-451: Consider using UISoundType enum for type safety.

The addSound and removeSound methods use int for the sound type parameter. Using UISoundType would provide compile-time type safety and better self-documentation.

♻️ Proposed refactor for type safety
-    void setClickSound(int soundId);
-    void addSound(int soundType, int soundId);
-    void removeSound(int soundType);
+    void setClickSound(int soundId);
+    void addSound(UISoundType soundType, int soundId);
+    void removeSound(UISoundType soundType);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/ui/uiwidget.h` around lines 449 - 451, Change the sound-type
parameters from int to the UISoundType enum to enforce type safety: update the
declarations of addSound(int soundType, int soundId) and removeSound(int
soundType) to addSound(UISoundType soundType, int soundId) and
removeSound(UISoundType soundType), and propagate the type change through
corresponding implementations and all call sites (including any overloads or
tests) to use UISoundType instead of raw ints; ensure any switch/case or map
keys that previously used ints are updated to accept UISoundType and include
necessary header or forward declaration for UISoundType.
src/client/luafunctions.cpp (1)

1178-1181: Consider relocating g_sounds binding to a more logical position.

The g_sounds singleton binding is placed between UIMap class bindings and UIMinimap registration. For better code organization, consider moving this binding to be with other singleton registrations (near the top of the function where g_things, g_map, etc. are registered), or at least add a comment explaining why it's placed here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/luafunctions.cpp` around lines 1178 - 1181, The g_sounds singleton
binding (g_lua.bindSingletonFunction("g_sounds", "getDebugSnapshot",
&SoundManager::getDebugSnapshot, &g_sounds)) is placed amid UIMap/UIMinimap
class bindings and should be moved or documented: relocate this bind call to the
block where other singletons (e.g., g_things, g_map) are registered near the top
of the function to keep singleton registrations together, or if there is a
specific reason for its current placement, add a clear comment above the binding
explaining why it must remain here; update references to
SoundManager::getDebugSnapshot and g_sounds accordingly.
modules/client_options/data_options.lua (1)

810-834: Consider simplifying the aux state tracking logic.

The aux field is used to track whether the enabled/disabled state has changed, but the double-negation and comparison logic (shouldBeDisabled ~= (not options.soundMusic.aux)) is hard to follow.

Consider renaming aux to something descriptive like wasEnabled or previouslyDisabled, and simplifying the condition:

local wasDisabled = not options.soundMusic.aux
if shouldBeDisabled ~= wasDisabled then
    ...
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/client_options/data_options.lua` around lines 810 - 834, The aux flag
in the soundMusic table (inside the action function) is confusing; rename
options.soundMusic.aux to a clear boolean like options.soundMusic.wasDisabled
(or wasEnabled) and simplify the comparison by computing a local wasDisabled =
not options.soundMusic.wasDisabled (or local wasEnabled =
options.soundMusic.wasEnabled) and then use if shouldBeDisabled ~= wasDisabled
then (or if shouldBeDisabled ~= (not options.soundMusic.wasEnabled)
accordingly). Update the assignment that flips the flag (currently
options.soundMusic.aux = not shouldBeDisabled) to set the new descriptive flag
consistently (e.g. options.soundMusic.wasDisabled = shouldBeDisabled or
options.soundMusic.wasEnabled = not shouldBeDisabled) and leave the
removeEvent/scheduleEvent/g_sounds calls unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modules/client_options/data_options.lua`:
- Around line 767-808: The master volume handler (soundMaster.action) currently
writes channel gains directly via g_sounds.getChannel(...):setGain(value / 100),
which overwrites per-channel sliders (soundMusic, soundAmbience, soundItems,
soundEventVolume) leading to last-write-wins; change to multiplicative behavior
by applying effectiveGain = (masterValue/100) * (channelOptionValue/100) for
each channel instead of just masterValue/100, and add/update a small helper
(e.g., applyChannelGain or use the existing channel setters) so both
soundMaster.action and each individual channel setter (soundMusic,
soundAmbience, soundItems, soundEventVolume) compute and set
g_sounds.getChannel(channelId):setGain(master * channel) so updates from either
control preserve the multiplicative relationship.

In `@modules/client_options/styles/sound/battleSounds.otui`:
- Around line 32-55: Widget IDs for the battle sound sub-channel controls are
inconsistent (e.g., battleSoundOwnBattlesubChannelsSpells,
battleSoundOwnBattleSubChannelsAttack,
battleSoundOwnBattleSoundSubChannelsHealing/Support) which will break lookups;
rename IDs to a single consistent pattern (for example:
battleSoundOwnBattleSubChannelsSpells, battleSoundOwnBattleSubChannelsAttack,
battleSoundOwnBattleSubChannelsHealing, battleSoundOwnBattleSubChannelsSupport)
and update any places that reference the old IDs (including the QtCheckBox
`@onCheckChange` callbacks and any Lua code in modules.client_options that
reads/writes options via setOption/getId). Ensure you remove the duplicated
"Sound" and fix the lowercase "s" in "subChannels" so all IDs match exactly.

In `@modules/client_options/styles/sound/sound.otui`:
- Around line 15-17: The sliders are incorrectly clamped to 1–100; update each
slider block to allow 0 by changing &minimumScrollValue from 1 to 0 (e.g.,
update the &minimumScrollValue in the blocks containing
&minimumScrollValue/&maximumScrollValue/&scrollSize at the shown occurrences —
lines 15-17, 35-37, 61-63, 82-84, 113-115) so the range becomes 0–100 and the
panel can represent a true muted (0) volume.

In `@modules/client_options/styles/sound/uiSounds.otui`:
- Around line 66-68: The option id "soundNotificationUIInteractions" must be
renamed to follow the existing soundNotificationsubChannels* pattern so it
round-trips with stored settings; change the id to
"soundNotificationsubChannelsUIInteractions" (or the exact existing subchannel
key used elsewhere) in the OptionCheckBox definition and update any references
that read/write subchannel keys (e.g., code handling
soundNotificationsubChannels*) so the UI toggle maps to the correct stored
subchannel.
- Around line 108-110: The tooltip for the setting identified by id
"soundNotificationsubChannelsSystemAnnouncements" is incorrect (it references
equipped-item timers); update the !tooltip value to a relevant, localized string
via tr(), e.g. tr('Play a sound when system announcements are posted') or
similar, so that the tooltip matches the feature (system announcement sound
notifications) and replace the existing incorrect text in the UI resource for
soundNotificationsubChannelsSystemAnnouncements.

In `@modules/corelib/const.lua`:
- Around line 332-342: Rename the misspelled key 'secundaryChannel' to
'secondaryChannel' in the constant table (replace the identifier
secundaryChannel = 12 with secondaryChannel = 12) and search the codebase for
any references to 'secundaryChannel' to update them to 'secondaryChannel' so
usages (lookups, assignments, comparisons) remain consistent.

In `@modules/game_soundDebug/sounddebug.lua`:
- Around line 188-195: The resetSummaries()/clearVisuals() functions call
setText() on UI label objects that may be nil if setupWidgets() hasn't run;
guard these calls by first checking the window/widget initialization (e.g., if
headerLabel and mixSummaryLabel and sourceSummaryLabel etc. are non-nil) or add
an isInitialized flag set in setupWidgets(), and only call setText() when
initialized; update resetSummaries(), clearVisuals(), and the other similar
blocks (the other summary-reset functions mentioned) to perform these nil checks
or gate the entire function behind the isInitialized boolean before mutating any
labels.

In `@src/client/uimap.cpp`:
- Around line 162-177: getTilePoint and getTileRect currently return physical
pixels because they add m_mapviewRect (already scaled by
g_window.getDisplayDensity()); change them to return logical/UI coordinates by
removing that added physical offset and instead account for display density: in
getTilePoint (and any use of transformPositionTo2D) stop adding
m_mapviewRect.topLeft() directly (or divide that topLeft by
g_window.getDisplayDensity()), so the returned point is in UI/widget
coordinates, and in getTileRect compute width/height in logical pixels (divide
m_mapView->m_tileSize * horizontal/verticalStretchFactor by
g_window.getDisplayDensity() before rounding and clamping). Update references to
m_mapviewRect.topLeft(), getTilePoint, and getTileRect accordingly so the
Lua-facing API returns logical UI coords consistent with the rest of UIMap.

In `@src/framework/sound/soundmanager_protocol.cpp`:
- Around line 172-186: The current per-category volume
(getProtocolVolumeSetting) is used only as an on/off gate in
SoundManager::shouldPlayProtocolSound so gain later ignores category volume;
change callers to compute a scaledCategoryVolume =
std::clamp(getProtocolVolumeSetting(soundSource), 0, 100) / 100.0f and pass that
scaled float into the playback/gain calculation instead of assuming full volume,
then use that same scaledCategoryVolume when calling recordProtocolDebugEvent()
/ logProtocolDebug() so the debug gain reflects category attenuation; keep
existing gating via
shouldPlayProtocolSound/isProtocolSubChannelEnabled/getSoundEffectType but plumb
the scaledCategoryVolume through the playback path and apply it multiplicatively
to the distance-derived gain.
- Around line 110-113: The option keys used when initializing
m_protocolSoundSettings are inconsistent: m_protocolSoundSettings.ownHealing,
.ownSupport and .ownWeapons use
"battleSoundOwnBattleSoundSubChannels{Healing,Support,Weapons}" which includes
an extra "Sound" and will miss the intended setting; update these keys to match
the correct pattern "battleSoundOwnBattleSubChannelsHealing",
"battleSoundOwnBattleSubChannelsSupport" and
"battleSoundOwnBattleSubChannelsWeapons" (the same pattern used by ownAttack) in
the soundmanager_protocol initialization, and scan for other occurrences of the
misspelled keys to correct them as well.

In `@src/framework/sound/soundmanager.cpp`:
- Around line 290-306: playUiSoundById currently ignores randomness by always
using the first ID from getRandomSoundIds; update playUiSoundById to select a
random element from the vector returned by getRandomSoundIds instead of using
.front(). Use a proper random engine (e.g., std::mt19937 seeded once) and
std::uniform_int_distribution to pick an index into the soundIds vector, then
call getAudioFileNameById(selectedId) and proceed to
playChannelSound(CHANNEL_UI, buildProtocolSoundPath(fileName)); ensure you
handle empty vectors as currently done.

---

Outside diff comments:
In `@src/framework/sound/combinedsoundsource.cpp`:
- Around line 62-108: CombinedSoundSource::setFading currently propagates fading
to child sources but doesn't persist the values to the inherited member
variables; update setFading to assign the incoming values to m_fadeState,
m_fadeTime, m_fadeStartTime, and m_fadeGain before iterating m_sources so the
object's fade state is consistent with other setters (e.g., setLooping, setGain)
and then continue propagating to each child source.

In `@src/framework/sound/soundmanager.h`:
- Around line 45-50: The new uint32_t field maxSoundDistance in struct
ClientItemAmbient is left uninitialized; make it default-initialized to a safe
sentinel (e.g., 0 or other agreed sentinel) in the member declaration so
default-constructed or partially-parsed ClientItemAmbient instances have a
defined value; also update any constructors or aggregate initializations that
might rely on an implicit value to ensure they don't overwrite the default
unintentionally (refer to ClientItemAmbient and maxSoundDistance).

---

Nitpick comments:
In `@modules/client_options/data_options.lua`:
- Around line 810-834: The aux flag in the soundMusic table (inside the action
function) is confusing; rename options.soundMusic.aux to a clear boolean like
options.soundMusic.wasDisabled (or wasEnabled) and simplify the comparison by
computing a local wasDisabled = not options.soundMusic.wasDisabled (or local
wasEnabled = options.soundMusic.wasEnabled) and then use if shouldBeDisabled ~=
wasDisabled then (or if shouldBeDisabled ~= (not options.soundMusic.wasEnabled)
accordingly). Update the assignment that flips the flag (currently
options.soundMusic.aux = not shouldBeDisabled) to set the new descriptive flag
consistently (e.g. options.soundMusic.wasDisabled = shouldBeDisabled or
options.soundMusic.wasEnabled = not shouldBeDisabled) and leave the
removeEvent/scheduleEvent/g_sounds calls unchanged.

In `@modules/corelib/ui/uibutton.lua`:
- Line 7: Remove the hardcoded sound id set by button:setClickSound(2774) and
instead apply the theme/style-driven default: read the click sound from the
button's style/theme (or from a shared constant like DEFAULT_CLICK_SOUND exposed
by the OTUI theme module) and call button:setClickSound(...) with that value;
update any initialization around the UI button creation in uibutton.lua to fall
back to the shared/default value when the style does not specify one so the
click sound can be overridden by theme changes.

In `@modules/corelib/ui/uiminiwindow.lua`:
- Line 7: The miniwindow currently registers the click sound with
miniwindow:addSound(ESoundUI.SoundTypeClick, 2785), which duplicates the
display-sound ID used elsewhere and differs from the base Button click ID 2774;
decide whether miniwindow clicks should use the distinct Button click sound
(2774) or a unique new ID, then update the call to
miniwindow:addSound(ESoundUI.SoundTypeClick, <chosen-id>) accordingly and ensure
any corresponding OTUI/10-windows.otui entries are adjusted to keep click vs
display sounds distinct (reference miniwindow:addSound and
ESoundUI.SoundTypeClick).

In `@src/client/luafunctions.cpp`:
- Around line 1178-1181: The g_sounds singleton binding
(g_lua.bindSingletonFunction("g_sounds", "getDebugSnapshot",
&SoundManager::getDebugSnapshot, &g_sounds)) is placed amid UIMap/UIMinimap
class bindings and should be moved or documented: relocate this bind call to the
block where other singletons (e.g., g_things, g_map) are registered near the top
of the function to keep singleton registrations together, or if there is a
specific reason for its current placement, add a clear comment above the binding
explaining why it must remain here; update references to
SoundManager::getDebugSnapshot and g_sounds accordingly.

In `@src/framework/ui/uiwidget.h`:
- Around line 449-451: Change the sound-type parameters from int to the
UISoundType enum to enforce type safety: update the declarations of addSound(int
soundType, int soundId) and removeSound(int soundType) to addSound(UISoundType
soundType, int soundId) and removeSound(UISoundType soundType), and propagate
the type change through corresponding implementations and all call sites
(including any overloads or tests) to use UISoundType instead of raw ints;
ensure any switch/case or map keys that previously used ints are updated to
accept UISoundType and include necessary header or forward declaration for
UISoundType.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3849e65-2b11-4ebb-bdc2-02b6d4bd755c

📥 Commits

Reviewing files that changed from the base of the PR and between 70d2f6d and 6d256a4.

📒 Files selected for processing (45)
  • data/styles/10-buttons.otui
  • data/styles/10-windows.otui
  • modules/client/client.lua
  • modules/client_options/data_options.lua
  • modules/client_options/options.lua
  • modules/client_options/styles/sound/audio.otui
  • modules/client_options/styles/sound/battleSounds.otui
  • modules/client_options/styles/sound/sound.otui
  • modules/client_options/styles/sound/uiSounds.otui
  • modules/corelib/const.lua
  • modules/corelib/ui/uibutton.lua
  • modules/corelib/ui/uiminiwindow.lua
  • modules/game_interface/interface.otmod
  • modules/game_screenshot/game_screenshot.lua
  • modules/game_soundDebug/sounddebug.lua
  • modules/game_soundDebug/sounddebug.otmod
  • modules/game_soundDebug/sounddebug.otui
  • modules/game_things/things.lua
  • src/CMakeLists.txt
  • src/client/creature.cpp
  • src/client/game.cpp
  • src/client/luafunctions.cpp
  • src/client/luavaluecasts_client.cpp
  • src/client/luavaluecasts_client.h
  • src/client/map.cpp
  • src/client/protocolgameparse.cpp
  • src/client/uimap.cpp
  • src/client/uimap.h
  • src/framework/luafunctions.cpp
  • src/framework/sound/combinedsoundsource.cpp
  • src/framework/sound/soundchannel.cpp
  • src/framework/sound/soundmanager.cpp
  • src/framework/sound/soundmanager.h
  • src/framework/sound/soundmanager_debug.cpp
  • src/framework/sound/soundmanager_itemambience.cpp
  • src/framework/sound/soundmanager_protocol.cpp
  • src/framework/sound/soundmanager_types.h
  • src/framework/sound/soundsource.cpp
  • src/framework/sound/soundsource.h
  • src/framework/sound/streamsoundsource.cpp
  • src/framework/sound/streamsoundsource.h
  • src/framework/ui/uiwidget.cpp
  • src/framework/ui/uiwidget.h
  • src/framework/ui/uiwidgetbasestyle.cpp
  • vc18/otclient.vcxproj
💤 Files with no reviewable changes (1)
  • modules/client_options/styles/sound/audio.otui

Comment on lines +767 to +808
soundMaster = {
value = 100,
aux = true,
action = function(value, options, controller, panels, extraWidgets)
if not g_sounds then
return
end
local soundMasterWidget = panels.soundPanel:recursiveGetChildById('soundMaster')
soundMasterWidget:setText(string.format('Master Volume: %d %%', value))
for channelName, channelId in pairs(SoundChannels) do
g_sounds.getChannel(channelId):setGain(value / 100)
end
local shouldDisable = value <= 1
local hasChanged = shouldDisable ~= (options.soundMaster.aux or false)
if not hasChanged then
return
end
options.soundMaster.aux = shouldDisable
if shouldDisable then
g_sounds.setAudioEnabled(false)
extraWidgets.audioButton:setIcon('/images/topbuttons/button_mute_pressed')
else
g_sounds.setAudioEnabled(true)
extraWidgets.audioButton:setIcon('/images/topbuttons/button_mute_up')
end
local function togglePanel(panel)
if not panel then
return
end
local infoPanel = panel:recursiveGetChildById('info')
local children = panel:getChildren()
for _, widget in ipairs(children) do
if widget:getStyle().__class ~= "UILabel" then
widget:setEnabled(not shouldDisable)
end
end
infoPanel:setVisible(shouldDisable)
infoPanel:setHeight(shouldDisable and 30 or 0)
end
togglePanel(panels.battleSoundsPanel)
togglePanel(panels.iuSoundPanel)
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's any other place that implements master volume as a multiplier
rg -n -C3 'soundMaster|masterVolume|setGain' --type=lua --iglob '*option*'

Repository: opentibiabr/otclient

Length of output: 5540


🏁 Script executed:

#!/bin/bash
# Get the full implementation of individual sound channels
rg -n -A 15 'soundMusic\s*=|soundAmbience\s*=|soundItems\s*=|soundEventVolume\s*=' modules/client_options/data_options.lua | head -100

Repository: opentibiabr/otclient

Length of output: 2291


🏁 Script executed:

#!/bin/bash
# Search for g_sounds definition and setGain implementation
rg -n -A 10 'function.*setGain|def.*setGain' --type=lua

Repository: opentibiabr/otclient

Length of output: 896


🏁 Script executed:

#!/bin/bash
# Search for g_sounds initialization and how it handles volume
rg -n -B 5 -A 10 'g_sounds\s*=' --type=lua | head -80

Repository: opentibiabr/otclient

Length of output: 671


🏁 Script executed:

#!/bin/bash
# Look for comments around the soundMaster definition
sed -n '750,810p' modules/client_options/data_options.lua | cat -n

Repository: opentibiabr/otclient

Length of output: 2864


🏁 Script executed:

#!/bin/bash
# Look for README or docs mentioning audio or sound
find . -type f \( -name "README*" -o -name "*.md" \) | xargs grep -l -i "audio\|sound\|volume" 2>/dev/null | head -5

Repository: opentibiabr/otclient

Length of output: 99


🏁 Script executed:

#!/bin/bash
# Check both README files for audio/sound information
echo "=== README.md ===" && grep -n -i -B 2 -A 5 "audio\|sound\|volume" README.md && echo -e "\n=== data/sounds/README.md ===" && cat data/sounds/README.md

Repository: opentibiabr/otclient

Length of output: 3141


🏁 Script executed:

#!/bin/bash
# Look for how options are loaded/applied
rg -n -B 3 -A 8 'onLoad|loadOption|applyOption' modules/client_options/data_options.lua | head -50

Repository: opentibiabr/otclient

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Look at the complete soundMaster action to see if it triggers updates to individual channels
sed -n '767,808p' modules/client_options/data_options.lua | cat -n

Repository: opentibiabr/otclient

Length of output: 2199


Master volume overwrites individual channel gains—needs multiplicative behavior or clarifying comment.

Lines 776-778 set all channel gains to the master volume value directly:

for channelName, channelId in pairs(SoundChannels) do
    g_sounds.getChannel(channelId):setGain(value / 100)
end

The individual channel sliders (soundMusic, soundAmbience, soundItems, soundEventVolume) each set their own gain independently without considering the master volume, creating a "last-write-wins" behavior. For example: set Master to 50%, then adjust Music to 100%—the Music channel ends up at 100%, not 50% of 100%.

Standard audio systems multiply master with individual channel volumes. Either implement multiplicative behavior (individual gain × master gain) or add a comment explaining why this simpler approach is intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/client_options/data_options.lua` around lines 767 - 808, The master
volume handler (soundMaster.action) currently writes channel gains directly via
g_sounds.getChannel(...):setGain(value / 100), which overwrites per-channel
sliders (soundMusic, soundAmbience, soundItems, soundEventVolume) leading to
last-write-wins; change to multiplicative behavior by applying effectiveGain =
(masterValue/100) * (channelOptionValue/100) for each channel instead of just
masterValue/100, and add/update a small helper (e.g., applyChannelGain or use
the existing channel setters) so both soundMaster.action and each individual
channel setter (soundMusic, soundAmbience, soundItems, soundEventVolume) compute
and set g_sounds.getChannel(channelId):setGain(master * channel) so updates from
either control preserve the multiplicative relationship.

Comment on lines +32 to +55
OptionCheckBox
id: battleSoundOwnBattlesubChannelsSpells
!text: tr('Spells')
anchors.top: prev.bottom
margin-top:5

MiniWindowContents
id: panelOwnBattleSubChannels
padding-left: 10
padding-right: 5
padding-top: 10
layout: verticalBox
QtCheckBox
id: battleSoundOwnBattleSubChannelsAttack
!text: tr('Attack')
@onCheckChange: modules.client_options.setOption(self:getId(), self:isChecked())
QtCheckBox
id: battleSoundOwnBattleSoundSubChannelsHealing
!text: tr('Healing')
@onCheckChange: modules.client_options.setOption(self:getId(), self:isChecked())
QtCheckBox
id: battleSoundOwnBattleSoundSubChannelsSupport
!text: tr('Support')
@onCheckChange: modules.client_options.setOption(self:getId(), self:isChecked())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent ID naming may cause bugs.

The widget IDs have inconsistent naming patterns:

  • Line 33: battleSoundOwnBattlesubChannelsSpells (lowercase 's' in "subChannels")
  • Line 45: battleSoundOwnBattleSubChannelsAttack (uppercase 'S')
  • Line 49: battleSoundOwnBattleSoundSubChannelsHealing (extra "Sound" word)
  • Line 53: battleSoundOwnBattleSoundSubChannelsSupport (extra "Sound" word)

This inconsistency could cause issues when accessing these widgets from Lua code or when the options system tries to save/load values by ID.

✏️ Proposed fix for consistent naming
     OptionCheckBox
-      id: battleSoundOwnBattlesubChannelsSpells
+      id: battleSoundOwnBattleSubChannelsSpells
       !text: tr('Spells')
       anchors.top: prev.bottom
       margin-top:5
...
       QtCheckBox
         id: battleSoundOwnBattleSubChannelsAttack
         !text: tr('Attack')
         `@onCheckChange`: modules.client_options.setOption(self:getId(), self:isChecked())
       QtCheckBox
-        id: battleSoundOwnBattleSoundSubChannelsHealing
+        id: battleSoundOwnBattleSubChannelsHealing
         !text: tr('Healing')
         `@onCheckChange`: modules.client_options.setOption(self:getId(), self:isChecked())
       QtCheckBox
-        id: battleSoundOwnBattleSoundSubChannelsSupport
+        id: battleSoundOwnBattleSubChannelsSupport
         !text: tr('Support')
         `@onCheckChange`: modules.client_options.setOption(self:getId(), self:isChecked())

     OptionCheckBox
-      id: battleSoundOwnBattleSoundSubChannelsWeapons
+      id: battleSoundOwnBattleSubChannelsWeapons
       anchors.top: prev.bottom
       !text: tr('Weapons')
       margin-top: -10
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/client_options/styles/sound/battleSounds.otui` around lines 32 - 55,
Widget IDs for the battle sound sub-channel controls are inconsistent (e.g.,
battleSoundOwnBattlesubChannelsSpells, battleSoundOwnBattleSubChannelsAttack,
battleSoundOwnBattleSoundSubChannelsHealing/Support) which will break lookups;
rename IDs to a single consistent pattern (for example:
battleSoundOwnBattleSubChannelsSpells, battleSoundOwnBattleSubChannelsAttack,
battleSoundOwnBattleSubChannelsHealing, battleSoundOwnBattleSubChannelsSupport)
and update any places that reference the old IDs (including the QtCheckBox
`@onCheckChange` callbacks and any Lua code in modules.client_options that
reads/writes options via setOption/getId). Ensure you remove the duplicated
"Sound" and fix the lowercase "s" in "subChannels" so all IDs match exactly.

Comment on lines +15 to +17
&minimumScrollValue: 1
&maximumScrollValue: 100
&scrollSize: 21
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Allow the new volume sliders to reach 0%.

All five sliders now clamp to 1-100, so the new panel can no longer mute an individual channel or faithfully represent a persisted 0 volume value.

🎚️ Suggested fix
-      &minimumScrollValue: 1
+      &minimumScrollValue: 0
...
-      &minimumScrollValue: 1
+      &minimumScrollValue: 0
...
-      &minimumScrollValue: 1
+      &minimumScrollValue: 0
...
-      &minimumScrollValue: 1
+      &minimumScrollValue: 0
...
-      &minimumScrollValue: 1
+      &minimumScrollValue: 0

Also applies to: 35-37, 61-63, 82-84, 113-115

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/client_options/styles/sound/sound.otui` around lines 15 - 17, The
sliders are incorrectly clamped to 1–100; update each slider block to allow 0 by
changing &minimumScrollValue from 1 to 0 (e.g., update the &minimumScrollValue
in the blocks containing &minimumScrollValue/&maximumScrollValue/&scrollSize at
the shown occurrences — lines 15-17, 35-37, 61-63, 82-84, 113-115) so the range
becomes 0–100 and the panel can represent a true muted (0) volume.

Comment on lines +66 to +68
OptionCheckBox
id: soundNotificationUIInteractions
!text: tr('UI Interactions')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Rename this option to the notification subchannel key.

id: soundNotificationUIInteractions breaks the soundNotificationsubChannels* naming used by the rest of this section. As written, the "UI Interactions" notification toggle is unlikely to round-trip with the stored notification setting or hit the expected subchannel gate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/client_options/styles/sound/uiSounds.otui` around lines 66 - 68, The
option id "soundNotificationUIInteractions" must be renamed to follow the
existing soundNotificationsubChannels* pattern so it round-trips with stored
settings; change the id to "soundNotificationsubChannelsUIInteractions" (or the
exact existing subchannel key used elsewhere) in the OptionCheckBox definition
and update any references that read/write subchannel keys (e.g., code handling
soundNotificationsubChannels*) so the UI toggle maps to the correct stored
subchannel.

Comment on lines +108 to +110
id: soundNotificationsubChannelsSystemAnnouncements
!text: tr('System Announcements')
!tooltip: tr('Check this box to see how much time or how many charges are left \non your equipped items')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tooltip text is copied from an unrelated setting.

This tooltip describes equipped-item timers, not system-announcement sounds, so it will send users to the wrong feature.

📝 Suggested text
-        !tooltip: tr('Check this box to see how much time or how many charges are left \non your equipped items')
+        !tooltip: tr('Play a sound when a system announcement is received.')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
id: soundNotificationsubChannelsSystemAnnouncements
!text: tr('System Announcements')
!tooltip: tr('Check this box to see how much time or how many charges are left \non your equipped items')
id: soundNotificationsubChannelsSystemAnnouncements
!text: tr('System Announcements')
!tooltip: tr('Play a sound when a system announcement is received.')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/client_options/styles/sound/uiSounds.otui` around lines 108 - 110,
The tooltip for the setting identified by id
"soundNotificationsubChannelsSystemAnnouncements" is incorrect (it references
equipped-item timers); update the !tooltip value to a relevant, localized string
via tr(), e.g. tr('Play a sound when system announcements are posted') or
similar, so that the tooltip matches the feature (system announcement sound
notifications) and replace the existing incorrect text in the UI resource for
soundNotificationsubChannelsSystemAnnouncements.

Comment on lines +188 to +195
local function resetSummaries()
headerLabel:setText('Sound debug is idle.')
mixSummaryLabel:setText('Waiting for active sources.')
sourceSummaryLabel:setText('No sources.')
itemSummaryLabel:setText('No ambient items active.')
eventSummaryLabel:setText('No positional sound events yet.')
spatialSummaryLabel:setText('Map and radar markers follow positional audio.')
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard clearVisuals() until the window is initialized.

init() wires this function to g_game.onGameEnd, but these labels are only assigned in setupWidgets(). Logging out before ever opening the debug window will call setText() on nil here.

🛡️ Proposed fix
 local function clearVisuals()
+  if not widgetsInitialized then
+    return
+  end
+
   for _, state in pairs(meterStates) do
     state.value = 0
     state.peak = 0
     state.base = 0
   end

Also applies to: 222-237, 487-490

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/game_soundDebug/sounddebug.lua` around lines 188 - 195, The
resetSummaries()/clearVisuals() functions call setText() on UI label objects
that may be nil if setupWidgets() hasn't run; guard these calls by first
checking the window/widget initialization (e.g., if headerLabel and
mixSummaryLabel and sourceSummaryLabel etc. are non-nil) or add an isInitialized
flag set in setupWidgets(), and only call setText() when initialized; update
resetSummaries(), clearVisuals(), and the other similar blocks (the other
summary-reset functions mentioned) to perform these nil checks or gate the
entire function behind the isInitialized boolean before mutating any labels.

Comment on lines +162 to +177
auto point = m_mapView->transformPositionTo2D(pos) - m_mapView->m_posInfo.drawOffset;
point.x *= m_mapView->m_posInfo.horizontalStretchFactor;
point.y *= m_mapView->m_posInfo.verticalStretchFactor;
point += m_mapviewRect.topLeft();
return point;
}

Rect UIMap::getTileRect(const Position& pos)
{
if (!pos.isMapPosition() || !m_mapView || !m_mapView->isInRange(pos))
return {};

const auto point = getTilePoint(pos);
const auto width = std::max<int>(1, std::lround(static_cast<float>(m_mapView->m_tileSize) * m_mapView->m_posInfo.horizontalStretchFactor));
const auto height = std::max<int>(1, std::lround(static_cast<float>(m_mapView->m_tileSize) * m_mapView->m_posInfo.verticalStretchFactor));
return { point.x, point.y, width, height };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Return logical UI coordinates from these helpers.

getTilePoint()/getTileRect() add m_mapviewRect, which is already multiplied by g_window.getDisplayDensity(). That makes this new Lua-facing API report physical pixels instead of widget/UI pixels, so overlays anchored with these values will be offset on HiDPI displays and won't round-trip cleanly with the rest of UIMap's point-based API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/uimap.cpp` around lines 162 - 177, getTilePoint and getTileRect
currently return physical pixels because they add m_mapviewRect (already scaled
by g_window.getDisplayDensity()); change them to return logical/UI coordinates
by removing that added physical offset and instead account for display density:
in getTilePoint (and any use of transformPositionTo2D) stop adding
m_mapviewRect.topLeft() directly (or divide that topLeft by
g_window.getDisplayDensity()), so the returned point is in UI/widget
coordinates, and in getTileRect compute width/height in logical pixels (divide
m_mapView->m_tileSize * horizontal/verticalStretchFactor by
g_window.getDisplayDensity() before rounding and clamping). Update references to
m_mapviewRect.topLeft(), getTilePoint, and getTileRect accordingly so the
Lua-facing API returns logical UI coords consistent with the rest of UIMap.

Comment on lines +110 to +113
m_protocolSoundSettings.ownAttack = getBooleanSetting("battleSoundOwnBattleSubChannelsAttack", true);
m_protocolSoundSettings.ownHealing = getBooleanSetting("battleSoundOwnBattleSoundSubChannelsHealing", true);
m_protocolSoundSettings.ownSupport = getBooleanSetting("battleSoundOwnBattleSoundSubChannelsSupport", true);
m_protocolSoundSettings.ownWeapons = getBooleanSetting("battleSoundOwnBattleSoundSubChannelsWeapons", true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

These option keys look misspelled.

battleSoundOwnBattleSoundSubChannels{Healing,Support,Weapons} has an extra Sound compared with ...OwnBattleSubChannelsAttack and the other groups. Unless the options layer uses the same typo, those three toggles will always fall back to the default value.

🔧 Proposed fix
-    m_protocolSoundSettings.ownHealing = getBooleanSetting("battleSoundOwnBattleSoundSubChannelsHealing", true);
-    m_protocolSoundSettings.ownSupport = getBooleanSetting("battleSoundOwnBattleSoundSubChannelsSupport", true);
-    m_protocolSoundSettings.ownWeapons = getBooleanSetting("battleSoundOwnBattleSoundSubChannelsWeapons", true);
+    m_protocolSoundSettings.ownHealing = getBooleanSetting("battleSoundOwnBattleSubChannelsHealing", true);
+    m_protocolSoundSettings.ownSupport = getBooleanSetting("battleSoundOwnBattleSubChannelsSupport", true);
+    m_protocolSoundSettings.ownWeapons = getBooleanSetting("battleSoundOwnBattleSubChannelsWeapons", true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
m_protocolSoundSettings.ownAttack = getBooleanSetting("battleSoundOwnBattleSubChannelsAttack", true);
m_protocolSoundSettings.ownHealing = getBooleanSetting("battleSoundOwnBattleSoundSubChannelsHealing", true);
m_protocolSoundSettings.ownSupport = getBooleanSetting("battleSoundOwnBattleSoundSubChannelsSupport", true);
m_protocolSoundSettings.ownWeapons = getBooleanSetting("battleSoundOwnBattleSoundSubChannelsWeapons", true);
m_protocolSoundSettings.ownAttack = getBooleanSetting("battleSoundOwnBattleSubChannelsAttack", true);
m_protocolSoundSettings.ownHealing = getBooleanSetting("battleSoundOwnBattleSubChannelsHealing", true);
m_protocolSoundSettings.ownSupport = getBooleanSetting("battleSoundOwnBattleSubChannelsSupport", true);
m_protocolSoundSettings.ownWeapons = getBooleanSetting("battleSoundOwnBattleSubChannelsWeapons", true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/sound/soundmanager_protocol.cpp` around lines 110 - 113, The
option keys used when initializing m_protocolSoundSettings are inconsistent:
m_protocolSoundSettings.ownHealing, .ownSupport and .ownWeapons use
"battleSoundOwnBattleSoundSubChannels{Healing,Support,Weapons}" which includes
an extra "Sound" and will miss the intended setting; update these keys to match
the correct pattern "battleSoundOwnBattleSubChannelsHealing",
"battleSoundOwnBattleSubChannelsSupport" and
"battleSoundOwnBattleSubChannelsWeapons" (the same pattern used by ownAttack) in
the soundmanager_protocol initialization, and scan for other occurrences of the
misspelled keys to correct them as well.

Comment on lines +172 to +186
bool SoundManager::shouldPlayProtocolSound(const uint8_t soundSource, const uint16_t soundEffectId)
{
if (!isAudioEnabled())
return false;

const auto volume = getProtocolVolumeSetting(soundSource);
if (volume <= 1)
return false;

if (soundSource >= 1) {
return isProtocolSubChannelEnabled(soundSource, getSoundEffectType(soundEffectId));
}

return soundSource == 0;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Apply the configured category volume to the gain.

The own/others/creature volume settings are only used as an on/off gate today. Once playback reaches this helper, gain is derived only from distance, so 10% and 100% sound identical, and the debug gain you record later is overstated too.

🔊 Suggested direction
-bool SoundManager::playProtocolAudioFileId(const uint32_t audioFileId, const Position& pos, const int channelId)
+bool SoundManager::playProtocolAudioFileId(const uint32_t audioFileId, const Position& pos, const int channelId, const float volumeScale)
 {
     const auto& localPlayer = g_game.getLocalPlayer();
     if (!localPlayer || !pos.isMapPosition())
         return false;
@@
-    const float gain = 1.0f - (distance / PROTOCOL_MAX_DISTANCE);
+    const float gain = (1.0f - (distance / PROTOCOL_MAX_DISTANCE)) * volumeScale;
     const float stereoBalance = std::clamp(static_cast<float>(pos.x - playerPos.x) / PROTOCOL_MAX_DISTANCE, -1.0f, 1.0f);
@@
 }

Then pass std::clamp(getProtocolVolumeSetting(soundSource), 0, 100) / 100.0f from the call sites and use that same scaled gain for recordProtocolDebugEvent() / logProtocolDebug().

Also applies to: 230-257

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/sound/soundmanager_protocol.cpp` around lines 172 - 186, The
current per-category volume (getProtocolVolumeSetting) is used only as an on/off
gate in SoundManager::shouldPlayProtocolSound so gain later ignores category
volume; change callers to compute a scaledCategoryVolume =
std::clamp(getProtocolVolumeSetting(soundSource), 0, 100) / 100.0f and pass that
scaled float into the playback/gain calculation instead of assuming full volume,
then use that same scaledCategoryVolume when calling recordProtocolDebugEvent()
/ logProtocolDebug() so the debug gain reflects category attenuation; keep
existing gating via
shouldPlayProtocolSound/isProtocolSubChannelEnabled/getSoundEffectType but plumb
the scaledCategoryVolume through the playback path and apply it multiplicatively
to the distance-derived gain.

Comment on lines +290 to +306
void SoundManager::playUiSoundById(const int soundId)
{
if (soundId <= 0)
return;

std::string fileName = getAudioFileNameById(soundId);
if (fileName.empty()) {
const auto soundIds = getRandomSoundIds(soundId);
if (!soundIds.empty())
fileName = getAudioFileNameById(soundIds.front());
}

if (fileName.empty())
return;

playChannelSound(CHANNEL_UI, buildProtocolSoundPath(fileName));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Random sound selection not implemented — always picks first element.

getRandomSoundIds() returns a list of sound IDs intended for random selection, but this function only uses .front(). This defeats the "random sound effect" feature described in the protobuf schema.

🐛 Proposed fix to randomize sound selection
 void SoundManager::playUiSoundById(const int soundId)
 {
     if (soundId <= 0)
         return;

     std::string fileName = getAudioFileNameById(soundId);
     if (fileName.empty()) {
         const auto soundIds = getRandomSoundIds(soundId);
-        if (!soundIds.empty())
-            fileName = getAudioFileNameById(soundIds.front());
+        if (!soundIds.empty()) {
+            const auto randomIndex = std::rand() % soundIds.size();
+            fileName = getAudioFileNameById(soundIds[randomIndex]);
+        }
     }

     if (fileName.empty())
         return;

     playChannelSound(CHANNEL_UI, buildProtocolSoundPath(fileName));
 }

Note: Consider using a proper random engine (e.g., std::mt19937 with std::uniform_int_distribution) for better randomness if this is called frequently.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void SoundManager::playUiSoundById(const int soundId)
{
if (soundId <= 0)
return;
std::string fileName = getAudioFileNameById(soundId);
if (fileName.empty()) {
const auto soundIds = getRandomSoundIds(soundId);
if (!soundIds.empty())
fileName = getAudioFileNameById(soundIds.front());
}
if (fileName.empty())
return;
playChannelSound(CHANNEL_UI, buildProtocolSoundPath(fileName));
}
void SoundManager::playUiSoundById(const int soundId)
{
if (soundId <= 0)
return;
std::string fileName = getAudioFileNameById(soundId);
if (fileName.empty()) {
const auto soundIds = getRandomSoundIds(soundId);
if (!soundIds.empty()) {
const auto randomIndex = std::rand() % soundIds.size();
fileName = getAudioFileNameById(soundIds[randomIndex]);
}
}
if (fileName.empty())
return;
playChannelSound(CHANNEL_UI, buildProtocolSoundPath(fileName));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/framework/sound/soundmanager.cpp` around lines 290 - 306, playUiSoundById
currently ignores randomness by always using the first ID from
getRandomSoundIds; update playUiSoundById to select a random element from the
vector returned by getRandomSoundIds instead of using .front(). Use a proper
random engine (e.g., std::mt19937 seeded once) and std::uniform_int_distribution
to pick an index into the soundIds vector, then call
getAudioFileNameById(selectedId) and proceed to playChannelSound(CHANNEL_UI,
buildProtocolSoundPath(fileName)); ensure you handle empty vectors as currently
done.

@kokekanon kokekanon marked this pull request as draft March 29, 2026 00:44
@kokekanon
Copy link
Copy Markdown
Contributor Author

kokekanon commented Mar 29, 2026

Honestly, I’m not sure if it’s a good idea to merge this or not. I don’t know what the impact of this PR on performance would be (I’m a noob in C++ and don’t know how to use a profiler). I’m just leaving it here for reference in case someone might want it.

Obviously, the module, the soundmanager_debug.cpp, and the uimap are used to verify that everything is working properly.

@gabrielew
Copy link
Copy Markdown
Contributor

and don’t know how to use a profiler). I’m just leaving it here for reference in case someone might

@dudantas can u help us?

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.

2 participants