From ac12ed0bdb6586273f47e08728e2d03855def31e Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Sat, 29 Aug 2026 01:15:47 -0500 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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; } From fd15fb53245172579e570af433c892074434d1e5 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 21 Sep 2026 20:44:10 -0500 Subject: [PATCH 05/11] Add shared IL2CPP runtime boundary helpers with contract tests --- .github/workflows/ci.yaml | 12 +++ mods/src/il2cpp/il2cpp_helper.h | 6 +- mods/src/il2cpp/runtime.h | 59 +++++++++++++++ tests/il2cpp_runtime.cc | 127 ++++++++++++++++++++++++++++++++ xmake.lua | 11 +++ 5 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 mods/src/il2cpp/runtime.h create mode 100644 tests/il2cpp_runtime.cc diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 61cf42826..ff6de6ff7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -189,6 +189,13 @@ jobs: shell: pwsh run: sccache --zero-stats + - name: Test IL2CPP runtime helpers + run: | + xmake build -y il2cpp-runtime-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + xmake run il2cpp-runtime-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Build run: xmake build -y stfc-community-mod @@ -469,6 +476,11 @@ jobs: shell: bash run: sccache --zero-stats + - name: Test IL2CPP runtime helpers + run: | + xmake build -y il2cpp-runtime-tests + xmake run il2cpp-runtime-tests + - name: Build # -D keeps the existing XMake compiler-cache diagnostics too. run: xmake -y -D diff --git a/mods/src/il2cpp/il2cpp_helper.h b/mods/src/il2cpp/il2cpp_helper.h index 3d4ff6e3f..19708b2cd 100644 --- a/mods/src/il2cpp/il2cpp_helper.h +++ b/mods/src/il2cpp/il2cpp_helper.h @@ -337,10 +337,10 @@ class IL2CppClassHelper inline IL2CppClassHelper il2cpp_get_class_helper_impl(const char* assembly, const char* namespacez, const char* name) { auto domain = il2cpp_domain_get(); - auto assemblyT = il2cpp_domain_assembly_open(domain, assembly); - auto image = il2cpp_assembly_get_image(assemblyT); + auto assemblyT = domain ? il2cpp_domain_assembly_open(domain, assembly) : nullptr; + auto image = assemblyT ? il2cpp_assembly_get_image(assemblyT) : nullptr; - auto cls = il2cpp_class_from_name(image, namespacez, name); + auto cls = image ? il2cpp_class_from_name(image, namespacez, name) : nullptr; return IL2CppClassHelper{cls}; } diff --git a/mods/src/il2cpp/runtime.h b/mods/src/il2cpp/runtime.h new file mode 100644 index 000000000..8617cedb1 --- /dev/null +++ b/mods/src/il2cpp/runtime.h @@ -0,0 +1,59 @@ +#pragma once + +#include "il2cpp_helper.h" +#include + +// Mechanics shared by optional runtime features. Signature/overload selection, +// argument storage and feature-specific failure policy remain with the caller. +namespace Il2CppRuntime +{ +inline Il2CppClass* Class(const char* assembly, const char* ns, const char* name) +{ return il2cpp_get_class_helper(assembly, ns, name).get_cls(); } + +inline const MethodInfo* Method(Il2CppClass* cls, const char* name, int count) +{ return IL2CppClassHelper(cls).GetMethodInfo(name, count); } + +inline bool Type(const Il2CppType* type, int expected) +{ return type && !type->byref && type->type == expected; } + +inline bool Reference(const Il2CppType* type) +{ + return Type(type, IL2CPP_TYPE_CLASS) || Type(type, IL2CPP_TYPE_GENERICINST) || Type(type, IL2CPP_TYPE_OBJECT) + || Type(type, IL2CPP_TYPE_STRING); +} + +inline bool Instance(const MethodInfo* method, int count, int result) +{ + return method && method->methodPointer && method->invoker_method && !(method->flags & METHOD_ATTRIBUTE_STATIC) + && method->parameters_count == count && Type(method->return_type, result) + && !method->has_full_generic_sharing_signature; +} + +// The method and argument ABI must already be established by the caller. +// IL2CPP takes references directly, but value/byref arguments as addresses. +// A null target is valid for static methods. Success is independent of whether +// the return value is null (including void methods). Outputs change on success only. +inline bool TryInvoke(const MethodInfo* method, void* target, void** args, Il2CppObject** result = nullptr) +{ + if (!method) + return false; + Il2CppException* exception = nullptr; + auto* value = il2cpp_runtime_invoke(method, target, args, &exception); + if (exception) + return false; + if (result) + *result = value; + return true; +} + +inline bool TryBoolean(Il2CppObject* boxed, bool& value) +{ + if (!boxed || !boxed->klass || !Type(il2cpp_class_get_type(boxed->klass), IL2CPP_TYPE_BOOLEAN)) + return false; + auto* data = static_cast(il2cpp_object_unbox(boxed)); + if (!data) + return false; + value = *data; + return true; +} +} // namespace Il2CppRuntime diff --git a/tests/il2cpp_runtime.cc b/tests/il2cpp_runtime.cc new file mode 100644 index 000000000..1b7113340 --- /dev/null +++ b/tests/il2cpp_runtime.cc @@ -0,0 +1,127 @@ +// Production helpers with a controlled IL2CPP boundary, on Windows and macOS. +#include +#if _WIN32 +#undef IL2CPP_IMPORT +#define IL2CPP_IMPORT +#endif +#include "il2cpp/runtime.h" +#include +#include +#include + +namespace +{ +Il2CppClass klass; +Il2CppType type{}; +Il2CppObject object{}; +MethodInfo method{}; +int stage = 3, calls = 0; +bool fail = false, boxed = false, null_result = false; +void* seen_target = nullptr; +void** seen_args = nullptr; +} // namespace +#if _WIN32 +#define API(ret, name, params) extern "C" ret name params +#define END_API +#else +#define API(ret, name, params) name##_t name = +[] params->ret +#define END_API ; +#endif +API(Il2CppDomain*, il2cpp_domain_get, ()) +{ return stage >= 0 ? reinterpret_cast(&object) : nullptr; } +END_API +API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain * domain, const char*)) +{ + if (!domain) + std::abort(); + return stage >= 1 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(const Il2CppImage*, il2cpp_assembly_get_image, (const Il2CppAssembly* assembly)) +{ + if (!assembly) + std::abort(); + return stage >= 2 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(Il2CppClass*, il2cpp_class_from_name, (const Il2CppImage* image, const char*, const char*)) +{ + if (!image) + std::abort(); + return stage >= 3 ? &klass : nullptr; +} +END_API +API(const MethodInfo*, il2cpp_class_get_method_from_name, (Il2CppClass * cls, const char*, int)) +{ + if (!cls) + std::abort(); + return &method; +} +END_API +API(const Il2CppType*, il2cpp_class_get_type, (Il2CppClass*)) +{ return &type; } +END_API +API(void*, il2cpp_object_unbox, (Il2CppObject*)) +{ return &boxed; } +END_API +API(Il2CppObject*, il2cpp_runtime_invoke, (const MethodInfo*, void* target, void** args, Il2CppException** error)) +{ + ++calls; + seen_target = target; + seen_args = args; + if (fail) + *error = reinterpret_cast(&object); + return null_result ? nullptr : &object; +} +END_API +void Require(bool condition) +{ + if (!condition) + throw std::runtime_error("runtime helper regression"); +} +int main() +{ + for (stage = -1; stage < 3; ++stage) + Require(!Il2CppRuntime::Class("Assembly", "Namespace", "Class")); + Require(Il2CppRuntime::Class("Assembly", "Namespace", "Class") == &klass); + Require(!Il2CppRuntime::Method(nullptr, "Method", 0)); + Require(Il2CppRuntime::Method(&klass, "Method", 0) == &method); + + Il2CppObject* result = &object; + Require(!Il2CppRuntime::TryInvoke(nullptr, nullptr, nullptr, &result) && calls == 0 && result == &object); + bool value = true; + void* args[] = {&value, &object, nullptr}; + Require(Il2CppRuntime::TryInvoke(&method, &object, args, &result)); + Require(seen_target == &object && seen_args == args && seen_args[0] == &value && seen_args[1] == &object); + // Static calls, null reference/void returns and exceptions remain distinct. + null_result = true; + Require(Il2CppRuntime::TryInvoke(&method, nullptr, args, &result) && !result && !seen_target); + result = &object; + fail = true; + Require(!Il2CppRuntime::TryInvoke(&method, nullptr, args, &result) && result == &object); + + object.klass = &klass; + type.type = IL2CPP_TYPE_BOOLEAN; + Require(Il2CppRuntime::TryBoolean(&object, value) && !value); + boxed = true; + Require(Il2CppRuntime::TryBoolean(&object, value) && value); + type.type = IL2CPP_TYPE_I4; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + Require(!Il2CppRuntime::TryBoolean(nullptr, value)); + type.type = IL2CPP_TYPE_CLASS; + Require(Il2CppRuntime::Reference(&type)); + type.byref = true; + Require(!Il2CppRuntime::Reference(&type)); + type.byref = false; + type.type = IL2CPP_TYPE_VOID; + method.methodPointer = reinterpret_cast(1); + method.invoker_method = reinterpret_cast(1); + method.return_type = &type; + Require(Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + method.flags = METHOD_ATTRIBUTE_STATIC; + Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + method.flags = 0; + method.has_full_generic_sharing_signature = true; + Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + std::cout << "IL2CPP runtime helper regressions passed\n"; +} diff --git a/xmake.lua b/xmake.lua index 9b25acbfb..ac08005a2 100644 --- a/xmake.lua +++ b/xmake.lua @@ -26,3 +26,14 @@ add_rules("mode.releasedbg") includes("xmake/rules/protobuf_sccache.lua") includes("xmake/rules/cxx_sccache.lua") includes("mods") + +target("il2cpp-runtime-tests") + set_kind("binary") + set_default(false) + add_files("tests/il2cpp_runtime.cc") + add_includedirs("mods/src") + add_packages("libil2cpp", "eastl") + set_exceptions("cxx") + if is_plat("windows") then + add_linkdirs("mods/src/il2cpp") + end From 733997cfe2cd34c9203b61cff755046ca9937643 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 21 Sep 2026 22:27:13 -0500 Subject: [PATCH 06/11] Narrow shared invocation helpers around existing loading screens --- mods/src/il2cpp/il2cpp_helper.h | 6 +- mods/src/il2cpp/runtime.h | 27 ++----- .../src/patches/parts/loading_screen_common.h | 16 ++--- tests/il2cpp_runtime.cc | 70 ++++--------------- 4 files changed, 28 insertions(+), 91 deletions(-) diff --git a/mods/src/il2cpp/il2cpp_helper.h b/mods/src/il2cpp/il2cpp_helper.h index 19708b2cd..3d4ff6e3f 100644 --- a/mods/src/il2cpp/il2cpp_helper.h +++ b/mods/src/il2cpp/il2cpp_helper.h @@ -337,10 +337,10 @@ class IL2CppClassHelper inline IL2CppClassHelper il2cpp_get_class_helper_impl(const char* assembly, const char* namespacez, const char* name) { auto domain = il2cpp_domain_get(); - auto assemblyT = domain ? il2cpp_domain_assembly_open(domain, assembly) : nullptr; - auto image = assemblyT ? il2cpp_assembly_get_image(assemblyT) : nullptr; + auto assemblyT = il2cpp_domain_assembly_open(domain, assembly); + auto image = il2cpp_assembly_get_image(assemblyT); - auto cls = image ? il2cpp_class_from_name(image, namespacez, name) : nullptr; + auto cls = il2cpp_class_from_name(image, namespacez, name); return IL2CppClassHelper{cls}; } diff --git a/mods/src/il2cpp/runtime.h b/mods/src/il2cpp/runtime.h index 8617cedb1..b3043673e 100644 --- a/mods/src/il2cpp/runtime.h +++ b/mods/src/il2cpp/runtime.h @@ -7,28 +7,6 @@ // argument storage and feature-specific failure policy remain with the caller. namespace Il2CppRuntime { -inline Il2CppClass* Class(const char* assembly, const char* ns, const char* name) -{ return il2cpp_get_class_helper(assembly, ns, name).get_cls(); } - -inline const MethodInfo* Method(Il2CppClass* cls, const char* name, int count) -{ return IL2CppClassHelper(cls).GetMethodInfo(name, count); } - -inline bool Type(const Il2CppType* type, int expected) -{ return type && !type->byref && type->type == expected; } - -inline bool Reference(const Il2CppType* type) -{ - return Type(type, IL2CPP_TYPE_CLASS) || Type(type, IL2CPP_TYPE_GENERICINST) || Type(type, IL2CPP_TYPE_OBJECT) - || Type(type, IL2CPP_TYPE_STRING); -} - -inline bool Instance(const MethodInfo* method, int count, int result) -{ - return method && method->methodPointer && method->invoker_method && !(method->flags & METHOD_ATTRIBUTE_STATIC) - && method->parameters_count == count && Type(method->return_type, result) - && !method->has_full_generic_sharing_signature; -} - // The method and argument ABI must already be established by the caller. // IL2CPP takes references directly, but value/byref arguments as addresses. // A null target is valid for static methods. Success is independent of whether @@ -48,7 +26,10 @@ inline bool TryInvoke(const MethodInfo* method, void* target, void** args, Il2Cp inline bool TryBoolean(Il2CppObject* boxed, bool& value) { - if (!boxed || !boxed->klass || !Type(il2cpp_class_get_type(boxed->klass), IL2CPP_TYPE_BOOLEAN)) + if (!boxed || !boxed->klass) + return false; + const auto* type = il2cpp_class_get_type(boxed->klass); + if (!type || type->byref || type->type != IL2CPP_TYPE_BOOLEAN) return false; auto* data = static_cast(il2cpp_object_unbox(boxed)); if (!data) diff --git a/mods/src/patches/parts/loading_screen_common.h b/mods/src/patches/parts/loading_screen_common.h index 071b63976..4f78890c7 100644 --- a/mods/src/patches/parts/loading_screen_common.h +++ b/mods/src/patches/parts/loading_screen_common.h @@ -3,7 +3,7 @@ #include "config.h" #include "errormsg.h" -#include +#include #include #include @@ -28,9 +28,8 @@ struct FakeColor { float r, g, b, a; }; inline Il2CppObject* InvokeRuntime(const MethodInfo* method, void* target, void** args, const char* name) { if (!method) return nullptr; - Il2CppException* exception = nullptr; - Il2CppObject* result = il2cpp_runtime_invoke(method, target, args, &exception); - if (exception) { + Il2CppObject* result = nullptr; + if (!Il2CppRuntime::TryInvoke(method, target, args, &result)) { spdlog::warn("[LS] {} invocation failed", name); return nullptr; } @@ -40,9 +39,7 @@ inline Il2CppObject* InvokeRuntime(const MethodInfo* method, void* target, void* inline bool InvokeVoid(const MethodInfo* method, void* target, void** args, const char* name) { if (!method) return false; - Il2CppException* exception = nullptr; - il2cpp_runtime_invoke(method, target, args, &exception); - if (exception) { + if (!Il2CppRuntime::TryInvoke(method, target, args)) { spdlog::warn("[LS] {} invocation failed", name); return false; } @@ -52,9 +49,8 @@ inline bool InvokeVoid(const MethodInfo* method, void* target, void** args, cons inline bool InvokeBool(const MethodInfo* method, void* target, void** args, const char* name) { Il2CppObject* result = InvokeRuntime(method, target, args, name); - if (!result) return false; - void* value = il2cpp_object_unbox(result); - return value ? *reinterpret_cast(value) : false; + bool value = false; + return Il2CppRuntime::TryBoolean(result, value) && value; } inline int32_t InvokeInt32(const MethodInfo* method, void* target, int32_t fallback, const char* name) diff --git a/tests/il2cpp_runtime.cc b/tests/il2cpp_runtime.cc index 1b7113340..b098a2bbc 100644 --- a/tests/il2cpp_runtime.cc +++ b/tests/il2cpp_runtime.cc @@ -15,8 +15,8 @@ Il2CppClass klass; Il2CppType type{}; Il2CppObject object{}; MethodInfo method{}; -int stage = 3, calls = 0; -bool fail = false, boxed = false, null_result = false; +int calls = 0; +bool fail = false, boxed = false, null_result = false, null_type = false, null_unbox = false; void* seen_target = nullptr; void** seen_args = nullptr; } // namespace @@ -27,42 +27,11 @@ void** seen_args = nullptr; #define API(ret, name, params) name##_t name = +[] params->ret #define END_API ; #endif -API(Il2CppDomain*, il2cpp_domain_get, ()) -{ return stage >= 0 ? reinterpret_cast(&object) : nullptr; } -END_API -API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain * domain, const char*)) -{ - if (!domain) - std::abort(); - return stage >= 1 ? reinterpret_cast(&object) : nullptr; -} -END_API -API(const Il2CppImage*, il2cpp_assembly_get_image, (const Il2CppAssembly* assembly)) -{ - if (!assembly) - std::abort(); - return stage >= 2 ? reinterpret_cast(&object) : nullptr; -} -END_API -API(Il2CppClass*, il2cpp_class_from_name, (const Il2CppImage* image, const char*, const char*)) -{ - if (!image) - std::abort(); - return stage >= 3 ? &klass : nullptr; -} -END_API -API(const MethodInfo*, il2cpp_class_get_method_from_name, (Il2CppClass * cls, const char*, int)) -{ - if (!cls) - std::abort(); - return &method; -} -END_API API(const Il2CppType*, il2cpp_class_get_type, (Il2CppClass*)) -{ return &type; } +{ return null_type ? nullptr : &type; } END_API API(void*, il2cpp_object_unbox, (Il2CppObject*)) -{ return &boxed; } +{ return null_unbox ? nullptr : &boxed; } END_API API(Il2CppObject*, il2cpp_runtime_invoke, (const MethodInfo*, void* target, void** args, Il2CppException** error)) { @@ -81,12 +50,6 @@ void Require(bool condition) } int main() { - for (stage = -1; stage < 3; ++stage) - Require(!Il2CppRuntime::Class("Assembly", "Namespace", "Class")); - Require(Il2CppRuntime::Class("Assembly", "Namespace", "Class") == &klass); - Require(!Il2CppRuntime::Method(nullptr, "Method", 0)); - Require(Il2CppRuntime::Method(&klass, "Method", 0) == &method); - Il2CppObject* result = &object; Require(!Il2CppRuntime::TryInvoke(nullptr, nullptr, nullptr, &result) && calls == 0 && result == &object); bool value = true; @@ -108,20 +71,17 @@ int main() type.type = IL2CPP_TYPE_I4; Require(!Il2CppRuntime::TryBoolean(&object, value) && value); Require(!Il2CppRuntime::TryBoolean(nullptr, value)); - type.type = IL2CPP_TYPE_CLASS; - Require(Il2CppRuntime::Reference(&type)); + type.type = IL2CPP_TYPE_BOOLEAN; type.byref = true; - Require(!Il2CppRuntime::Reference(&type)); - type.byref = false; - type.type = IL2CPP_TYPE_VOID; - method.methodPointer = reinterpret_cast(1); - method.invoker_method = reinterpret_cast(1); - method.return_type = &type; - Require(Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); - method.flags = METHOD_ATTRIBUTE_STATIC; - Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); - method.flags = 0; - method.has_full_generic_sharing_signature = true; - Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + type.byref = false; + null_type = true; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + null_type = false; + null_unbox = true; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + null_unbox = false; + object.klass = nullptr; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); std::cout << "IL2CPP runtime helper regressions passed\n"; } From d6c10b7d0d0988d4438c488cfa9354524d820106 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 21 Sep 2026 22:28:55 -0500 Subject: [PATCH 07/11] Guard existing IL2CPP class lookup when metadata is unavailable --- .github/workflows/ci.yaml | 12 ++++++ mods/src/il2cpp/il2cpp_helper.h | 6 +-- tests/il2cpp_class_lookup.cc | 71 +++++++++++++++++++++++++++++++++ xmake.lua | 11 +++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 tests/il2cpp_class_lookup.cc diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 61cf42826..025c4356b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -189,6 +189,13 @@ jobs: shell: pwsh run: sccache --zero-stats + - name: Test IL2CPP class lookup + run: | + xmake build -y il2cpp-class-lookup-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + xmake run il2cpp-class-lookup-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Build run: xmake build -y stfc-community-mod @@ -469,6 +476,11 @@ jobs: shell: bash run: sccache --zero-stats + - name: Test IL2CPP class lookup + run: | + xmake build -y il2cpp-class-lookup-tests + xmake run il2cpp-class-lookup-tests + - name: Build # -D keeps the existing XMake compiler-cache diagnostics too. run: xmake -y -D diff --git a/mods/src/il2cpp/il2cpp_helper.h b/mods/src/il2cpp/il2cpp_helper.h index 3d4ff6e3f..19708b2cd 100644 --- a/mods/src/il2cpp/il2cpp_helper.h +++ b/mods/src/il2cpp/il2cpp_helper.h @@ -337,10 +337,10 @@ class IL2CppClassHelper inline IL2CppClassHelper il2cpp_get_class_helper_impl(const char* assembly, const char* namespacez, const char* name) { auto domain = il2cpp_domain_get(); - auto assemblyT = il2cpp_domain_assembly_open(domain, assembly); - auto image = il2cpp_assembly_get_image(assemblyT); + auto assemblyT = domain ? il2cpp_domain_assembly_open(domain, assembly) : nullptr; + auto image = assemblyT ? il2cpp_assembly_get_image(assemblyT) : nullptr; - auto cls = il2cpp_class_from_name(image, namespacez, name); + auto cls = image ? il2cpp_class_from_name(image, namespacez, name) : nullptr; return IL2CppClassHelper{cls}; } diff --git a/tests/il2cpp_class_lookup.cc b/tests/il2cpp_class_lookup.cc new file mode 100644 index 000000000..0d471ea00 --- /dev/null +++ b/tests/il2cpp_class_lookup.cc @@ -0,0 +1,71 @@ +// Production helpers with a controlled IL2CPP boundary, on Windows and macOS. +#include +#if _WIN32 +#undef IL2CPP_IMPORT +#define IL2CPP_IMPORT +#endif +#include "il2cpp/il2cpp_helper.h" +#include +#include +#include + +namespace +{ +Il2CppClass klass; +Il2CppObject object{}; +MethodInfo method{}; +int stage = 3; +} // namespace +#if _WIN32 +#define API(ret, name, params) extern "C" ret name params +#define END_API +#else +#define API(ret, name, params) name##_t name = +[] params->ret +#define END_API ; +#endif +API(Il2CppDomain*, il2cpp_domain_get, ()) +{ return stage >= 0 ? reinterpret_cast(&object) : nullptr; } +END_API +API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain * domain, const char*)) +{ + if (!domain) + std::abort(); + return stage >= 1 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(const Il2CppImage*, il2cpp_assembly_get_image, (const Il2CppAssembly* assembly)) +{ + if (!assembly) + std::abort(); + return stage >= 2 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(Il2CppClass*, il2cpp_class_from_name, (const Il2CppImage* image, const char*, const char*)) +{ + if (!image) + std::abort(); + return stage >= 3 ? &klass : nullptr; +} +END_API +API(const MethodInfo*, il2cpp_class_get_method_from_name, (Il2CppClass * cls, const char*, int)) +{ + if (!cls) + std::abort(); + return &method; +} +END_API +void Require(bool condition) +{ + if (!condition) + throw std::runtime_error("class lookup regression"); +} +int main() +{ + for (stage = -1; stage < 3; ++stage) + Require(!il2cpp_get_class_helper("Assembly", "Namespace", "Class").get_cls()); + Require(il2cpp_get_class_helper("Assembly", "Namespace", "Class").get_cls() == &klass); + Require(!IL2CppClassHelper(nullptr).GetMethodInfo("Method", 0)); + Require(IL2CppClassHelper(&klass).GetMethodInfo("Method", 0) == &method); + + std::cout << "IL2CPP class lookup regressions passed\n"; +} diff --git a/xmake.lua b/xmake.lua index 9b25acbfb..cfbefafff 100644 --- a/xmake.lua +++ b/xmake.lua @@ -26,3 +26,14 @@ add_rules("mode.releasedbg") includes("xmake/rules/protobuf_sccache.lua") includes("xmake/rules/cxx_sccache.lua") includes("mods") + +target("il2cpp-class-lookup-tests") + set_kind("binary") + set_default(false) + add_files("tests/il2cpp_class_lookup.cc") + add_includedirs("mods/src") + add_packages("libil2cpp", "eastl") + set_exceptions("cxx") + if is_plat("windows") then + add_linkdirs("mods/src/il2cpp") + end From 5ecf29323d1a22a5e55c6beea6549a32dedc83c2 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 1 Oct 2026 22:09:20 -0500 Subject: [PATCH 08/11] Install fleet observers independently and read current notification preferences --- example_community_patch_settings_en.toml | 2 + mods/src/config.cc | 7 +-- mods/src/defaultconfig.h | 1 + mods/src/notification_desktop_mac.mm | 3 + mods/src/patches/fleet_watch.cc | 6 ++ mods/src/patches/notification_service.cc | 8 ++- mods/src/patches/parts/fleet_notifications.cc | 60 ++++++++++--------- 7 files changed, 51 insertions(+), 36 deletions(-) diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 5d1c72e56..fed836ccd 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -282,6 +282,8 @@ loadingtiphooks = true doubleclickassignshiphooks = true forbiddentechconfirmhooks = true audioeventhooks = true +# Install fleet observers independently of the desktop/audio selections. +fleetnotificationhooks = true freeresizehooks = true game_version = true giftsbulkclaimhooks = true diff --git a/mods/src/config.cc b/mods/src/config.cc index 501b83809..7f8921efb 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -968,6 +968,8 @@ void Config::Load() get_config_or_default(config, parsed, "patches", "doubleclickassignshiphooks", DCP::doubleclickassignshiphooks, write_config); this->installForbiddenTechConfirmationHooks = get_config_or_default(config, parsed, "patches", "forbiddentechconfirmhooks", DCP::forbiddentechconfirmhooks, write_config); + this->installFleetNotificationHooks = + get_config_or_default(config, parsed, "patches", "fleetnotificationhooks", DCP::fleetnotificationhooks, write_config); this->installAudioEventHooks = get_config_or_default(config, parsed, "patches", "audioeventhooks", DCP::audioeventhooks, write_config); this->installInstantCargoCounterHooks = @@ -1347,11 +1349,6 @@ void Config::Load() spdlog::debug("Final fleet notification events: {}", fleet_events_string); parsed["ui"].as_table()->insert_or_assign("notify_fleet_events", fleet_events_string); -#if _WIN32 || __APPLE__ - this->installFleetNotificationHooks = (this->notify_fleet_events | this->audio_fleet_events) != 0; -#else - this->installFleetNotificationHooks = false; -#endif spdlog::debug(""); diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index 3b427ba20..263930300 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -100,6 +100,7 @@ namespace Patches constexpr bool doubleclickassignshiphooks = true; constexpr bool forbiddentechconfirmhooks = true; constexpr bool audioeventhooks = true; + constexpr bool fleetnotificationhooks = true; constexpr bool instantcargocounterhooks = true; constexpr bool cargoformathooks = true; // on by default: cargo number precision override constexpr bool officersorthooks = true; // restore Below Deck Ability sort option diff --git a/mods/src/notification_desktop_mac.mm b/mods/src/notification_desktop_mac.mm index 63f8e3514..019f5afe8 100644 --- a/mods/src/notification_desktop_mac.mm +++ b/mods/src/notification_desktop_mac.mm @@ -3,6 +3,7 @@ #import #import #include +#include namespace { @@ -22,6 +23,8 @@ void notification_desktop_mac_init() { + static std::atomic_bool requested{false}; + if (requested.exchange(true)) return; dispatch_async(dispatch_get_main_queue(), ^{ @autoreleasepool { UNUserNotificationCenter* center = Center(); diff --git a/mods/src/patches/fleet_watch.cc b/mods/src/patches/fleet_watch.cc index fbda37e57..7af1de8b0 100644 --- a/mods/src/patches/fleet_watch.cc +++ b/mods/src/patches/fleet_watch.cc @@ -429,6 +429,12 @@ void Tick() if (!slot.occupied || slot.fast_poll_started_ms == 0) { continue; } + // A subscriber preference can change without a fleet state transition. + if (!needs_fast_poll(slot.state)) { + slot.fast_poll_started_ms = 0; + --s_fast_poll_count; + continue; + } const auto lifetime = now_ms - slot.fast_poll_started_ms; if (lifetime >= kFastPollLifetimeMs) { slot.fast_poll_started_ms = 0; diff --git a/mods/src/patches/notification_service.cc b/mods/src/patches/notification_service.cc index dfece590b..11b355b2c 100644 --- a/mods/src/patches/notification_service.cc +++ b/mods/src/patches/notification_service.cc @@ -461,6 +461,11 @@ ScopedToastNotificationSuppression::~ScopedToastNotificationSuppression() void notification_init() { +#if __APPLE__ + // Desktop enablement can follow audio-only/all-off initialization. + if (!Config::Get().notify_banner_types.empty() || Config::Get().notify_fleet_events != 0) + notification_desktop_mac_init(); +#endif if (s_initialized) { return; } @@ -529,8 +534,6 @@ void notification_init() spdlog::warn("[Notify] Windows notification service failed (unknown error)"); } #elif __APPLE__ - if (!Config::Get().notify_banner_types.empty() || Config::Get().notify_fleet_events != 0) - notification_desktop_mac_init(); spdlog::info("[Notify] macOS notification service ready"); #else spdlog::info("[Notify] Notification service: platform not supported (no-op)"); @@ -544,6 +547,7 @@ void notification_emit(std::string_view title, std::string_view body) const std::string owned_body{body}; show_system_notification(owned_title.c_str(), owned_body.c_str()); #elif __APPLE__ + notification_desktop_mac_init(); notification_desktop_mac_emit(title, body); #else (void)title; diff --git a/mods/src/patches/parts/fleet_notifications.cc b/mods/src/patches/parts/fleet_notifications.cc index cd357eefb..8f4f5bab5 100644 --- a/mods/src/patches/parts/fleet_notifications.cc +++ b/mods/src/patches/parts/fleet_notifications.cc @@ -98,14 +98,21 @@ static_assert(docked(FleetState::Impulsing, FleetState::Docked)); static_assert(!docked(FleetState::Repairing, FleetState::Docked)); static_assert(repair_complete(FleetState::Repairing, FleetState::Docked)); -FleetNotificationMask s_enabled_notifications = 0; -FleetNotificationMask s_enabled_events = 0; - bool notification_enabled(FleetNotificationKind kind) -{ return (s_enabled_notifications & fleet_notification_bit(kind)) != 0; } +{ return (Config::Get().notify_fleet_events & fleet_notification_bit(kind)) != 0; } + +FleetNotificationMask enabled_events() +{ + const auto& config = Config::Get(); + auto enabled = config.notify_fleet_events; + for (const auto& entry : kFleetNotificationCatalog) + if (config.alert_fleet_events[static_cast(entry.kind)] != NotificationSound::None) + enabled |= fleet_notification_bit(entry.kind); + return enabled; +} bool event_enabled(FleetNotificationKind kind) -{ return (s_enabled_events & fleet_notification_bit(kind)) != 0; } +{ return (enabled_events() & fleet_notification_bit(kind)) != 0; } void play_event_audio(FleetNotificationKind kind) { @@ -136,13 +143,14 @@ std::array s_miner_opc{}; void observe_miner_opc(const fleet_watch::Snapshot& snapshot, FleetPlayerData* fleet, bool publish) { - if (!event_enabled(FleetNotificationKind::MinerOpc) || snapshot.slot < 0 || snapshot.slot >= kFleetSlotCount) { + if (snapshot.slot < 0 || snapshot.slot >= kFleetSlotCount) { return; } const bool mining = snapshot.state == FleetState::Mining; const auto cargo = mining ? read_fleet_opc_sample(fleet, snapshot.slot, snapshot.fleet_id, snapshot.state) : FleetOpcCargo{}; - if (!s_miner_opc[snapshot.slot].Observe(snapshot.fleet_id, mining, cargo.known, cargo.opc, publish)) { + if (!s_miner_opc[snapshot.slot].Observe(snapshot.fleet_id, mining, cargo.known, cargo.opc, + publish && event_enabled(FleetNotificationKind::MinerOpc))) { return; } spdlog::debug("[FleetNotifications] event=MinerOPC slot={} fleet={}", snapshot.slot, snapshot.fleet_id); @@ -167,12 +175,13 @@ std::array s_arrivals{}; void emit_transition(const fleet_watch::Transition& transition) { const auto slot = transition.after.slot; - if (event_enabled(FleetNotificationKind::ArrivedInSystem) && slot >= 0 && slot < kFleetSlotCount) { + if (slot >= 0 && slot < kFleetSlotCount) { const bool native_warp = transition.after.state == FleetState::Impulsing && transition.fleet && transition.fleet->PreviousState == FleetState::Warping; if (s_arrivals[slot].Observe(transition.after.fleet_id, transition.observation_epoch, arrival_phase(transition.before.state), arrival_phase(transition.after.state), - native_warp)) { + native_warp) + && event_enabled(FleetNotificationKind::ArrivedInSystem)) { play_event_audio(FleetNotificationKind::ArrivedInSystem); if (notification_enabled(FleetNotificationKind::ArrivedInSystem)) notification_emit("Fleet Arrived", "Your " + fleet_subject(transition.fleet) + " has arrived in-system"); @@ -223,7 +232,12 @@ constexpr bool needs_fast_poll(FleetNotificationMask enabled, FleetState state) } bool needs_enabled_fast_poll(FleetState state) -{ return needs_fast_poll(s_enabled_events, state); } +{ + // Recheck desktop authorization independently of one-time metadata setup. + // This existing observation path notices later desktop enablement without a new poller. + notification_init(); + return needs_fast_poll(enabled_events(), state); +} static_assert(needs_fast_poll(fleet_notification_bit(FleetNotificationKind::ArrivedInSystem), FleetState::Warping)); static_assert(!needs_fast_poll(fleet_notification_bit(FleetNotificationKind::ArrivedInSystem), FleetState::Impulsing)); @@ -280,6 +294,8 @@ void ToastFleetObserver_HandleMiningDepleted_Hook(auto original, void* self, int } else { original(self, fleet_id); } + if (!event_enabled(FleetNotificationKind::NodeDepleted)) + return; const auto id = static_cast(fleet_id); if (!allow_node_depletion(id)) { return; @@ -303,14 +319,10 @@ bool install_node_depletion_hook() ErrorMsg::MissingHelper("HUD", "ToastFleetObserver"); return false; } -#if __APPLE__ const auto* metadata = method_contract::Resolve(helper.get_cls(), "HandleMiningDepleted", false, "System.Void", {"System.Int64"}); auto* method = method_contract::Pointer(metadata); -#else - auto* method = helper.GetMethod("HandleMiningDepleted", 1); -#endif - if (!method) { + if (!method || metadata->has_full_generic_sharing_signature) { ErrorMsg::MissingMethod("ToastFleetObserver", "HandleMiningDepleted"); return false; } @@ -321,22 +333,12 @@ bool install_node_depletion_hook() void InstallFleetNotificationHooks() { #if _WIN32 || __APPLE__ - s_enabled_notifications = Config::Get().notify_fleet_events; -#endif -#if _WIN32 || __APPLE__ - s_enabled_events = s_enabled_notifications | Config::Get().audio_fleet_events; notification_init(); - - // OPC uses the existing round-robin observations (~2.5s per slot); no additional mining poll or detour. - constexpr auto observed_events = - kAllFleetNotifications & ~fleet_notification_bit(FleetNotificationKind::NodeDepleted); - if ((s_enabled_events & observed_events) != 0 - && !fleet_watch::Subscribe({emit_transition, needs_enabled_fast_poll, - event_enabled(FleetNotificationKind::MinerOpc) ? observe_miner_opc : nullptr})) { + // Continuous quiet observation prevents replay when a feature is enabled later. + // OPC keeps the existing round-robin cadence; no additional mining poll or detour. + if (!fleet_watch::Subscribe({emit_transition, needs_enabled_fast_poll, observe_miner_opc})) spdlog::warn("[FleetNotifications] Fleet Watch subscription failed"); - } - if (event_enabled(FleetNotificationKind::NodeDepleted) && !install_node_depletion_hook()) { + if (!install_node_depletion_hook()) spdlog::warn("[FleetNotifications] node-depletion hook installation failed"); - } #endif } From 4b0d57ef3fa2a3685edede62efc86360e9249e86 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 1 Oct 2026 22:18:16 -0500 Subject: [PATCH 09/11] Match fleet event tokens by ASCII bytes and remove stale audio mask cache --- mods/src/config.cc | 9 ++------- mods/src/config.h | 1 - mods/src/patches/fleet_notification_types.h | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/mods/src/config.cc b/mods/src/config.cc index 7f8921efb..b552cd6f3 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -1091,14 +1091,10 @@ void Config::Load() if (!this->installToastBannerHooks && any_toast_audio_alert_configured) { spdlog::warn("audio alerts require patches.toastbannerhooks = true"); } - this->audio_fleet_events = 0; for (const auto& entry : kFleetNotificationCatalog) { const auto sound = get_notification_sound(config, parsed, entry.audio_config_name, DCA::alert_fleet_default, write_config); this->alert_fleet_events[static_cast(entry.kind)] = sound; - if (sound != NotificationSound::None) { - this->audio_fleet_events |= fleet_notification_bit(entry.kind); - } } 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); @@ -1321,13 +1317,12 @@ void Config::Load() if (trimmed.empty()) { continue; } - const auto normalized = AsciiStrToUpper(trimmed); - if (normalized == "ALL") { + if (fleet_notification_name_matches(trimmed, "All")) { this->notify_fleet_events = kAllFleetNotifications; break; } const auto match = std::ranges::find_if(kFleetNotificationCatalog, [&](const auto& entry) { - return normalized == AsciiStrToUpper(entry.config_name); + return fleet_notification_name_matches(trimmed, entry.config_name); }); if (match == kFleetNotificationCatalog.end()) { spdlog::warn("Unknown fleet notification event '{}'; ignoring it", trimmed); diff --git a/mods/src/config.h b/mods/src/config.h index 6d4c021f0..44e0597c4 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -203,7 +203,6 @@ class Config final std::vector disabled_banner_types; std::vector notify_banner_types; FleetNotificationMask notify_fleet_events = 0; - FleetNotificationMask audio_fleet_events = 0; std::array alert_fleet_events{}; int extend_chest_purchase_max; diff --git a/mods/src/patches/fleet_notification_types.h b/mods/src/patches/fleet_notification_types.h index 192f1d56b..8e43d492d 100644 --- a/mods/src/patches/fleet_notification_types.h +++ b/mods/src/patches/fleet_notification_types.h @@ -36,6 +36,21 @@ inline constexpr std::array kFleetNotificationCatalog{ static_assert(kFleetNotificationCatalog.size() == static_cast(FleetNotificationKind::Count)); +// Match ASCII configuration tokens without locale, signed ctype input or NUL truncation. +constexpr bool fleet_notification_name_matches(std::string_view value, std::string_view expected) +{ + if (value.size() != expected.size()) return false; + const auto fold = [](unsigned char c) { return c >= 'a' && c <= 'z' ? c - ('a' - 'A') : c; }; + for (std::size_t i = 0; i < value.size(); ++i) + if (fold(static_cast(value[i])) != fold(static_cast(expected[i]))) + return false; + return true; +} +static_assert(fleet_notification_name_matches("mInErOpc", "MinerOPC")); +static_assert(fleet_notification_name_matches(std::string_view{"AllSuffix", 3}, "All")); +static_assert(!fleet_notification_name_matches(std::string_view{"All\0suffix", 10}, "All")); +static_assert(!fleet_notification_name_matches("\xC3\xA9", "All")); + constexpr FleetNotificationMask fleet_notification_bit(FleetNotificationKind kind) { return FleetNotificationMask{1} << static_cast(kind); } From 47ade00b143e31a48b36e2cb1eb97ea8fc2ece66 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 1 Oct 2026 22:24:11 -0500 Subject: [PATCH 10/11] Separate OPC hook readiness from preferences and clear disabled overlays --- example_community_patch_settings_en.toml | 2 + mods/src/config.cc | 3 +- mods/src/defaultconfig.h | 1 + mods/src/patches/parts/opc_indicators.cc | 247 ++++++++++------------- 4 files changed, 108 insertions(+), 145 deletions(-) diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index a8c34a81f..e48ad3ea8 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -284,6 +284,8 @@ forbiddentechconfirmhooks = true audioeventhooks = true # Install fleet observers independently of the desktop/audio selections. fleetnotificationhooks = true +# Install OPC indicator hooks independently of highlight and ETA preferences. +opcindicatorhooks = true freeresizehooks = true game_version = true giftsbulkclaimhooks = true diff --git a/mods/src/config.cc b/mods/src/config.cc index d6c36dc95..56063842d 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -968,6 +968,8 @@ void Config::Load() get_config_or_default(config, parsed, "patches", "doubleclickassignshiphooks", DCP::doubleclickassignshiphooks, write_config); this->installForbiddenTechConfirmationHooks = get_config_or_default(config, parsed, "patches", "forbiddentechconfirmhooks", DCP::forbiddentechconfirmhooks, write_config); + this->installOpcIndicatorHooks = + get_config_or_default(config, parsed, "patches", "opcindicatorhooks", DCP::opcindicatorhooks, write_config); this->installFleetNotificationHooks = get_config_or_default(config, parsed, "patches", "fleetnotificationhooks", DCP::fleetnotificationhooks, write_config); this->installAudioEventHooks = @@ -1102,7 +1104,6 @@ void Config::Load() DCU::highlight_opc_fleets, write_config); this->fleet_hud_opc_eta = get_config_or_default(config, parsed, "ui", "fleet_hud_opc_eta", DCU::fleet_hud_opc_eta, write_config); - this->installOpcIndicatorHooks = this->highlight_opc_fleets || this->fleet_hud_opc_eta; read_daily_bulk_claim_factions(config, parsed, this->daily_bulk_claim_factions, DCU::daily_bulk_claim_factions, write_config); diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index 6525b37cc..2e24ba2d2 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -101,6 +101,7 @@ namespace Patches constexpr bool forbiddentechconfirmhooks = true; constexpr bool audioeventhooks = true; constexpr bool fleetnotificationhooks = true; + constexpr bool opcindicatorhooks = true; constexpr bool instantcargocounterhooks = true; constexpr bool cargoformathooks = true; // on by default: cargo number precision override constexpr bool officersorthooks = true; // restore Below Deck Ability sort option diff --git a/mods/src/patches/parts/opc_indicators.cc b/mods/src/patches/parts/opc_indicators.cc index ccf617b6d..b2e123cd3 100644 --- a/mods/src/patches/parts/opc_indicators.cc +++ b/mods/src/patches/parts/opc_indicators.cc @@ -1,3 +1,4 @@ +#include "il2cpp/method_contract.h" #include #include "config.h" #include "errormsg.h" @@ -76,9 +77,13 @@ std::array s_opc_highlight_setup_failures{}; std::array s_opc_highlight_retry_at_ms{}; std::array s_opc_highlight_retry_fleet_ids{}; std::array s_opc_highlight_retry_anchor_ids{}; -bool s_highlight_enabled = false; -bool s_eta_enabled = false; +bool s_highlight_ready = false; +bool s_eta_ready = false; OpcCardRenderState s_opc_card_render_state{}; +FieldInfo* s_state_context_field = nullptr; +FieldInfo* s_flag_context_field = nullptr; +const MethodInfo* s_local_get_fleet = nullptr; +const MethodInfo* s_is_index_selected = nullptr; struct Color { float r; @@ -273,14 +278,11 @@ bool invoke_void(const MethodInfo* method, void* target, void** args, const char return true; } -const MethodInfo* resolve_instance_void(IL2CppClassHelper& helper, const char* name, int parameter_count) +const MethodInfo* resolve_instance_void(IL2CppClassHelper& helper, const char* name, + std::initializer_list parameters = {}) { - auto* method = helper.GetMethodInfoSpecial( - name, [parameter_count](int count, const Il2CppType**) { return count == parameter_count; }); - return method && !(method->flags & METHOD_ATTRIBUTE_STATIC) && method->methodPointer && method->return_type - && method->return_type->type == IL2CPP_TYPE_VOID - ? method - : nullptr; + const auto* method = method_contract::Resolve(helper.get_cls(), name, false, "System.Void", parameters); + return method && !method->has_full_generic_sharing_signature ? method : nullptr; } void destroy_game_object(GameObject* game_object) @@ -383,7 +385,9 @@ void* fleet_panel_controller(void* component) if (!fleet_bar || !is_instance_class_field(panel_field, "Digit.Prime.Ships", "FleetLocalViewController")) { return nullptr; } - return *reinterpret_cast(reinterpret_cast(fleet_bar) + panel_field->offset); + Il2CppObject* panel = nullptr; + il2cpp_field_get_value(static_cast(fleet_bar), panel_field, &panel); + return panel; } Transform* fleet_panel_timer_anchor(void* component) @@ -401,8 +405,8 @@ Transform* fleet_panel_timer_anchor(void* component) if (!is_instance_class_field(timer_widget_field, "Digit.Client.UI", "TimerWidget")) { return nullptr; } - auto* timer_widget = - *reinterpret_cast(reinterpret_cast(fleet_panel) + timer_widget_field->offset); + Il2CppObject* timer_widget = nullptr; + il2cpp_field_get_value(static_cast(fleet_panel), timer_widget_field, &timer_widget); return component_transform(timer_widget); } @@ -592,6 +596,15 @@ void update_opc_highlight(Transform* body_transform, FleetPlayerData* fleet) auto* highlight = find_opc_highlight(body_transform); const auto slot = fleet ? fleet->Index : -1; + if (!Config::Get().highlight_opc_fleets) { + if (highlight) highlight->SetActive(false); + if (slot >= 0 && slot < kFleetSlotCount) { + s_opc_highlight_retry_fleet_ids[slot] = 0; + s_opc_highlight_retry_anchor_ids[slot] = 0; + clear_ui_retry(s_opc_highlight_setup_failures[slot], s_opc_highlight_retry_at_ms[slot]); + } + return; + } if (!fleet || slot < 0 || slot >= kFleetSlotCount) { if (highlight) { highlight->SetActive(false); @@ -647,9 +660,11 @@ bool fleet_tile_is_selected(void* fleet_state_widget, FleetPlayerData* fleet) static auto fleet_bar_helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.HUD", "FleetBarViewController"); - static auto is_index_selected = fleet_bar_helper.GetMethod("IsIndexSelected", 1); - auto* fleet_bar = component_in_parent(fleet_state_widget, fleet_bar_helper); - return fleet_bar && is_index_selected && is_index_selected(fleet_bar, fleet->Index); + auto* fleet_bar = component_in_parent(fleet_state_widget, fleet_bar_helper); + auto index = fleet->Index; + void* args[]{&index}; + auto* result = fleet_bar ? invoke(s_is_index_selected, fleet_bar, args, "FleetBar.IsIndexSelected") : nullptr; + return result && *static_cast(il2cpp_object_unbox(result)); } bool configure_opc_eta_label(void* label, Transform* transform, bool selected, bool safe_on_node) @@ -1149,6 +1164,12 @@ void update_opc_eta_label(void* ui_component, FleetPlayerData* fleet, Transform* { auto* label_anchor = known_label_anchor ? known_label_anchor : fleet_state_widget_label_anchor(ui_component); const bool panel_component = ui_component && fleet_panel_controller(ui_component) == ui_component; + if (!Config::Get().fleet_hud_opc_eta) { + hide_opc_eta(label_anchor); + update_opc_card_label(ui_component, nullptr, {}); + reset_opc_eta_slot(fleet ? fleet->Index : -1); + return; + } if (!ui_component || !fleet) { hide_opc_eta(label_anchor); if (panel_component) { @@ -1183,7 +1204,7 @@ void update_opc_eta_label(void* ui_component, FleetPlayerData* fleet, Transform* auto safe_on_node = render.computed_safe; if (refresh_due) { const auto status = read_opc_status(fleet, slot, render.fleet_id, fleet_state); - display = Config::Get().fleet_hud_opc_eta ? format_opc_eta(status) : std::string{}; + display = format_opc_eta(status); card_display = display.empty() ? std::string{} : format_opc_card_display(status); safe_on_node = status.safe_on_node; log_opc_eta(fleet, status, display); @@ -1291,49 +1312,30 @@ void update_opc_eta_label(void* ui_component, FleetPlayerData* fleet, Transform* label_object->SetActive(true); } -FleetPlayerData* fleet_state_widget_context(void* self) +FleetPlayerData* widget_context(void* self, FieldInfo* field) { - if (!self) { - return nullptr; - } - - static auto helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.HUD", "FleetStateWidget"); - static auto get_context = helper.GetMethod("get_Context", 0); - return get_context ? get_context(self) : nullptr; + FleetPlayerData* fleet = nullptr; + if (self && field) + il2cpp_field_get_value(static_cast(self), field, &fleet); + return fleet; } - +FleetPlayerData* fleet_state_widget_context(void* self) +{ return widget_context(self, s_state_context_field); } FleetPlayerData* fleetbar_flag_widget_context(void* self) -{ - if (!self) { - return nullptr; - } - - static auto helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.HUD", "FleetbarFlagWidget"); - static auto get_context = helper.GetMethod("get_Context", 0); - return get_context ? get_context(self) : nullptr; -} - +{ return widget_context(self, s_flag_context_field); } FleetPlayerData* fleet_local_view_fleet(void* self) -{ - if (!self) { - return nullptr; - } - - static auto helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.Ships", "FleetLocalViewController"); - static auto get_fleet = helper.GetMethod("get_fleet", 0); - return get_fleet ? get_fleet(self) : nullptr; -} +{ return self ? reinterpret_cast(invoke(s_local_get_fleet, self, nullptr, "FleetLocal.get_fleet")) : nullptr; } void FleetStateWidget_SetWidgetData_Hook(auto original, void* self) { original(self); - if (s_eta_enabled) + if (s_eta_ready) update_opc_eta_label(self, fleet_state_widget_context(self)); } void FleetStateWidget_ClearWidgetData_Hook(auto original, void* self) { - if (!s_eta_enabled) { + if (!s_eta_ready) { original(self); return; } @@ -1356,13 +1358,13 @@ void FleetStateWidget_ClearWidgetData_Hook(auto original, void* self) void FleetbarFlagWidget_SetWidgetData_Hook(auto original, void* self) { original(self); - if (s_highlight_enabled) + if (s_highlight_ready) update_opc_highlight(opc_anchor_from_fleetbar_flag(self), fleetbar_flag_widget_context(self)); } void FleetbarFlagWidget_ClearWidgetData_Hook(auto original, void* self) { - if (!s_highlight_enabled) { + if (!s_highlight_ready) { original(self); return; } @@ -1385,15 +1387,16 @@ void FleetbarFlagWidget_ClearWidgetData_Hook(auto original, void* self) void FleetLocalViewController_BindDataContext_Hook(auto original, void* self, void* provider, void* data_context) { original(self, provider, data_context); + if (!s_eta_ready && !s_highlight_ready) return; auto* tile_transform = component_transform(self); auto* label_anchor = opc_anchor_from_tile(tile_transform); auto* fleet = fleet_local_view_fleet(self); - if (s_highlight_enabled) { + if (s_highlight_ready) { update_opc_highlight(label_anchor, fleet); } const bool panel_component = fleet_panel_controller(self) == self; - if (s_eta_enabled && (label_anchor || panel_component)) { + if (s_eta_ready && (label_anchor || panel_component)) { reset_opc_eta_slot(fleet ? fleet->Index : -1); update_opc_eta_label(self, fleet, label_anchor); } @@ -1402,13 +1405,14 @@ void FleetLocalViewController_BindDataContext_Hook(auto original, void* self, vo void FleetLocalViewController_OnCurrentCargoReactiveEvent_Hook(auto original, void* self, int32_t dirty_flags) { original(self, dirty_flags); + if (!s_eta_ready && !s_highlight_ready) return; auto* tile_transform = component_transform(self); auto* fleet = fleet_local_view_fleet(self); invalidate_fleet_opc_sample(fleet ? fleet->Index : -1); - if (s_highlight_enabled) { + if (s_highlight_ready) { update_opc_highlight(opc_anchor_from_tile(tile_transform), fleet); } - if (s_eta_enabled) { + if (s_eta_ready) { update_opc_eta_label(self, fleet, opc_anchor_from_tile(tile_transform)); } } @@ -1417,109 +1421,64 @@ void FleetLocalViewController_OnCurrentCargoReactiveEvent_Hook(auto original, vo void InstallOpcIndicatorHooks() { - const bool use_opc_highlight = Config::Get().highlight_opc_fleets; - const bool use_opc_eta = Config::Get().fleet_hud_opc_eta; #if !defined(_WIN32) && !defined(__APPLE__) - if (use_opc_highlight || use_opc_eta) { - spdlog::warn("[OpcIndicators] disabled: unsupported platform"); - } + spdlog::warn("[OpcIndicators] unavailable: unsupported platform"); return; #endif - - if (!use_opc_highlight && !use_opc_eta) { + auto local = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.Ships", "FleetLocalViewController"); + auto state = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.HUD", "FleetStateWidget"); + auto flag = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.HUD", "FleetbarFlagWidget"); + auto bar = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.HUD", "FleetBarViewController"); + const auto* bind = resolve_instance_void(local, "BindDataContext", + {"Digit.Client.UI.IDataContextProvider", "System.Object"}); + const auto* cargo = resolve_instance_void(local, "OnCurrentCargoReactiveEvent", {"System.Int32"}); + const auto* state_set = resolve_instance_void(state, "SetWidgetData"); + const auto* state_clear = resolve_instance_void(state, "ClearWidgetData"); + const auto* flag_set = resolve_instance_void(flag, "SetWidgetData"); + const auto* flag_clear = resolve_instance_void(flag, "ClearWidgetData"); + s_local_get_fleet = method_contract::Resolve(local.get_cls(), "get_fleet", false, + "Digit.PrimeServer.Models.FleetPlayerData", {}); + s_is_index_selected = method_contract::Resolve(bar.get_cls(), "IsIndexSelected", false, + "System.Boolean", {"System.Int32"}); + s_state_context_field = state.get_cls() ? il2cpp_class_get_field_from_name(state.get_cls(), "m_context") : nullptr; + s_flag_context_field = flag.get_cls() ? il2cpp_class_get_field_from_name(flag.get_cls(), "m_context") : nullptr; + const bool local_ready = bind && cargo && s_local_get_fleet && !s_local_get_fleet->has_full_generic_sharing_signature; + const bool eta_ready = local_ready && state_set && state_clear && s_is_index_selected + && !s_is_index_selected->has_full_generic_sharing_signature + && is_instance_class_field(s_state_context_field, "Digit.PrimeServer.Models", "FleetPlayerData"); + const bool highlight_ready = local_ready && flag_set && flag_clear + && is_instance_class_field(s_flag_context_field, "Digit.PrimeServer.Models", "FleetPlayerData"); + if (!eta_ready && !highlight_ready) { + spdlog::warn("[OpcIndicators] unavailable: required method/context contracts"); return; } - - auto fleet_local_helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.Ships", "FleetLocalViewController"); - const auto* bind_data_context = - fleet_local_helper.isValidHelper() ? resolve_instance_void(fleet_local_helper, "BindDataContext", 2) : nullptr; - const auto* cargo_updated = fleet_local_helper.isValidHelper() - ? resolve_instance_void(fleet_local_helper, "OnCurrentCargoReactiveEvent", 1) - : nullptr; - if (!fleet_local_helper.isValidHelper()) { - ErrorMsg::MissingHelper("Ships", "FleetLocalViewController"); - } else { - if (!bind_data_context) { - ErrorMsg::MissingMethod("FleetLocalViewController", "BindDataContext"); - } - if (!cargo_updated) { - ErrorMsg::MissingMethod("FleetLocalViewController", "OnCurrentCargoReactiveEvent"); - } - } - const bool local_ready = bind_data_context && cargo_updated; - - const MethodInfo* state_set = nullptr; - const MethodInfo* state_clear = nullptr; - if (use_opc_eta) { - auto state_helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.HUD", "FleetStateWidget"); - if (!state_helper.isValidHelper()) { - ErrorMsg::MissingHelper("HUD", "FleetStateWidget"); - } else { - state_set = resolve_instance_void(state_helper, "SetWidgetData", 0); - state_clear = resolve_instance_void(state_helper, "ClearWidgetData", 0); - if (!state_set) { - ErrorMsg::MissingMethod("FleetStateWidget", "SetWidgetData"); - } - if (!state_clear) { - ErrorMsg::MissingMethod("FleetStateWidget", "ClearWidgetData"); - } - } - } - - const MethodInfo* flag_set = nullptr; - const MethodInfo* flag_clear = nullptr; - if (use_opc_highlight) { - auto flag_helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.HUD", "FleetbarFlagWidget"); - if (!flag_helper.isValidHelper()) { - ErrorMsg::MissingHelper("HUD", "FleetbarFlagWidget"); - } else { - flag_set = resolve_instance_void(flag_helper, "SetWidgetData", 0); - flag_clear = resolve_instance_void(flag_helper, "ClearWidgetData", 0); - if (!flag_set) { - ErrorMsg::MissingMethod("FleetbarFlagWidget", "SetWidgetData"); - } - if (!flag_clear) { - ErrorMsg::MissingMethod("FleetbarFlagWidget", "ClearWidgetData"); - } - } - } - -#if __APPLE__ - // Resolve distinct targets before installing the requested hooks. - std::vector targets{bind_data_context, cargo_updated}; - if (use_opc_eta) { targets.push_back(state_set); targets.push_back(state_clear); } - if (use_opc_highlight) { targets.push_back(flag_set); targets.push_back(flag_clear); } - for (size_t i = 0; i < targets.size(); ++i) { - if (!targets[i] || !targets[i]->methodPointer) { - spdlog::warn("[OpcIndicators] disabled: missing Mac hook target"); - return; - } + std::vector targets{bind, cargo}; + if (eta_ready) { targets.push_back(state_set); targets.push_back(state_clear); } + if (highlight_ready) { targets.push_back(flag_set); targets.push_back(flag_clear); } + for (size_t i = 0; i < targets.size(); ++i) for (size_t j = 0; j < i; ++j) if (targets[i]->methodPointer == targets[j]->methodPointer) { - spdlog::warn("[OpcIndicators] disabled: shared native hook target"); + spdlog::warn("[OpcIndicators] unavailable: shared native hook target"); return; } - } -#endif - - const bool eta_ready = use_opc_eta && local_ready && state_set && state_clear; - const bool highlight_ready = use_opc_highlight && local_ready && flag_set && flag_clear; bool installed = true; if (eta_ready) { - installed = SPUD_STATIC_DETOUR(state_clear->methodPointer, FleetStateWidget_ClearWidgetData_Hook) - && SPUD_STATIC_DETOUR(state_set->methodPointer, FleetStateWidget_SetWidgetData_Hook); + const bool a = SPUD_STATIC_DETOUR(state_clear->methodPointer, FleetStateWidget_ClearWidgetData_Hook) != nullptr; + const bool b = SPUD_STATIC_DETOUR(state_set->methodPointer, FleetStateWidget_SetWidgetData_Hook) != nullptr; + installed = a && b; } if (installed && highlight_ready) { - installed = SPUD_STATIC_DETOUR(flag_clear->methodPointer, FleetbarFlagWidget_ClearWidgetData_Hook) - && SPUD_STATIC_DETOUR(flag_set->methodPointer, FleetbarFlagWidget_SetWidgetData_Hook); - } - if (installed && (eta_ready || highlight_ready)) { - installed = SPUD_STATIC_DETOUR(bind_data_context->methodPointer, FleetLocalViewController_BindDataContext_Hook) - && SPUD_STATIC_DETOUR(cargo_updated->methodPointer, FleetLocalViewController_OnCurrentCargoReactiveEvent_Hook); - } - // Partial installations stay on the original path; never retry them on macOS. - s_eta_enabled = installed && eta_ready; - s_highlight_enabled = installed && highlight_ready; - if (!installed) - spdlog::warn("[OpcIndicators] disabled: native hook installation failed"); + const bool a = SPUD_STATIC_DETOUR(flag_clear->methodPointer, FleetbarFlagWidget_ClearWidgetData_Hook) != nullptr; + const bool b = SPUD_STATIC_DETOUR(flag_set->methodPointer, FleetbarFlagWidget_SetWidgetData_Hook) != nullptr; + installed = a && b; + } + if (installed) { + const bool a = SPUD_STATIC_DETOUR(bind->methodPointer, FleetLocalViewController_BindDataContext_Hook) != nullptr; + const bool b = SPUD_STATIC_DETOUR(cargo->methodPointer, FleetLocalViewController_OnCurrentCargoReactiveEvent_Hook) != nullptr; + installed = a && b; + } + // A partial installation remains native; do not retry the same targets. + s_eta_ready = installed && eta_ready; + s_highlight_ready = installed && highlight_ready; + spdlog::info("[OpcIndicators] eta_ready={} highlight_ready={}", s_eta_ready, s_highlight_ready); } From 0927907e66e544476408c08a4d92aa24c2a09f97 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 1 Oct 2026 22:32:18 -0500 Subject: [PATCH 11/11] Read actual mining-slot interface amounts for node-limited OPC estimates --- mods/src/patches/parts/opc_indicators.cc | 4 +- mods/src/prime/MiningSlot.h | 70 ++++++++++++------------ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/mods/src/patches/parts/opc_indicators.cc b/mods/src/patches/parts/opc_indicators.cc index b2e123cd3..b04a2f155 100644 --- a/mods/src/patches/parts/opc_indicators.cc +++ b/mods/src/patches/parts/opc_indicators.cc @@ -162,8 +162,8 @@ FleetOpcStatus read_opc_status(FleetPlayerData* fleet, int slot, uint64_t fleet_ status.rate_known = std::isfinite(status.rate_per_second) && status.rate_per_second > 0.0; if (mining_data) { - const auto node_current = mining_data->CurrentValue; - const auto node_max = mining_data->MaxValue; + const auto node_current = mining_data->AmountMined; + const auto node_max = static_cast(mining_data->Amount); status.node_known = std::isfinite(node_current) && std::isfinite(node_max) && node_current >= 0.0 && node_max >= node_current; if (status.node_known) { diff --git a/mods/src/prime/MiningSlot.h b/mods/src/prime/MiningSlot.h index bf8d1b08b..0a880619d 100644 --- a/mods/src/prime/MiningSlot.h +++ b/mods/src/prime/MiningSlot.h @@ -1,51 +1,51 @@ #pragma once #include - +#include +#include #include #include +// FleetPlayerData.MiningData is IMiningSlot; resolve its actual implementation. +// Both MiningSlot and FleetMiningSlot use the same validated numeric contracts. struct MiningSlot { -public: - __declspec(property(get = __get_ResourceId)) int64_t ResourceId; - __declspec(property(get = __get_MiningSpeed)) float MiningSpeed; - __declspec(property(get = __get_CurrentValue)) double CurrentValue; - __declspec(property(get = __get_MaxValue)) double MaxValue; + __declspec(property(get = __get_ResourceId)) int64_t ResourceId; + __declspec(property(get = __get_MiningSpeed)) float MiningSpeed; + __declspec(property(get = __get_AmountMined)) double AmountMined; + __declspec(property(get = __get_Amount)) int64_t Amount; private: - static IL2CppClassHelper& get_class_helper() + template T Read(const char* name, const char* explicit_name, const char* type, T unavailable) { - static auto class_helper = - il2cpp_get_class_helper("Digit.Client.PrimeLib.Runtime", "Digit.PrimeServer.Models", "MiningSlot"); - return class_helper; + auto* object = reinterpret_cast(this); + auto* cls = il2cpp_object_get_class(object); + static auto slot_interface = il2cpp_get_class_helper( + "Digit.Client.PrimeLib.Runtime", "Digit.PrimeServer.Models", "IMiningSlot"); + if (!cls || !slot_interface.get_cls() || !il2cpp_class_is_assignable_from(slot_interface.get_cls(), cls)) + return unavailable; + const MethodInfo* method = nullptr; + for (auto* owner = cls; owner && !method; owner = il2cpp_class_get_parent(owner)) { + const auto* public_getter = method_contract::Resolve(owner, name, false, type, {}); + const auto* explicit_getter = method_contract::Resolve(owner, explicit_name, false, type, {}); + if (public_getter && explicit_getter) return unavailable; + method = public_getter ? public_getter : explicit_getter; + } + Il2CppObject* value = nullptr; + if (!method || method->has_full_generic_sharing_signature + || !Il2CppRuntime::TryInvoke(method, object, nullptr, &value) || !value + || il2cpp_object_get_class(value) != il2cpp_class_from_type(method->return_type)) + return unavailable; + return *static_cast(il2cpp_object_unbox(value)); } public: int64_t __get_ResourceId() - { - static auto property = get_class_helper().GetProperty("ResourceId"); - auto* value = property.Get(this); - return value ? *value : 0; - } - + { return Read("get_ResourceId", "Digit.PrimeServer.Models.IMiningSlot.get_ResourceId", "System.Int64", 0); } float __get_MiningSpeed() - { - static auto property = get_class_helper().GetProperty("MiningSpeed"); - auto* value = property.Get(this); - return value ? *value : 0; - } - - double __get_CurrentValue() - { - static auto property = get_class_helper().GetProperty("CurrentValue"); - auto* value = property.Get(this); - return value ? *value : std::numeric_limits::quiet_NaN(); - } - - double __get_MaxValue() - { - static auto property = get_class_helper().GetProperty("MaxValue"); - auto* value = property.Get(this); - return value ? *value : std::numeric_limits::quiet_NaN(); - } + { return Read("get_MiningSpeed", "Digit.PrimeServer.Models.IMiningSlot.get_MiningSpeed", "System.Single", 0.0f); } + double __get_AmountMined() + { return Read("get_AmountMined", "Digit.PrimeServer.Models.IMiningSlot.get_AmountMined", "System.Double", + std::numeric_limits::quiet_NaN()); } + int64_t __get_Amount() + { return Read("get_Amount", "Digit.PrimeServer.Models.IMiningSlot.get_Amount", "System.Int64", -1); } };