From ac12ed0bdb6586273f47e08728e2d03855def31e Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Sat, 29 Aug 2026 01:15:47 -0500 Subject: [PATCH 1/4] Add cross-platform notification audio cues --- example_community_patch_settings_da.toml | 8 + example_community_patch_settings_de.toml | 8 + ...munity_patch_settings_en-GB-x-cockney.toml | 8 + ...mmunity_patch_settings_en-x-minionese.toml | 8 + example_community_patch_settings_en.toml | 8 + example_community_patch_settings_es.toml | 8 + example_community_patch_settings_fr.toml | 8 + example_community_patch_settings_nl.toml | 8 + example_community_patch_settings_ru.toml | 8 + example_community_patch_settings_tlh.toml | 8 + mods/src/config.cc | 28 +++ mods/src/config.h | 7 + mods/src/defaultconfig.h | 5 + mods/src/notification_audio_mac.mm | 27 ++ mods/src/patches/notification_audio.cc | 236 ++++++++++++++++++ mods/src/patches/notification_audio.h | 23 ++ .../src/patches/notification_audio_platform.h | 6 + mods/src/patches/notification_service.cc | 26 +- mods/xmake.lua | 1 + 19 files changed, 436 insertions(+), 3 deletions(-) create mode 100644 mods/src/notification_audio_mac.mm create mode 100644 mods/src/patches/notification_audio.cc create mode 100644 mods/src/patches/notification_audio.h create mode 100644 mods/src/patches/notification_audio_platform.h diff --git a/example_community_patch_settings_da.toml b/example_community_patch_settings_da.toml index 4e4003d2f..ff8585a4e 100644 --- a/example_community_patch_settings_da.toml +++ b/example_community_patch_settings_da.toml @@ -37,6 +37,14 @@ disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + # <[======================================================================================================]> # # ***** * * * diff --git a/example_community_patch_settings_de.toml b/example_community_patch_settings_de.toml index 2dda59892..2a8fc320b 100644 --- a/example_community_patch_settings_de.toml +++ b/example_community_patch_settings_de.toml @@ -37,6 +37,14 @@ disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + # <[=======================================]> # # **** ** ** diff --git a/example_community_patch_settings_en-GB-x-cockney.toml b/example_community_patch_settings_en-GB-x-cockney.toml index 0ccf711ca..e16874cd2 100644 --- a/example_community_patch_settings_en-GB-x-cockney.toml +++ b/example_community_patch_settings_en-GB-x-cockney.toml @@ -37,6 +37,14 @@ disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + # <[=======================================]> # # **** ** ** diff --git a/example_community_patch_settings_en-x-minionese.toml b/example_community_patch_settings_en-x-minionese.toml index 9f1e1e3a6..20d316196 100644 --- a/example_community_patch_settings_en-x-minionese.toml +++ b/example_community_patch_settings_en-x-minionese.toml @@ -37,6 +37,14 @@ disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + # <[=======================================]> # # **** ** ** diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 9ffe927fe..22eeb26c2 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -37,6 +37,14 @@ disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + # <[=======================================]> # # **** ** ** diff --git a/example_community_patch_settings_es.toml b/example_community_patch_settings_es.toml index b89c9a2e8..6ea0664d8 100644 --- a/example_community_patch_settings_es.toml +++ b/example_community_patch_settings_es.toml @@ -37,6 +37,14 @@ disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + # <[=====================================================]> # # * * * diff --git a/example_community_patch_settings_fr.toml b/example_community_patch_settings_fr.toml index fdbc5f79d..66441cb22 100644 --- a/example_community_patch_settings_fr.toml +++ b/example_community_patch_settings_fr.toml @@ -37,6 +37,14 @@ disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + # <[=======================================]> # # **** ** ** diff --git a/example_community_patch_settings_nl.toml b/example_community_patch_settings_nl.toml index b2b39b208..00361a2ae 100644 --- a/example_community_patch_settings_nl.toml +++ b/example_community_patch_settings_nl.toml @@ -37,6 +37,14 @@ disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + # <[=======================================]> # # **** ** ** diff --git a/example_community_patch_settings_ru.toml b/example_community_patch_settings_ru.toml index ad3db036a..120e2c2a1 100644 --- a/example_community_patch_settings_ru.toml +++ b/example_community_patch_settings_ru.toml @@ -37,6 +37,14 @@ disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + # <[============================================================]> # # diff --git a/example_community_patch_settings_tlh.toml b/example_community_patch_settings_tlh.toml index b9bce7597..fbe4b4606 100644 --- a/example_community_patch_settings_tlh.toml +++ b/example_community_patch_settings_tlh.toml @@ -37,6 +37,14 @@ disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + # <[=====================================================]> # # * * **** * * diff --git a/mods/src/config.cc b/mods/src/config.cc index 95deab836..e11aaca62 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -328,6 +328,25 @@ T get_config_or_default(toml::table& config, toml::table& new_config, std::strin return (T)final_value; } +NotificationSound get_notification_sound(toml::table& config, toml::table& new_config, std::string_view item, + std::string_view default_value, bool write_log) +{ + const auto value = get_config_or_default(config, new_config, "audio", item, + std::string(default_value), false); + auto sound = notification_sound_from_name(StripAsciiWhitespace(value)); + if (!sound.has_value()) { + spdlog::warn("invalid config value audio.{}: '{}'; using {}", item, value, default_value); + sound = notification_sound_from_name(default_value); + } + + const auto result = sound.value_or(NotificationSound::None); + new_config["audio"].as_table()->insert_or_assign(item, notification_sound_name(result)); + if (write_log) { + spdlog::debug("config value audio.{} value: {}", item, notification_sound_name(result)); + } + return result; +} + std::string_view to_string(MissionHudVisibility visibility) { switch (visibility) { @@ -951,6 +970,15 @@ void Config::Load() } } this->installAudioEventHooks = this->trace_audio_events || !this->disabled_audio_events.empty(); + this->alert_victory = + get_notification_sound(config, parsed, "alert_victory", DCA::alert_victory, write_config); + this->alert_defeat = get_notification_sound(config, parsed, "alert_defeat", DCA::alert_defeat, write_config); + this->alert_armada_created = + get_notification_sound(config, parsed, "alert_armada_created", DCA::alert_armada_created, write_config); + this->alert_armada_battle_won = + get_notification_sound(config, parsed, "alert_armada_battle_won", DCA::alert_armada_battle_won, write_config); + this->alert_armada_battle_lost = + get_notification_sound(config, parsed, "alert_armada_battle_lost", DCA::alert_armada_battle_lost, write_config); this->auto_open_bulk_claim_flyout = get_config_or_default(config, parsed, "ui", "auto_open_bulk_claim_flyout", DCU::auto_open_bulk_claim_flyout, write_config); diff --git a/mods/src/config.h b/mods/src/config.h index 43f5c3913..93264d756 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -8,6 +8,8 @@ #include +#include "patches/notification_audio.h" + #if _WIN32 #include #endif @@ -196,6 +198,11 @@ class Config final bool disable_toast_banners; bool trace_audio_events; std::vector disabled_audio_events; + NotificationSound alert_victory; + NotificationSound alert_defeat; + NotificationSound alert_armada_created; + NotificationSound alert_armada_battle_won; + NotificationSound alert_armada_battle_lost; bool auto_open_bulk_claim_flyout; bool auto_confirm_ft_upgrade; diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index b22cad5b8..efe3530e2 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -11,6 +11,11 @@ namespace Audio { constexpr const char* disabled_events = ""; constexpr bool trace_events = false; + constexpr const char* alert_victory = "none"; + constexpr const char* alert_defeat = "none"; + constexpr const char* alert_armada_created = "none"; + constexpr const char* alert_armada_battle_won = "none"; + constexpr const char* alert_armada_battle_lost = "none"; } // namespace Audio namespace SystemConfig diff --git a/mods/src/notification_audio_mac.mm b/mods/src/notification_audio_mac.mm new file mode 100644 index 000000000..cae47a9eb --- /dev/null +++ b/mods/src/notification_audio_mac.mm @@ -0,0 +1,27 @@ +#ifdef __APPLE__ + +#include "patches/notification_audio_platform.h" + +#import + +bool notification_audio_platform_play(const uint8_t* data, size_t size) +{ + @autoreleasepool { + static NSSound* current_sound = nil; + + NSData* sound_data = [NSData dataWithBytesNoCopy:const_cast(data) length:size freeWhenDone:NO]; + NSSound* sound = [[NSSound alloc] initWithData:sound_data]; + if (!sound) return false; + + [current_sound stop]; +#if __has_feature(objc_arc) + current_sound = sound; +#else + [current_sound release]; + current_sound = sound; +#endif + return [current_sound play]; + } +} + +#endif diff --git a/mods/src/patches/notification_audio.cc b/mods/src/patches/notification_audio.cc new file mode 100644 index 000000000..ebdd87c3c --- /dev/null +++ b/mods/src/patches/notification_audio.cc @@ -0,0 +1,236 @@ +#include "patches/notification_audio.h" + +#include "patches/notification_audio_platform.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if _WIN32 +#include +#include +#endif + +namespace +{ +struct ToneSegment { + double frequency_hz; + int duration_ms; +}; + +constexpr int kSampleRate = 44100; +constexpr double kTwoPi = 6.28318530717958647692; +constexpr double kAmplitude = 0.26; + +constexpr std::array kDefaultPattern{{{740.0, 70}, {0.0, 22}, {880.0, 85}, {0.0, 18}}}; +constexpr std::array kInfoPattern{{{659.0, 80}, {0.0, 24}, {880.0, 110}}}; +constexpr std::array kSuccessPattern{{{587.0, 70}, {0.0, 18}, {740.0, 70}, {0.0, 18}, {988.0, 120}}}; +constexpr std::array kWarningPattern{{{622.0, 90}, {0.0, 36}, {466.0, 110}, {0.0, 28}, {466.0, 90}}}; +constexpr std::array kAlarmPattern{{{880.0, 90}, {0.0, 42}, {880.0, 90}, {0.0, 42}, {698.0, 160}}}; +constexpr std::array kArrivalPattern{{{523.0, 65}, {0.0, 18}, {659.0, 70}, {0.0, 18}, {1046.0, 125}}}; +constexpr std::array kSoftPattern{{{523.0, 90}, {0.0, 26}, {659.0, 110}}}; +constexpr std::array kPingPattern{{{1046.0, 95}}}; +constexpr std::array kRepairPattern{{{440.0, 70}, {0.0, 18}, {554.0, 70}, {0.0, 18}, {740.0, 140}}}; + +std::once_flag s_sound_buffers_once; +std::array, static_cast(NotificationSound::Count)> s_sound_buffers; + +std::string normalize_sound_name(std::string_view value) +{ + std::string normalized; + normalized.reserve(value.size()); + for (const auto ch : value) { + if (ch >= 'A' && ch <= 'Z') { + normalized.push_back(static_cast(ch - 'A' + 'a')); + } else if (ch == '-' || ch == ' ') { + normalized.push_back('_'); + } else { + normalized.push_back(ch); + } + } + return normalized; +} + +std::span sound_pattern(NotificationSound sound) +{ + switch (sound) { + case NotificationSound::Default: + return kDefaultPattern; + case NotificationSound::Info: + return kInfoPattern; + case NotificationSound::Success: + return kSuccessPattern; + case NotificationSound::Warning: + return kWarningPattern; + case NotificationSound::Alarm: + return kAlarmPattern; + case NotificationSound::Arrival: + return kArrivalPattern; + case NotificationSound::Soft: + return kSoftPattern; + case NotificationSound::Ping: + return kPingPattern; + case NotificationSound::Repair: + return kRepairPattern; + default: + return {}; + } +} + +void append_u16(std::vector& buffer, uint16_t value) +{ + buffer.push_back(static_cast(value & 0xff)); + buffer.push_back(static_cast((value >> 8) & 0xff)); +} + +void append_u32(std::vector& buffer, uint32_t value) +{ + buffer.push_back(static_cast(value & 0xff)); + buffer.push_back(static_cast((value >> 8) & 0xff)); + buffer.push_back(static_cast((value >> 16) & 0xff)); + buffer.push_back(static_cast((value >> 24) & 0xff)); +} + +void append_ascii(std::vector& buffer, std::string_view value) +{ buffer.insert(buffer.end(), value.begin(), value.end()); } + +std::vector build_wav(std::span pattern) +{ + uint32_t sample_count = 0; + for (const auto& segment : pattern) { + sample_count += static_cast((static_cast(segment.duration_ms) * kSampleRate) / 1000); + } + + constexpr uint16_t channels = 1; + constexpr uint16_t bits_per_sample = 16; + const uint32_t data_bytes = sample_count * channels * (bits_per_sample / 8); + + std::vector buffer; + buffer.reserve(44 + data_bytes); + append_ascii(buffer, "RIFF"); + append_u32(buffer, 36 + data_bytes); + append_ascii(buffer, "WAVE"); + append_ascii(buffer, "fmt "); + append_u32(buffer, 16); + append_u16(buffer, 1); + append_u16(buffer, channels); + append_u32(buffer, kSampleRate); + append_u32(buffer, kSampleRate * channels * (bits_per_sample / 8)); + append_u16(buffer, channels * (bits_per_sample / 8)); + append_u16(buffer, bits_per_sample); + append_ascii(buffer, "data"); + append_u32(buffer, data_bytes); + + double phase = 0.0; + for (const auto& segment : pattern) { + const auto segment_samples = static_cast((static_cast(segment.duration_ms) * kSampleRate) / 1000); + const auto step = segment.frequency_hz > 0.0 ? kTwoPi * segment.frequency_hz / kSampleRate : 0.0; + for (int index = 0; index < segment_samples; ++index) { + const auto fade_in = std::min(1.0, static_cast(index) / 80.0); + const auto fade_out = std::min(1.0, static_cast(segment_samples - index) / 120.0); + const auto sample = segment.frequency_hz > 0.0 ? std::sin(phase) * kAmplitude * fade_in * fade_out : 0.0; + append_u16(buffer, static_cast(static_cast(sample * 32767.0))); + phase += step; + if (phase > kTwoPi) { + phase -= kTwoPi; + } + } + } + return buffer; +} + +void initialize_sound_buffers() +{ + for (size_t index = 0; index < s_sound_buffers.size(); ++index) { + const auto pattern = sound_pattern(static_cast(index)); + if (!pattern.empty()) { + s_sound_buffers[index] = build_wav(pattern); + } + } +} +} // namespace + +const char* notification_sound_name(NotificationSound sound) +{ + switch (sound) { + case NotificationSound::None: + return "none"; + case NotificationSound::Default: + return "default"; + case NotificationSound::Info: + return "info"; + case NotificationSound::Success: + return "success"; + case NotificationSound::Warning: + return "warning"; + case NotificationSound::Alarm: + return "alarm"; + case NotificationSound::Arrival: + return "arrival"; + case NotificationSound::Soft: + return "soft"; + case NotificationSound::Ping: + return "ping"; + case NotificationSound::Repair: + return "repair"; + default: + return "none"; + } +} + +std::optional notification_sound_from_name(std::string_view name) +{ + const auto normalized = normalize_sound_name(name); + if (normalized == "none" || normalized == "off" || normalized == "silent") + return NotificationSound::None; + if (normalized == "default") + return NotificationSound::Default; + if (normalized == "info") + return NotificationSound::Info; + if (normalized == "success" || normalized == "victory") + return NotificationSound::Success; + if (normalized == "warning" || normalized == "warn") + return NotificationSound::Warning; + if (normalized == "alarm" || normalized == "attack") + return NotificationSound::Alarm; + if (normalized == "arrival" || normalized == "arrive") + return NotificationSound::Arrival; + if (normalized == "soft" || normalized == "quiet") + return NotificationSound::Soft; + if (normalized == "ping") + return NotificationSound::Ping; + if (normalized == "repair" || normalized == "repaired") + return NotificationSound::Repair; + return std::nullopt; +} + +void notification_audio_play(NotificationSound sound) +{ + if (sound == NotificationSound::None) + return; + + std::call_once(s_sound_buffers_once, initialize_sound_buffers); + const auto index = static_cast(sound); + if (index >= s_sound_buffers.size() || s_sound_buffers[index].empty()) + return; + + const auto& buffer = s_sound_buffers[index]; + if (!notification_audio_platform_play(buffer.data(), buffer.size())) { + spdlog::warn("[NotifyAudio] Failed to play '{}' cue", notification_sound_name(sound)); + } +} + +#if _WIN32 +bool notification_audio_platform_play(const uint8_t* data, size_t) +{ return PlaySoundW(reinterpret_cast(data), nullptr, SND_ASYNC | SND_MEMORY | SND_NODEFAULT) != FALSE; } +#elif !defined(__APPLE__) +bool notification_audio_platform_play(const uint8_t*, size_t) +{ return false; } +#endif diff --git a/mods/src/patches/notification_audio.h b/mods/src/patches/notification_audio.h new file mode 100644 index 000000000..8afd5347a --- /dev/null +++ b/mods/src/patches/notification_audio.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +enum class NotificationSound : uint8_t { + None = 0, + Default, + Info, + Success, + Warning, + Alarm, + Arrival, + Soft, + Ping, + Repair, + Count, +}; + +[[nodiscard]] const char* notification_sound_name(NotificationSound sound); +[[nodiscard]] std::optional notification_sound_from_name(std::string_view name); +void notification_audio_play(NotificationSound sound); diff --git a/mods/src/patches/notification_audio_platform.h b/mods/src/patches/notification_audio_platform.h new file mode 100644 index 000000000..3ee8a973c --- /dev/null +++ b/mods/src/patches/notification_audio_platform.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +[[nodiscard]] bool notification_audio_platform_play(const uint8_t* data, size_t size); diff --git a/mods/src/patches/notification_service.cc b/mods/src/patches/notification_service.cc index 46109b8c4..189fabe42 100644 --- a/mods/src/patches/notification_service.cc +++ b/mods/src/patches/notification_service.cc @@ -1,5 +1,6 @@ #include "patches/notification_service.h" #include "patches/battle_notify_parser.h" +#include "patches/notification_audio.h" #include "config.h" #include "str_utils.h" @@ -93,6 +94,19 @@ static const char* toast_state_title(int state) } } +static NotificationSound toast_state_sound(int state) +{ + const auto& config = Config::Get(); + switch (state) { + case Victory: return config.alert_victory; + case Defeat: return config.alert_defeat; + case ArmadaCreated: return config.alert_armada_created; + case ArmadaBattleWon: return config.alert_armada_battle_won; + case ArmadaBattleLost: return config.alert_armada_battle_lost; + default: return NotificationSound::None; + } +} + // --------------------------------------------------------------------------- // Platform notification delivery // --------------------------------------------------------------------------- @@ -477,17 +491,23 @@ void notification_init() } catch (...) { spdlog::warn("[Notify] Windows notification service failed (unknown error)"); } +#else +#if __APPLE__ + spdlog::info("[Notify] macOS audio notification service ready"); #else spdlog::info("[Notify] Notification service: platform not supported (no-op)"); #endif +#endif } void notification_handle_toast(Toast* toast) { -#if !_WIN32 - return; // No notification delivery on non-Windows platforms yet -#else + if (!toast) return; + auto state = toast->get_State(); + notification_audio_play(toast_state_sound(state)); + +#if _WIN32 // Check if this toast type is in the user's notify list const auto& notify_types = Config::Get().notify_banner_types; diff --git a/mods/xmake.lua b/mods/xmake.lua index 1a80fc8ab..aee96c5a2 100644 --- a/mods/xmake.lua +++ b/mods/xmake.lua @@ -116,6 +116,7 @@ do if is_plat("windows") then add_cxflags("/bigobj") add_linkdirs("src/il2cpp") + add_syslinks("winmm") elseif is_plat("macosx") then add_cxflags("-fms-extensions") -- Add Objective-C++ source From 05daae5b8624e290fb36c6210b0c39eae7fe7b2a Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Sat, 29 Aug 2026 01:55:16 -0500 Subject: [PATCH 2/4] Warn when audio alerts lack toast hooks --- example_community_patch_settings_da.toml | 2 +- example_community_patch_settings_de.toml | 2 +- example_community_patch_settings_en-GB-x-cockney.toml | 2 +- example_community_patch_settings_en-x-minionese.toml | 2 +- example_community_patch_settings_en.toml | 2 +- example_community_patch_settings_es.toml | 2 +- example_community_patch_settings_fr.toml | 2 +- example_community_patch_settings_nl.toml | 2 +- example_community_patch_settings_ru.toml | 2 +- example_community_patch_settings_tlh.toml | 2 +- mods/src/config.cc | 7 +++++++ 11 files changed, 17 insertions(+), 10 deletions(-) diff --git a/example_community_patch_settings_da.toml b/example_community_patch_settings_da.toml index ff8585a4e..8906a3d17 100644 --- a/example_community_patch_settings_da.toml +++ b/example_community_patch_settings_da.toml @@ -39,6 +39,7 @@ trace_events = false # Optional generated notification cues; "none" disables an alert. # Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. alert_victory = "none" alert_defeat = "none" alert_armada_created = "none" @@ -837,4 +838,3 @@ verify_ssl = true # <[========================================================================================================================================================================================================]> [sync.targets.spocksclub] - diff --git a/example_community_patch_settings_de.toml b/example_community_patch_settings_de.toml index 2a8fc320b..29fd63105 100644 --- a/example_community_patch_settings_de.toml +++ b/example_community_patch_settings_de.toml @@ -39,6 +39,7 @@ trace_events = false # Optional generated notification cues; "none" disables an alert. # Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. alert_victory = "none" alert_defeat = "none" alert_armada_created = "none" @@ -837,4 +838,3 @@ verify_ssl = true # <[===================================================================================================================================================================================]> [sync.targets.spocksclub] - diff --git a/example_community_patch_settings_en-GB-x-cockney.toml b/example_community_patch_settings_en-GB-x-cockney.toml index e16874cd2..68196debe 100644 --- a/example_community_patch_settings_en-GB-x-cockney.toml +++ b/example_community_patch_settings_en-GB-x-cockney.toml @@ -39,6 +39,7 @@ trace_events = false # Optional generated notification cues; "none" disables an alert. # Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. alert_victory = "none" alert_defeat = "none" alert_armada_created = "none" @@ -837,4 +838,3 @@ verify_ssl = true # <[========================================================================================]> [sync.targets.spocksclub] - diff --git a/example_community_patch_settings_en-x-minionese.toml b/example_community_patch_settings_en-x-minionese.toml index 20d316196..80171600b 100644 --- a/example_community_patch_settings_en-x-minionese.toml +++ b/example_community_patch_settings_en-x-minionese.toml @@ -39,6 +39,7 @@ trace_events = false # Optional generated notification cues; "none" disables an alert. # Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. alert_victory = "none" alert_defeat = "none" alert_armada_created = "none" @@ -837,4 +838,3 @@ verify_ssl = true # <[=============================================================================================================]> [sync.targets.spocksclub] - diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 22eeb26c2..44fbed0d6 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -39,6 +39,7 @@ trace_events = false # Optional generated notification cues; "none" disables an alert. # Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. alert_victory = "none" alert_defeat = "none" alert_armada_created = "none" @@ -837,4 +838,3 @@ verify_ssl = true # <[========================================================================================]> [sync.targets.spocksclub] - diff --git a/example_community_patch_settings_es.toml b/example_community_patch_settings_es.toml index 6ea0664d8..5e07467e3 100644 --- a/example_community_patch_settings_es.toml +++ b/example_community_patch_settings_es.toml @@ -39,6 +39,7 @@ trace_events = false # Optional generated notification cues; "none" disables an alert. # Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. alert_victory = "none" alert_defeat = "none" alert_armada_created = "none" @@ -837,4 +838,3 @@ verify_ssl = true # <[==========================================================================================================================================================================================]> [sync.targets.spocksclub] - diff --git a/example_community_patch_settings_fr.toml b/example_community_patch_settings_fr.toml index 66441cb22..c80c17447 100644 --- a/example_community_patch_settings_fr.toml +++ b/example_community_patch_settings_fr.toml @@ -39,6 +39,7 @@ trace_events = false # Optional generated notification cues; "none" disables an alert. # Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. alert_victory = "none" alert_defeat = "none" alert_armada_created = "none" @@ -837,4 +838,3 @@ verify_ssl = true # <[==========================================================================================================================================================================================]> [sync.targets.spocksclub] - diff --git a/example_community_patch_settings_nl.toml b/example_community_patch_settings_nl.toml index 00361a2ae..64be505a7 100644 --- a/example_community_patch_settings_nl.toml +++ b/example_community_patch_settings_nl.toml @@ -39,6 +39,7 @@ trace_events = false # Optional generated notification cues; "none" disables an alert. # Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. alert_victory = "none" alert_defeat = "none" alert_armada_created = "none" @@ -837,4 +838,3 @@ verify_ssl = true # <[===========================================================================================================================]> [sync.targets.spocksclub] - diff --git a/example_community_patch_settings_ru.toml b/example_community_patch_settings_ru.toml index 120e2c2a1..693344ba4 100644 --- a/example_community_patch_settings_ru.toml +++ b/example_community_patch_settings_ru.toml @@ -39,6 +39,7 @@ trace_events = false # Optional generated notification cues; "none" disables an alert. # Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. alert_victory = "none" alert_defeat = "none" alert_armada_created = "none" @@ -837,4 +838,3 @@ verify_ssl = true # <[=====================================================================================================================================================================]> [sync.targets.spocksclub] - diff --git a/example_community_patch_settings_tlh.toml b/example_community_patch_settings_tlh.toml index fbe4b4606..a30e387c6 100644 --- a/example_community_patch_settings_tlh.toml +++ b/example_community_patch_settings_tlh.toml @@ -39,6 +39,7 @@ trace_events = false # Optional generated notification cues; "none" disables an alert. # Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. alert_victory = "none" alert_defeat = "none" alert_armada_created = "none" @@ -837,4 +838,3 @@ verify_ssl = true # <[======================================================================================================]> [sync.targets.spocksclub] - diff --git a/mods/src/config.cc b/mods/src/config.cc index e11aaca62..424eef469 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -979,6 +979,13 @@ void Config::Load() get_notification_sound(config, parsed, "alert_armada_battle_won", DCA::alert_armada_battle_won, write_config); this->alert_armada_battle_lost = get_notification_sound(config, parsed, "alert_armada_battle_lost", DCA::alert_armada_battle_lost, write_config); + if (!this->installToastBannerHooks + && (this->alert_victory != NotificationSound::None || this->alert_defeat != NotificationSound::None + || this->alert_armada_created != NotificationSound::None + || this->alert_armada_battle_won != NotificationSound::None + || this->alert_armada_battle_lost != NotificationSound::None)) { + spdlog::warn("audio alerts require patches.toastbannerhooks = true"); + } this->auto_open_bulk_claim_flyout = get_config_or_default(config, parsed, "ui", "auto_open_bulk_claim_flyout", DCU::auto_open_bulk_claim_flyout, write_config); From b39cf0ad527a4d044042716d48dae429aa1262c8 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Tue, 1 Sep 2026 21:43:26 -0500 Subject: [PATCH 3/4] Centralize toast audio alert configuration Use one descriptor catalog for parsing, dependency checks, and toast-state lookup while preserving unconditional config normalization. --- mods/src/config.cc | 53 +++++++++++++++++------- mods/src/config.h | 12 +++--- mods/src/patches/notification_service.cc | 15 +------ 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/mods/src/config.cc b/mods/src/config.cc index 424eef469..5878c90c9 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -30,6 +30,27 @@ namespace DCS = DefaultConfig::Sync; namespace DCSC = DefaultConfig::SystemConfig; namespace DCSH = DefaultConfig::Shortcuts; +namespace +{ +struct ToastAudioAlertConfig { + int toast_state; + std::string_view config_name; + std::string_view default_sound; + NotificationSound Config::* config_member; +}; + +constexpr std::array kToastAudioAlerts{ + ToastAudioAlertConfig{ToastState::Victory, "alert_victory", DCA::alert_victory, &Config::alert_victory}, + ToastAudioAlertConfig{ToastState::Defeat, "alert_defeat", DCA::alert_defeat, &Config::alert_defeat}, + ToastAudioAlertConfig{ToastState::ArmadaCreated, "alert_armada_created", DCA::alert_armada_created, + &Config::alert_armada_created}, + ToastAudioAlertConfig{ToastState::ArmadaBattleWon, "alert_armada_battle_won", DCA::alert_armada_battle_won, + &Config::alert_armada_battle_won}, + ToastAudioAlertConfig{ToastState::ArmadaBattleLost, "alert_armada_battle_lost", DCA::alert_armada_battle_lost, + &Config::alert_armada_battle_lost}, +}; +} // namespace + static const eastl::tuple bannerTypes[] = { {"All", ToastState::All}, {"Standard", ToastState::Standard}, @@ -126,6 +147,19 @@ Config& Config::Get() return config; } +NotificationSound Config::NotificationSoundForToast(int toast_state) const +{ + const auto alert = std::ranges::find(kToastAudioAlerts, toast_state, &ToastAudioAlertConfig::toast_state); + return alert == kToastAudioAlerts.end() ? NotificationSound::None : this->*(alert->config_member); +} + +bool Config::AnyToastAudioAlertConfigured() const +{ + return std::ranges::any_of(kToastAudioAlerts, [this](const auto& alert) { + return this->*(alert.config_member) != NotificationSound::None; + }); +} + MissionHudVisibility Config::MissionHudButtonVisibility(std::string_view button_name) const { const auto it = this->mission_hud_buttons.find(std::string(button_name)); @@ -970,20 +1004,11 @@ void Config::Load() } } this->installAudioEventHooks = this->trace_audio_events || !this->disabled_audio_events.empty(); - this->alert_victory = - get_notification_sound(config, parsed, "alert_victory", DCA::alert_victory, write_config); - this->alert_defeat = get_notification_sound(config, parsed, "alert_defeat", DCA::alert_defeat, write_config); - this->alert_armada_created = - get_notification_sound(config, parsed, "alert_armada_created", DCA::alert_armada_created, write_config); - this->alert_armada_battle_won = - get_notification_sound(config, parsed, "alert_armada_battle_won", DCA::alert_armada_battle_won, write_config); - this->alert_armada_battle_lost = - get_notification_sound(config, parsed, "alert_armada_battle_lost", DCA::alert_armada_battle_lost, write_config); - if (!this->installToastBannerHooks - && (this->alert_victory != NotificationSound::None || this->alert_defeat != NotificationSound::None - || this->alert_armada_created != NotificationSound::None - || this->alert_armada_battle_won != NotificationSound::None - || this->alert_armada_battle_lost != NotificationSound::None)) { + for (const auto& alert : kToastAudioAlerts) { + this->*(alert.config_member) = + get_notification_sound(config, parsed, alert.config_name, alert.default_sound, write_config); + } + if (!this->installToastBannerHooks && AnyToastAudioAlertConfigured()) { spdlog::warn("audio alerts require patches.toastbannerhooks = true"); } this->auto_open_bulk_claim_flyout = get_config_or_default(config, parsed, "ui", "auto_open_bulk_claim_flyout", diff --git a/mods/src/config.h b/mods/src/config.h index 93264d756..0e5a4c46e 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -142,6 +142,8 @@ class Config final [[nodiscard]] MissionHudVisibility MissionHudButtonVisibility(std::string_view button_name) const; [[nodiscard]] bool MissionHudTweaksEnabled() const; + [[nodiscard]] NotificationSound NotificationSoundForToast(int toast_state) const; + [[nodiscard]] bool AnyToastAudioAlertConfigured() const; // Disallow copying/moving to enforce singleton Config(const Config&) = delete; @@ -198,11 +200,11 @@ class Config final bool disable_toast_banners; bool trace_audio_events; std::vector disabled_audio_events; - NotificationSound alert_victory; - NotificationSound alert_defeat; - NotificationSound alert_armada_created; - NotificationSound alert_armada_battle_won; - NotificationSound alert_armada_battle_lost; + NotificationSound alert_victory = NotificationSound::None; + NotificationSound alert_defeat = NotificationSound::None; + NotificationSound alert_armada_created = NotificationSound::None; + NotificationSound alert_armada_battle_won = NotificationSound::None; + NotificationSound alert_armada_battle_lost = NotificationSound::None; bool auto_open_bulk_claim_flyout; bool auto_confirm_ft_upgrade; diff --git a/mods/src/patches/notification_service.cc b/mods/src/patches/notification_service.cc index 189fabe42..45e09b850 100644 --- a/mods/src/patches/notification_service.cc +++ b/mods/src/patches/notification_service.cc @@ -94,19 +94,6 @@ static const char* toast_state_title(int state) } } -static NotificationSound toast_state_sound(int state) -{ - const auto& config = Config::Get(); - switch (state) { - case Victory: return config.alert_victory; - case Defeat: return config.alert_defeat; - case ArmadaCreated: return config.alert_armada_created; - case ArmadaBattleWon: return config.alert_armada_battle_won; - case ArmadaBattleLost: return config.alert_armada_battle_lost; - default: return NotificationSound::None; - } -} - // --------------------------------------------------------------------------- // Platform notification delivery // --------------------------------------------------------------------------- @@ -505,7 +492,7 @@ void notification_handle_toast(Toast* toast) if (!toast) return; auto state = toast->get_State(); - notification_audio_play(toast_state_sound(state)); + notification_audio_play(Config::Get().NotificationSoundForToast(state)); #if _WIN32 From 218c5947e3246d0130aae14865e191f4a23a1f5c Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 3 Sep 2026 00:39:59 -0500 Subject: [PATCH 4/4] Refine notification audio catalog and initialization --- mods/src/config.cc | 34 ++-- mods/src/config.h | 1 - mods/src/patches/notification_audio.cc | 197 +++++++++++++---------- mods/src/patches/notification_audio.h | 2 +- mods/src/patches/notification_service.cc | 13 +- 5 files changed, 135 insertions(+), 112 deletions(-) diff --git a/mods/src/config.cc b/mods/src/config.cc index 5878c90c9..a00f13481 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -39,16 +39,15 @@ struct ToastAudioAlertConfig { NotificationSound Config::* config_member; }; -constexpr std::array kToastAudioAlerts{ - ToastAudioAlertConfig{ToastState::Victory, "alert_victory", DCA::alert_victory, &Config::alert_victory}, - ToastAudioAlertConfig{ToastState::Defeat, "alert_defeat", DCA::alert_defeat, &Config::alert_defeat}, - ToastAudioAlertConfig{ToastState::ArmadaCreated, "alert_armada_created", DCA::alert_armada_created, - &Config::alert_armada_created}, - ToastAudioAlertConfig{ToastState::ArmadaBattleWon, "alert_armada_battle_won", DCA::alert_armada_battle_won, - &Config::alert_armada_battle_won}, - ToastAudioAlertConfig{ToastState::ArmadaBattleLost, "alert_armada_battle_lost", DCA::alert_armada_battle_lost, - &Config::alert_armada_battle_lost}, -}; +constexpr auto kToastAudioAlerts = std::to_array({ + {ToastState::Victory, "alert_victory", DCA::alert_victory, &Config::alert_victory}, + {ToastState::Defeat, "alert_defeat", DCA::alert_defeat, &Config::alert_defeat}, + {ToastState::ArmadaCreated, "alert_armada_created", DCA::alert_armada_created, &Config::alert_armada_created}, + {ToastState::ArmadaBattleWon, "alert_armada_battle_won", DCA::alert_armada_battle_won, + &Config::alert_armada_battle_won}, + {ToastState::ArmadaBattleLost, "alert_armada_battle_lost", DCA::alert_armada_battle_lost, + &Config::alert_armada_battle_lost}, +}); } // namespace static const eastl::tuple bannerTypes[] = { @@ -153,13 +152,6 @@ NotificationSound Config::NotificationSoundForToast(int toast_state) const return alert == kToastAudioAlerts.end() ? NotificationSound::None : this->*(alert->config_member); } -bool Config::AnyToastAudioAlertConfigured() const -{ - return std::ranges::any_of(kToastAudioAlerts, [this](const auto& alert) { - return this->*(alert.config_member) != NotificationSound::None; - }); -} - MissionHudVisibility Config::MissionHudButtonVisibility(std::string_view button_name) const { const auto it = this->mission_hud_buttons.find(std::string(button_name)); @@ -1004,11 +996,13 @@ void Config::Load() } } this->installAudioEventHooks = this->trace_audio_events || !this->disabled_audio_events.empty(); + bool any_toast_audio_alert_configured = false; for (const auto& alert : kToastAudioAlerts) { - this->*(alert.config_member) = - get_notification_sound(config, parsed, alert.config_name, alert.default_sound, write_config); + const auto sound = get_notification_sound(config, parsed, alert.config_name, alert.default_sound, write_config); + this->*(alert.config_member) = sound; + any_toast_audio_alert_configured |= sound != NotificationSound::None; } - if (!this->installToastBannerHooks && AnyToastAudioAlertConfigured()) { + if (!this->installToastBannerHooks && any_toast_audio_alert_configured) { spdlog::warn("audio alerts require patches.toastbannerhooks = true"); } this->auto_open_bulk_claim_flyout = get_config_or_default(config, parsed, "ui", "auto_open_bulk_claim_flyout", diff --git a/mods/src/config.h b/mods/src/config.h index 0e5a4c46e..ffb19d1e7 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -143,7 +143,6 @@ class Config final [[nodiscard]] MissionHudVisibility MissionHudButtonVisibility(std::string_view button_name) const; [[nodiscard]] bool MissionHudTweaksEnabled() const; [[nodiscard]] NotificationSound NotificationSoundForToast(int toast_state) const; - [[nodiscard]] bool AnyToastAudioAlertConfigured() const; // Disallow copying/moving to enforce singleton Config(const Config&) = delete; diff --git a/mods/src/patches/notification_audio.cc b/mods/src/patches/notification_audio.cc index ebdd87c3c..1e2d5f7df 100644 --- a/mods/src/patches/notification_audio.cc +++ b/mods/src/patches/notification_audio.cc @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -39,8 +38,92 @@ constexpr std::array kSoftPattern{{{523.0, 90}, {0.0, 26}, {659. constexpr std::array kPingPattern{{{1046.0, 95}}}; constexpr std::array kRepairPattern{{{440.0, 70}, {0.0, 18}, {554.0, 70}, {0.0, 18}, {740.0, 140}}}; -std::once_flag s_sound_buffers_once; -std::array, static_cast(NotificationSound::Count)> s_sound_buffers; +struct SoundDefinition { + NotificationSound sound; + std::string_view name; + std::span pattern; +}; + +constexpr auto kSoundDefinitions = std::to_array({ + {NotificationSound::None, "none", {}}, + {NotificationSound::Default, "default", kDefaultPattern}, + {NotificationSound::Info, "info", kInfoPattern}, + {NotificationSound::Success, "success", kSuccessPattern}, + {NotificationSound::Warning, "warning", kWarningPattern}, + {NotificationSound::Alarm, "alarm", kAlarmPattern}, + {NotificationSound::Arrival, "arrival", kArrivalPattern}, + {NotificationSound::Soft, "soft", kSoftPattern}, + {NotificationSound::Ping, "ping", kPingPattern}, + {NotificationSound::Repair, "repair", kRepairPattern}, +}); + +struct SoundAlias { + std::string_view name; + NotificationSound sound; +}; + +constexpr auto kSoundAliases = std::to_array({ + {"off", NotificationSound::None}, + {"silent", NotificationSound::None}, + {"victory", NotificationSound::Success}, + {"warn", NotificationSound::Warning}, + {"attack", NotificationSound::Alarm}, + {"arrive", NotificationSound::Arrival}, + {"quiet", NotificationSound::Soft}, + {"repaired", NotificationSound::Repair}, +}); + +consteval bool SoundCatalogIsValid() +{ + if (kSoundDefinitions.size() != static_cast(NotificationSound::Count)) { + return false; + } + + for (size_t index = 0; index < kSoundDefinitions.size(); ++index) { + const auto& definition = kSoundDefinitions[index]; + if (static_cast(definition.sound) != index || definition.name.empty() + || (definition.sound == NotificationSound::None) != definition.pattern.empty()) { + return false; + } + for (size_t other = index + 1; other < kSoundDefinitions.size(); ++other) { + if (definition.name == kSoundDefinitions[other].name) { + return false; + } + } + } + + for (size_t index = 0; index < kSoundAliases.size(); ++index) { + const auto& alias = kSoundAliases[index]; + if (alias.name.empty() || static_cast(alias.sound) >= kSoundDefinitions.size()) { + return false; + } + for (const auto& definition : kSoundDefinitions) { + if (alias.name == definition.name) { + return false; + } + } + for (size_t other = index + 1; other < kSoundAliases.size(); ++other) { + if (alias.name == kSoundAliases[other].name) { + return false; + } + } + } + return true; +} + +static_assert(SoundCatalogIsValid()); + +constexpr const SoundDefinition* sound_definition(NotificationSound sound) +{ + const auto index = static_cast(sound); + if (index >= kSoundDefinitions.size() || kSoundDefinitions[index].sound != sound) { + return nullptr; + } + return &kSoundDefinitions[index]; +} + +static_assert(sound_definition(NotificationSound::Success)->name == "success"); +static_assert(sound_definition(NotificationSound::Count) == nullptr); std::string normalize_sound_name(std::string_view value) { @@ -58,32 +141,6 @@ std::string normalize_sound_name(std::string_view value) return normalized; } -std::span sound_pattern(NotificationSound sound) -{ - switch (sound) { - case NotificationSound::Default: - return kDefaultPattern; - case NotificationSound::Info: - return kInfoPattern; - case NotificationSound::Success: - return kSuccessPattern; - case NotificationSound::Warning: - return kWarningPattern; - case NotificationSound::Alarm: - return kAlarmPattern; - case NotificationSound::Arrival: - return kArrivalPattern; - case NotificationSound::Soft: - return kSoftPattern; - case NotificationSound::Ping: - return kPingPattern; - case NotificationSound::Repair: - return kRepairPattern; - default: - return {}; - } -} - void append_u16(std::vector& buffer, uint16_t value) { buffer.push_back(static_cast(value & 0xff)); @@ -146,68 +203,40 @@ std::vector build_wav(std::span pattern) return buffer; } -void initialize_sound_buffers() +using SoundBuffers = std::array, static_cast(NotificationSound::Count)>; + +const SoundBuffers& sound_buffers() { - for (size_t index = 0; index < s_sound_buffers.size(); ++index) { - const auto pattern = sound_pattern(static_cast(index)); - if (!pattern.empty()) { - s_sound_buffers[index] = build_wav(pattern); + static const auto buffers = [] { + SoundBuffers result; + for (const auto& definition : kSoundDefinitions) { + if (!definition.pattern.empty()) { + result[static_cast(definition.sound)] = build_wav(definition.pattern); + } } - } + return result; + }(); + return buffers; } } // namespace -const char* notification_sound_name(NotificationSound sound) +std::string_view notification_sound_name(NotificationSound sound) { - switch (sound) { - case NotificationSound::None: - return "none"; - case NotificationSound::Default: - return "default"; - case NotificationSound::Info: - return "info"; - case NotificationSound::Success: - return "success"; - case NotificationSound::Warning: - return "warning"; - case NotificationSound::Alarm: - return "alarm"; - case NotificationSound::Arrival: - return "arrival"; - case NotificationSound::Soft: - return "soft"; - case NotificationSound::Ping: - return "ping"; - case NotificationSound::Repair: - return "repair"; - default: - return "none"; - } + const auto* definition = sound_definition(sound); + return definition ? definition->name : kSoundDefinitions.front().name; } std::optional notification_sound_from_name(std::string_view name) { const auto normalized = normalize_sound_name(name); - if (normalized == "none" || normalized == "off" || normalized == "silent") - return NotificationSound::None; - if (normalized == "default") - return NotificationSound::Default; - if (normalized == "info") - return NotificationSound::Info; - if (normalized == "success" || normalized == "victory") - return NotificationSound::Success; - if (normalized == "warning" || normalized == "warn") - return NotificationSound::Warning; - if (normalized == "alarm" || normalized == "attack") - return NotificationSound::Alarm; - if (normalized == "arrival" || normalized == "arrive") - return NotificationSound::Arrival; - if (normalized == "soft" || normalized == "quiet") - return NotificationSound::Soft; - if (normalized == "ping") - return NotificationSound::Ping; - if (normalized == "repair" || normalized == "repaired") - return NotificationSound::Repair; + if (const auto definition = std::ranges::find(kSoundDefinitions, normalized, &SoundDefinition::name); + definition != kSoundDefinitions.end()) { + return definition->sound; + } + if (const auto alias = std::ranges::find(kSoundAliases, normalized, &SoundAlias::name); + alias != kSoundAliases.end()) { + return alias->sound; + } return std::nullopt; } @@ -216,12 +245,14 @@ void notification_audio_play(NotificationSound sound) if (sound == NotificationSound::None) return; - std::call_once(s_sound_buffers_once, initialize_sound_buffers); const auto index = static_cast(sound); - if (index >= s_sound_buffers.size() || s_sound_buffers[index].empty()) + if (index >= kSoundDefinitions.size()) + return; + + const auto& buffer = sound_buffers()[index]; + if (buffer.empty()) return; - const auto& buffer = s_sound_buffers[index]; if (!notification_audio_platform_play(buffer.data(), buffer.size())) { spdlog::warn("[NotifyAudio] Failed to play '{}' cue", notification_sound_name(sound)); } diff --git a/mods/src/patches/notification_audio.h b/mods/src/patches/notification_audio.h index 8afd5347a..345fa9840 100644 --- a/mods/src/patches/notification_audio.h +++ b/mods/src/patches/notification_audio.h @@ -18,6 +18,6 @@ enum class NotificationSound : uint8_t { Count, }; -[[nodiscard]] const char* notification_sound_name(NotificationSound sound); +[[nodiscard]] std::string_view notification_sound_name(NotificationSound sound); [[nodiscard]] std::optional notification_sound_from_name(std::string_view name); void notification_audio_play(NotificationSound sound); diff --git a/mods/src/patches/notification_service.cc b/mods/src/patches/notification_service.cc index f2f8d43c4..293424f4c 100644 --- a/mods/src/patches/notification_service.cc +++ b/mods/src/patches/notification_service.cc @@ -450,6 +450,7 @@ static std::string strip_unity_rich_text(const std::string& s) void notification_init() { +#if _WIN32 // Resolve LanguageManager::Localize(out string, LocaleTextContext) — the // 2-parameter overload that takes an LTC and returns a localized string. auto lm_helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.Localization", "LanguageManager"); @@ -500,7 +501,6 @@ void notification_init() spdlog::warn("[Notify] Could not resolve Object::ToString — placeholder formatting may be incomplete"); } -#if _WIN32 try { winrt::init_apartment(winrt::apartment_type::single_threaded); spdlog::info("[Notify] Windows notification service initialized"); @@ -510,26 +510,25 @@ void notification_init() } catch (...) { spdlog::warn("[Notify] Windows notification service failed (unknown error)"); } -#else -#if __APPLE__ +#elif __APPLE__ spdlog::info("[Notify] macOS audio notification service ready"); #else spdlog::info("[Notify] Notification service: platform not supported (no-op)"); #endif -#endif } void notification_handle_toast(Toast* toast) { if (!toast) return; - auto state = toast->get_State(); - notification_audio_play(Config::Get().NotificationSoundForToast(state)); + const auto& config = Config::Get(); + const auto state = toast->get_State(); + notification_audio_play(config.NotificationSoundForToast(state)); #if _WIN32 // Check if this toast type is in the user's notify list - const auto& notify_types = Config::Get().notify_banner_types; + const auto& notify_types = config.notify_banner_types; if (std::ranges::find(notify_types, state) == notify_types.end()) { return; }