diff --git a/example_community_patch_settings_da.toml b/example_community_patch_settings_da.toml index c6373de8..22f5b0a5 100644 --- a/example_community_patch_settings_da.toml +++ b/example_community_patch_settings_da.toml @@ -37,6 +37,15 @@ 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 +# Requires patches.toastbannerhooks = true. +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 c08d66ba..59c5ddff 100644 --- a/example_community_patch_settings_de.toml +++ b/example_community_patch_settings_de.toml @@ -37,6 +37,15 @@ 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 +# Requires patches.toastbannerhooks = true. +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 04317d9e..7ae598b9 100644 --- a/example_community_patch_settings_en-GB-x-cockney.toml +++ b/example_community_patch_settings_en-GB-x-cockney.toml @@ -37,6 +37,15 @@ 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 +# Requires patches.toastbannerhooks = true. +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 7d7f284d..354507f9 100644 --- a/example_community_patch_settings_en-x-minionese.toml +++ b/example_community_patch_settings_en-x-minionese.toml @@ -37,6 +37,15 @@ 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 +# Requires patches.toastbannerhooks = true. +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 7cb04d7f..8c3a6f67 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -37,6 +37,15 @@ 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 +# Requires patches.toastbannerhooks = true. +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 1d601318..442166d4 100644 --- a/example_community_patch_settings_es.toml +++ b/example_community_patch_settings_es.toml @@ -37,6 +37,15 @@ 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 +# Requires patches.toastbannerhooks = true. +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 8dc5fcdc..64c3d66c 100644 --- a/example_community_patch_settings_fr.toml +++ b/example_community_patch_settings_fr.toml @@ -37,6 +37,15 @@ 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 +# Requires patches.toastbannerhooks = true. +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 37811b0f..ca398bd6 100644 --- a/example_community_patch_settings_nl.toml +++ b/example_community_patch_settings_nl.toml @@ -37,6 +37,15 @@ 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 +# Requires patches.toastbannerhooks = true. +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 92c18bbf..55f55728 100644 --- a/example_community_patch_settings_ru.toml +++ b/example_community_patch_settings_ru.toml @@ -37,6 +37,15 @@ 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 +# Requires patches.toastbannerhooks = true. +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 4de27097..37a8dde7 100644 --- a/example_community_patch_settings_tlh.toml +++ b/example_community_patch_settings_tlh.toml @@ -37,6 +37,15 @@ 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 +# Requires patches.toastbannerhooks = true. +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 96650c21..27682b08 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -32,6 +32,26 @@ 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 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[] = { {"All", ToastState::All}, {"Standard", ToastState::Standard}, @@ -128,6 +148,12 @@ 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); +} + MissionHudVisibility Config::MissionHudButtonVisibility(std::string_view button_name) const { const auto it = this->mission_hud_buttons.find(std::string(button_name)); @@ -324,6 +350,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; +} + template T get_config_or_default_with_alias(toml::table& config, toml::table& new_config, std::string_view section, std::string_view item, std::string_view alias, T default_value, bool write_log) @@ -1052,6 +1097,15 @@ void Config::Load() this->disabled_audio_events.emplace_back(stripped); } } + bool any_toast_audio_alert_configured = false; + for (const auto& alert : kToastAudioAlerts) { + 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 && 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", DCU::auto_open_bulk_claim_flyout, write_config); diff --git a/mods/src/config.h b/mods/src/config.h index ece72099..c0063409 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 @@ -158,6 +160,7 @@ class Config final void AdjustUiViewerScale(bool scaleUp); [[nodiscard]] MissionHudVisibility MissionHudButtonVisibility(std::string_view button_name) const; + [[nodiscard]] NotificationSound NotificationSoundForToast(int toast_state) const; // Disallow copying/moving to enforce singleton Config(const Config&) = delete; @@ -217,6 +220,11 @@ class Config final bool disable_toast_banners; bool trace_audio_events; std::vector disabled_audio_events; + 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/defaultconfig.h b/mods/src/defaultconfig.h index 0a0a49f9..8609e833 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 00000000..cae47a9e --- /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 00000000..1e2d5f7d --- /dev/null +++ b/mods/src/patches/notification_audio.cc @@ -0,0 +1,267 @@ +#include "patches/notification_audio.h" + +#include "patches/notification_audio_platform.h" + +#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}}}; + +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) +{ + 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; +} + +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; +} + +using SoundBuffers = std::array, static_cast(NotificationSound::Count)>; + +const SoundBuffers& sound_buffers() +{ + 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 + +std::string_view notification_sound_name(NotificationSound sound) +{ + 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 (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; +} + +void notification_audio_play(NotificationSound sound) +{ + if (sound == NotificationSound::None) + return; + + const auto index = static_cast(sound); + if (index >= kSoundDefinitions.size()) + return; + + const auto& buffer = sound_buffers()[index]; + if (buffer.empty()) + return; + + 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 00000000..345fa984 --- /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]] 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_audio_platform.h b/mods/src/patches/notification_audio_platform.h new file mode 100644 index 00000000..3ee8a973 --- /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 54edf2b3..293424f4 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" @@ -449,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"); @@ -499,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"); @@ -509,6 +510,8 @@ void notification_init() } catch (...) { spdlog::warn("[Notify] Windows notification service failed (unknown error)"); } +#elif __APPLE__ + spdlog::info("[Notify] macOS audio notification service ready"); #else spdlog::info("[Notify] Notification service: platform not supported (no-op)"); #endif @@ -516,13 +519,16 @@ void notification_init() void notification_handle_toast(Toast* toast) { -#if !_WIN32 - return; // No notification delivery on non-Windows platforms yet -#else - auto state = toast->get_State(); + if (!toast) return; + + 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; } diff --git a/mods/xmake.lua b/mods/xmake.lua index 1a80fc8a..aee96c5a 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