diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c4db85207..61cf42826 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,11 +17,11 @@ concurrency: cancel-in-progress: true env: - XMAKE_VERSION: "3.1.0" + XMAKE_VERSION: "3.1.1" # Bump this when a dependency cache must be invalidated manually. XMAKE_CACHE_SCHEMA: "v4" # Narrow protobuf-only compiler-cache experiment. - PROTOBUF_SCCACHE_VERSION: "v0.17.0" + PROTOBUF_SCCACHE_VERSION: "v0.18.0" PROTOBUF_SCCACHE_SCHEMA: "v1" STFC_PROTOBUF_SCCACHE: "1" # Keep sccache local; actions/cache owns persistence explicitly. @@ -323,7 +323,7 @@ jobs: runs-on: ${{ matrix.runner }} env: - MACOSX_DEPLOYMENT_TARGET: "13.5" + MACOSX_DEPLOYMENT_TARGET: "14.6" SWIFT_MODULE_CACHE_SCHEMA: "v1" SCCACHE_DIR: ${{ github.workspace }}/build/.protobuf-sccache SCCACHE_CACHE_SIZE: "1G" @@ -739,18 +739,18 @@ jobs: shell: bash run: | set -euo pipefail - + APP="build/macosx/arm64/release/STFC Community Mod.app" - + check_linkage() { local binary="$1" local deps - + echo echo "==> $binary" deps="$(otool -L "$binary")" printf '%s\n' "$deps" - + if printf '%s\n' "$deps" | grep -E \ '/opt/homebrew/|/usr/local/(opt|Cellar)/|(^|/)libssl(\.[0-9]+)*\.dylib|(^|/)libcrypto(\.[0-9]+)*\.dylib|(^|/)libcurl(\.[0-9]+)*\.dylib' then @@ -758,7 +758,7 @@ jobs: return 1 fi } - + check_linkage "$APP/Contents/MacOS/macOSLauncher" check_linkage "$APP/Contents/stfc-community-mod-loader" check_linkage "$APP/Contents/libstfc-community-mod.dylib" diff --git a/AGENTS.md b/AGENTS.md index 507b3d8f9..cdd3d4c3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ This project uses **XMake** (not CMake). All build configuration is in `xmake.lu ```bash # Configure and build (command line) xmake # Build default target -xmake f -p macosx -a arm64 -m debug --target_minver=13.5 # Configure for macOS ARM debug +xmake f -p macosx -a arm64 -m debug --target_minver=14.6 # Configure for macOS ARM debug xmake f -p windows -m release # Configure for Windows release # Generate Visual Studio solution @@ -40,7 +40,7 @@ Delete the `build/` folder to reset. Also delete `.vs/` for a full Visual Studio - Keep changes scoped. Do not stage unrelated dirty files or generated artifacts unless the user explicitly asks. - Before finishing C++ or patch work, run `git diff --check` and the narrowest relevant xmake build. -- For macOS core mod changes, use `xmake f -p macosx -a arm64 -m debug --target_minver=13.5 -y && xmake -y mods`. +- For macOS core mod changes, use `xmake f -p macosx -a arm64 -m debug --target_minver=14.6 -y && xmake -y mods`. - Review the final diff for risky hooks, platform guards, config default mismatches, and missing example config updates. - If a subtree such as `macos-launcher/` needs specialized guidance, prefer a nested `AGENTS.md` near that code instead of overloading this root file. diff --git a/assets/LoadingScreens/M95 Loading Screen.png b/assets/LoadingScreens/M95 Loading Screen.png new file mode 100644 index 000000000..6766c1761 Binary files /dev/null and b/assets/LoadingScreens/M95 Loading Screen.png differ diff --git a/assets/loadingscreen.png b/assets/loadingscreen.png index 052f22596..6766c1761 100644 Binary files a/assets/loadingscreen.png and b/assets/loadingscreen.png differ diff --git a/docs/macos-fleet-audio-test.md b/docs/macos-fleet-audio-test.md new file mode 100644 index 000000000..33ddc7a1b --- /dev/null +++ b/docs/macos-fleet-audio-test.md @@ -0,0 +1,50 @@ +# Focused macOS fleet-audio baseline + +`testing/macos-fleet-audio` combines upstream 1.1.8.0 (`4bf238c1`) with: + +- Cross-platform notification audio (#253, `7933965b`). +- The named-event `All` audio filter (#259, `cfe2227a`). +- Fleet-transition notifications (#268 consumer commits `d5c8a010`, `49d8bc75`). +- Independent generated fleet audio (`ae7e64bc`). +- Xcode launcher build compatibility (#291, `bfa22bbd`), including the macOS 14.6 + deployment target and updated CI tools. + +The Fleet Watch observer already comes from upstream. This branch does not include +the rest of the personal play stack and does not yet change arrival detection. +The component revisions above are pinned; later feeder-branch refreshes do not +automatically change this test build. + +## Testing + +Requires macOS 14.6 or newer. Use the universal macOS installer or mod archive from this branch's successful +Build run. Back up the existing mod and TOML first, close the game before replacing +the mod, and retain the exact run URL with the results. Do not switch builds during +a comparison. Verify the startup log identifies the installed test build. + +First test with the same settings that showed the problem. Record the relevant +`[audio]` settings and whether it is the generated cue or the original game sound +that goes missing. `All` affects the named Fabric event route, not every game audio +path; generated mod cues remain independent. + +For an isolated generated-arrival test, edit these keys inside the existing +`[audio]` section (do not append a duplicate section): + +```toml +disabled_events = "All" +alert_fleet_arrived_in_system = "arrival" +alert_fleet_arrived_at_destination = "none" +``` + +Temporarily set other `alert_*` keys to `"none"` if isolating overlapping sounds. +Fleet cues do not require desktop notifications or toast banner hooks. There is no +generated departure cue in this baseline. + +Compare short and long warps, one fleet and several fleets, viewed and off-screen +destinations, and a focused versus background game window. For a missed cue, record +the approximate time, fleet, destination, warp duration, visible fleet state and +whether another alert played. Keep the session log. `trace_events = true` logs named +game audio events only; it does not trace generated fleet transitions. + +Restore the previous mod and configuration to undo the test. Build success is not +macOS runtime validation: the purpose of this baseline is to establish a repeatable +case before stacking a separate arrival correction. diff --git a/example_community_patch_settings_da.toml b/example_community_patch_settings_da.toml index c60c5c39f..86a1978d1 100644 --- a/example_community_patch_settings_da.toml +++ b/example_community_patch_settings_da.toml @@ -7,7 +7,7 @@ # | #*** # # #*** # * #** #*** | # | | # +--------------------------------------------------------------+ -# Oprettet den 2026-09-05T13:04:20.428Z +# Oprettet den 2026-09-17T11:10:54.288Z # # Denne konfigurationsfil blev oprettet af https://modconfig.pages.dev # og leveres, som den er. Hvis der er problemer med STFC Community Mod @@ -17,7 +17,7 @@ # Discord : https://discord.gg/PrpHgs7Vjs # GitHub : https://github.com/netniV/stfc-mod # -# Dagens konfigurationsfil præsenteres af bogstavet A og tallet 6 +# Dagens konfigurationsfil præsenteres af bogstavet I og tallet 6 # # <[=======================================]> @@ -31,12 +31,30 @@ # <[=======================================]> [audio] -# Comma-separated audio event names to suppress; leave blank to allow all audio events +# Comma-separated audio event names to suppress; use "All" as a wildcard for named events handled by this filter disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + +# Fleet cues are independent of desktop notifications and toastbannerhooks. +# Use the sound names above; "none" disables a cue. Windows and macOS. +alert_fleet_arrived_in_system = "none" +alert_fleet_arrived_at_destination = "none" +alert_fleet_started_mining = "none" +alert_fleet_node_depleted = "none" +alert_fleet_docked = "none" +alert_fleet_repair_complete = "none" + # <[======================================================================================================]> # # ***** * * * @@ -403,6 +421,9 @@ set_hotkeys_disabled = "CTRL-ALT-MINUS" # CTRL-ALT-= set_hotkeys_enabled = "CTRL-ALT-=" +# Vis eller skjul genvejstip +toggle_shortcut_hints = "HOME" + # Subgroup: Queue & Primary # ------------------------- @@ -457,6 +478,9 @@ show_artifacts = "SHIFT-I" # Vis udehold show_awayteam = "T" +# Åbn indbakken med kamprapporter +show_battlelogs = "SHIFT-B" + # Vis bogmærker show_bookmarks = "B" @@ -496,6 +520,9 @@ show_galaxy_native = "CTRL-G" # Gå til gaver show_gifts = "/" +# Åbn Haven +show_haven = "ALT-H" + # Gå til beholdning show_inventory = "I" @@ -520,23 +547,14 @@ show_scrapyard = "Y" # Gå til indstillinger show_settings = "SHIFT-S" -# Slå genvejstips på skærmen til/fra (viser mod-genveje, hvor de understøttes) -# NONE deaktiverer mod-tiphooks og tekstcache; tildel en tast for at aktivere (kræver genstart). -toggle_shortcut_hints = "HOME" - -# Symboler i mærker bruger AutoHotkey-notation: ^ Ctrl, ! Alt, + Shift, # Win/Command - -# Gå til administration af skibe -show_ships = "N" +# Åbn valget af fredsskjolde +show_shields = "CTRL-S" # Åbn skibsværftets Byg skib-skærm show_shipconstruction = "SHIFT-N" -# Åbn valget af fredsskjolde -show_shields = "CTRL-S" - -# Åbn indbakken med kamprapporter -show_battlelogs = "SHIFT-B" +# Gå til administration af skibe +show_ships = "N" # Stationens ydre show_stationexterior = "SHIFT-G" @@ -682,6 +700,10 @@ disabled_banner_types = "" # Specifies banner types that should trigger notifications (comma separated) notify_banner_types = "" +# Fleet events that should trigger Windows notifications (comma separated, or "All") +# Options: ArrivedInSystem, ArrivedAtDestination, StartedMining, NodeDepleted, Docked, RepairComplete +notify_fleet_events = "" + # Subgroup: Chat # -------------- @@ -866,7 +888,7 @@ url = "" # ----------------- # Hvor længe cachede elementer skal bevares (ms) -resolver_cache_ttl = "300" +resolver_cache_ttl = 300 # Skal SSL-certifikater kontrolleres? verify_ssl = true diff --git a/example_community_patch_settings_de.toml b/example_community_patch_settings_de.toml index 6756ed409..324cf0a38 100644 --- a/example_community_patch_settings_de.toml +++ b/example_community_patch_settings_de.toml @@ -7,7 +7,7 @@ # | #*** # # #*** # * #** #*** | # | | # +--------------------------------------------------------------+ -# Erstellt am 2026-09-05T13:04:20.385Z +# Erstellt am 2026-09-17T11:10:54.237Z # # Diese Konfigurationsdatei wurde von https://modconfig.pages.dev erstellt # und wird ohne Gewähr bereitgestellt. Bei Problemen mit dem STFC Community Mod, @@ -17,7 +17,7 @@ # Discord : https://discord.gg/PrpHgs7Vjs # GitHub : https://github.com/netniV/stfc-mod # -# Die heutige Konfigurationsdatei wird Ihnen präsentiert vom Buchstaben I und der Zahl 2 +# Die heutige Konfigurationsdatei wird Ihnen präsentiert vom Buchstaben S und der Zahl 9 # # <[=======================================]> @@ -31,12 +31,30 @@ # <[=======================================]> [audio] -# Comma-separated audio event names to suppress; leave blank to allow all audio events +# Comma-separated audio event names to suppress; use "All" as a wildcard for named events handled by this filter disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + +# Fleet cues are independent of desktop notifications and toastbannerhooks. +# Use the sound names above; "none" disables a cue. Windows and macOS. +alert_fleet_arrived_in_system = "none" +alert_fleet_arrived_at_destination = "none" +alert_fleet_started_mining = "none" +alert_fleet_node_depleted = "none" +alert_fleet_docked = "none" +alert_fleet_repair_complete = "none" + # <[=======================================]> # # **** ** ** @@ -403,6 +421,9 @@ set_hotkeys_disabled = "CTRL-ALT-MINUS" # STRG-ALT-= set_hotkeys_enabled = "CTRL-ALT-=" +# Tastenkürzel-Hinweise ein- oder ausblenden +toggle_shortcut_hints = "HOME" + # Subgroup: Warteschlange und Primäraktion # ---------------------------------------- @@ -457,6 +478,9 @@ show_artifacts = "SHIFT-I" # Außenteam anzeigen show_awayteam = "T" +# Den Posteingang für Kampfberichte öffnen +show_battlelogs = "SHIFT-B" + # Lesezeichen anzeigen show_bookmarks = "B" @@ -496,6 +520,9 @@ show_galaxy_native = "CTRL-G" # Zu den Geschenken show_gifts = "/" +# Haven öffnen +show_haven = "ALT-H" + # Zum Inventar show_inventory = "I" @@ -520,22 +547,14 @@ show_scrapyard = "Y" # Zu den Einstellungen show_settings = "SHIFT-S" -# Bildschirmhinweise für Tastenkürzel umschalten (zeigt unterstützte Mod-Tastenkürzel) -# NONE deaktiviert Mod-Hinweis-Hooks und den Textcache; zum Aktivieren eine Taste zuweisen (Neustart erforderlich). -toggle_shortcut_hints = "HOME" -# Hinweissymbole verwenden die AutoHotkey-Schreibweise: ^ Strg, ! Alt, + Umschalt, # Win/Befehl - -# Zur Schiffsverwaltung -show_ships = "N" +# Die Auswahl für Friedensschilde öffnen +show_shields = "CTRL-S" # Den Bildschirm Schiff bauen der Werft öffnen show_shipconstruction = "SHIFT-N" -# Die Auswahl für Friedensschilde öffnen -show_shields = "CTRL-S" - -# Den Posteingang für Kampfberichte öffnen -show_battlelogs = "SHIFT-B" +# Zur Schiffsverwaltung +show_ships = "N" # Stationsaußenansicht show_stationexterior = "SHIFT-G" @@ -681,6 +700,10 @@ disabled_banner_types = "" # Specifies banner types that should trigger notifications (comma separated) notify_banner_types = "" +# Fleet events that should trigger Windows notifications (comma separated, or "All") +# Options: ArrivedInSystem, ArrivedAtDestination, StartedMining, NodeDepleted, Docked, RepairComplete +notify_fleet_events = "" + # Subgroup: Chat # -------------- @@ -865,7 +888,7 @@ url = "" # ------------------ # Speicherdauer zwischengespeicherter Einträge (ms) -resolver_cache_ttl = "300" +resolver_cache_ttl = 300 # Sollen SSL-Zertifikate überprüft werden? verify_ssl = true diff --git a/example_community_patch_settings_en-GB-x-cockney.toml b/example_community_patch_settings_en-GB-x-cockney.toml index ded80a9ef..793492718 100644 --- a/example_community_patch_settings_en-GB-x-cockney.toml +++ b/example_community_patch_settings_en-GB-x-cockney.toml @@ -7,7 +7,7 @@ # | #*** # # #*** # * #** #*** | # | | # +--------------------------------------------------------------+ -# Knocked up on 2026-09-05T13:04:20.451Z +# Knocked up on 2026-09-17T11:10:54.322Z # # This configuration file was knocked up by https://modconfig.pages.dev # and comes as-is. If the STFC Community Mod gives you any aggro @@ -17,7 +17,7 @@ # Discord : https://discord.gg/PrpHgs7Vjs # GitHub : https://github.com/netniV/stfc-mod # -# Today's configuration comes courtesy of the letter W and the number 1 +# Today's configuration comes courtesy of the letter U and the number 8 # # <[=======================================]> @@ -31,12 +31,30 @@ # <[=======================================]> [audio] -# Comma-separated audio event names to suppress; leave blank to allow all audio events +# Comma-separated audio event names to suppress; use "All" as a wildcard for named events handled by this filter disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + +# Fleet cues are independent of desktop notifications and toastbannerhooks. +# Use the sound names above; "none" disables a cue. Windows and macOS. +alert_fleet_arrived_in_system = "none" +alert_fleet_arrived_at_destination = "none" +alert_fleet_started_mining = "none" +alert_fleet_node_depleted = "none" +alert_fleet_docked = "none" +alert_fleet_repair_complete = "none" + # <[=======================================]> # # **** ** ** @@ -403,6 +421,9 @@ set_hotkeys_disabled = "CTRL-ALT-MINUS" # CTRL-ALT-= set_hotkeys_enabled = "CTRL-ALT-=" +# Show or hide shortcut hints +toggle_shortcut_hints = "HOME" + # Subgroup: Queue & Primary # ------------------------- @@ -457,6 +478,9 @@ show_artifacts = "SHIFT-I" # Show Away Team show_awayteam = "T" +# Open the Battle Reports inbox +show_battlelogs = "SHIFT-B" + # Show Bookmarks show_bookmarks = "B" @@ -496,6 +520,9 @@ show_galaxy_native = "CTRL-G" # Go to Gifts show_gifts = "/" +# Open Haven +show_haven = "ALT-H" + # Go to Inventory show_inventory = "I" @@ -520,22 +547,14 @@ show_scrapyard = "Y" # Go to Settings show_settings = "SHIFT-S" -# Toggle on-screen shortcut hints (shows mod bindings where supported) -# NONE disables mod hint hooks and label caching; bind a key to enable (restart required). -toggle_shortcut_hints = "HOME" -# Badge modifiers use AutoHotkey notation: ^ Ctrl, ! Alt, + Shift, # Win/Command - -# Go to Manage Ships -show_ships = "N" +# Open the Peace Shield selection popup +show_shields = "CTRL-S" # Open the Shipyard Build Ship screen show_shipconstruction = "SHIFT-N" -# Open the Peace Shield selection popup -show_shields = "CTRL-S" - -# Open the Battle Reports inbox -show_battlelogs = "SHIFT-B" +# Go to Manage Ships +show_ships = "N" # Station Exterior show_stationexterior = "SHIFT-G" @@ -681,6 +700,10 @@ disabled_banner_types = "" # Specifies banner types that should trigger notifications (comma separated) notify_banner_types = "" +# Fleet events that should trigger Windows notifications (comma separated, or "All") +# Options: ArrivedInSystem, ArrivedAtDestination, StartedMining, NodeDepleted, Docked, RepairComplete +notify_fleet_events = "" + # Subgroup: Chat # -------------- @@ -865,7 +888,7 @@ url = "" # ----------------- # How long to keep cached items (ms) -resolver_cache_ttl = "300" +resolver_cache_ttl = 300 # Should SSL Certificates be checked? verify_ssl = true diff --git a/example_community_patch_settings_en-x-minionese.toml b/example_community_patch_settings_en-x-minionese.toml index 78b129750..82a683598 100644 --- a/example_community_patch_settings_en-x-minionese.toml +++ b/example_community_patch_settings_en-x-minionese.toml @@ -7,7 +7,7 @@ # | #*** # # #*** # * #** #*** | # | | # +--------------------------------------------------------------+ -# Generated on 2026-09-05T13:04:20.443Z +# Generated on 2026-09-17T11:10:54.313Z # # This configuration file was generated by https://modconfig.pages.dev # and is provided as-is. If there are any issues with the STFC Community Mod @@ -17,7 +17,7 @@ # Discord : https://discord.gg/PrpHgs7Vjs # GitHub : https://github.com/netniV/stfc-mod # -# Today's configuration file is brought to you by the letter F and the number 6 +# Today's configuration file is brought to you by the letter I and the number 3 # # <[=======================================]> @@ -31,12 +31,30 @@ # <[=======================================]> [audio] -# Comma-separated audio event names to suppress; leave blank to allow all audio events +# Comma-separated audio event names to suppress; use "All" as a wildcard for named events handled by this filter disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + +# Fleet cues are independent of desktop notifications and toastbannerhooks. +# Use the sound names above; "none" disables a cue. Windows and macOS. +alert_fleet_arrived_in_system = "none" +alert_fleet_arrived_at_destination = "none" +alert_fleet_started_mining = "none" +alert_fleet_node_depleted = "none" +alert_fleet_docked = "none" +alert_fleet_repair_complete = "none" + # <[=======================================]> # # **** ** ** @@ -403,6 +421,9 @@ set_hotkeys_disabled = "CTRL-ALT-MINUS" # CTRL-ALT-= set_hotkeys_enabled = "CTRL-ALT-=" +# Show or hide shortcut hints +toggle_shortcut_hints = "HOME" + # Subgroup: Queue & Primary # ------------------------- @@ -457,6 +478,9 @@ show_artifacts = "SHIFT-I" # Show Away Team show_awayteam = "T" +# Open the Battle Reports inbox +show_battlelogs = "SHIFT-B" + # Show Bookmarks show_bookmarks = "B" @@ -496,6 +520,9 @@ show_galaxy_native = "CTRL-G" # Go to Gifts show_gifts = "/" +# Open Haven +show_haven = "ALT-H" + # Go to Inventory show_inventory = "I" @@ -520,22 +547,14 @@ show_scrapyard = "Y" # Go to Settings show_settings = "SHIFT-S" -# Toggle on-screen shortcut hints (shows mod bindings where supported) -# NONE disables mod hint hooks and label caching; bind a key to enable (restart required). -toggle_shortcut_hints = "HOME" -# Badge modifiers use AutoHotkey notation: ^ Ctrl, ! Alt, + Shift, # Win/Command - -# Go to Manage Ships -show_ships = "N" +# Open the Peace Shield selection popup +show_shields = "CTRL-S" # Open the Shipyard Build Ship screen show_shipconstruction = "SHIFT-N" -# Open the Peace Shield selection popup -show_shields = "CTRL-S" - -# Open the Battle Reports inbox -show_battlelogs = "SHIFT-B" +# Go to Manage Ships +show_ships = "N" # Station Exterior show_stationexterior = "SHIFT-G" @@ -681,6 +700,10 @@ disabled_banner_types = "" # Specifies banner types that should trigger notifications (comma separated) notify_banner_types = "" +# Fleet events that should trigger Windows notifications (comma separated, or "All") +# Options: ArrivedInSystem, ArrivedAtDestination, StartedMining, NodeDepleted, Docked, RepairComplete +notify_fleet_events = "" + # Subgroup: Chat # -------------- @@ -865,7 +888,7 @@ url = "" # ----------------- # How long to keep cached items (ms) -resolver_cache_ttl = "300" +resolver_cache_ttl = 300 # Should SSL Certificates be checked? verify_ssl = true diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index 7706e7c95..b7f673c88 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -7,7 +7,7 @@ # | #*** # # #*** # * #** #*** | # | | # +--------------------------------------------------------------+ -# Generated on 2026-09-05T13:04:20.375Z +# Generated on 2026-09-17T11:10:54.224Z # # This configuration file was generated by https://modconfig.pages.dev # and is provided as-is. If there are any issues with the STFC Community Mod @@ -17,7 +17,7 @@ # Discord : https://discord.gg/PrpHgs7Vjs # GitHub : https://github.com/netniV/stfc-mod # -# Today's configuration file is brought to you by the letter K and the number 7 +# Today's configuration file is brought to you by the letter S and the number 6 # # <[=======================================]> @@ -31,12 +31,30 @@ # <[=======================================]> [audio] -# Comma-separated audio event names to suppress; leave blank to allow all audio events +# Comma-separated audio event names to suppress; use "All" as a wildcard for named events handled by this filter disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + +# Fleet cues are independent of desktop notifications and toastbannerhooks. +# Use the sound names above; "none" disables a cue. Windows and macOS. +alert_fleet_arrived_in_system = "none" +alert_fleet_arrived_at_destination = "none" +alert_fleet_started_mining = "none" +alert_fleet_node_depleted = "none" +alert_fleet_docked = "none" +alert_fleet_repair_complete = "none" + # <[=======================================]> # # **** ** ** @@ -403,6 +421,9 @@ set_hotkeys_disabled = "CTRL-ALT-MINUS" # CTRL-ALT-= set_hotkeys_enabled = "CTRL-ALT-=" +# Show or hide shortcut hints +toggle_shortcut_hints = "HOME" + # Subgroup: Queue & Primary # ------------------------- @@ -457,6 +478,9 @@ show_artifacts = "SHIFT-I" # Show Away Team show_awayteam = "T" +# Open the Battle Reports inbox +show_battlelogs = "SHIFT-B" + # Show Bookmarks show_bookmarks = "B" @@ -496,6 +520,9 @@ show_galaxy_native = "CTRL-G" # Go to Gifts show_gifts = "/" +# Open Haven +show_haven = "ALT-H" + # Go to Inventory show_inventory = "I" @@ -520,22 +547,14 @@ show_scrapyard = "Y" # Go to Settings show_settings = "SHIFT-S" -# Toggle on-screen shortcut hints (shows mod bindings where supported) -# NONE disables mod hint hooks and label caching; bind a key to enable (restart required). -toggle_shortcut_hints = "HOME" -# Badge modifiers use AutoHotkey notation: ^ Ctrl, ! Alt, + Shift, # Win/Command - -# Go to Manage Ships -show_ships = "N" +# Open the Peace Shield selection popup +show_shields = "CTRL-S" # Open the Shipyard Build Ship screen show_shipconstruction = "SHIFT-N" -# Open the Peace Shield selection popup -show_shields = "CTRL-S" - -# Open the Battle Reports inbox -show_battlelogs = "SHIFT-B" +# Go to Manage Ships +show_ships = "N" # Station Exterior show_stationexterior = "SHIFT-G" @@ -681,6 +700,10 @@ disabled_banner_types = "" # Specifies banner types that should trigger notifications (comma separated) notify_banner_types = "" +# Fleet events that should trigger Windows notifications (comma separated, or "All") +# Options: ArrivedInSystem, ArrivedAtDestination, StartedMining, NodeDepleted, Docked, RepairComplete +notify_fleet_events = "" + # Subgroup: Chat # -------------- @@ -865,7 +888,7 @@ url = "" # ----------------- # How long to keep cached items (ms) -resolver_cache_ttl = "300" +resolver_cache_ttl = 300 # Should SSL Certificates be checked? verify_ssl = true diff --git a/example_community_patch_settings_es.toml b/example_community_patch_settings_es.toml index 68b2bb4f7..f6bc9e94c 100644 --- a/example_community_patch_settings_es.toml +++ b/example_community_patch_settings_es.toml @@ -7,7 +7,7 @@ # | #*** # # #*** # * #** #*** | # | | # +--------------------------------------------------------------+ -# Generado el 2026-09-05T13:04:20.411Z +# Generado el 2026-09-17T11:10:54.267Z # # Este archivo de configuración fue generado por https://modconfig.pages.dev # y se proporciona tal cual. Si hay algún problema con el Mod Comunitario de STFC @@ -17,7 +17,7 @@ # Discord : https://discord.gg/PrpHgs7Vjs # GitHub : https://github.com/netniV/stfc-mod # -# El archivo de configuración de hoy está patrocinado por la letra Q y el número 8 +# El archivo de configuración de hoy está patrocinado por la letra I y el número 0 # # <[=======================================]> @@ -31,12 +31,30 @@ # <[=======================================]> [audio] -# Comma-separated audio event names to suppress; leave blank to allow all audio events +# Comma-separated audio event names to suppress; use "All" as a wildcard for named events handled by this filter disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + +# Fleet cues are independent of desktop notifications and toastbannerhooks. +# Use the sound names above; "none" disables a cue. Windows and macOS. +alert_fleet_arrived_in_system = "none" +alert_fleet_arrived_at_destination = "none" +alert_fleet_started_mining = "none" +alert_fleet_node_depleted = "none" +alert_fleet_docked = "none" +alert_fleet_repair_complete = "none" + # <[=====================================================]> # # * * * @@ -403,6 +421,9 @@ set_hotkeys_disabled = "CTRL-ALT-MINUS" # CTRL-ALT-= set_hotkeys_enabled = "CTRL-ALT-=" +# Mostrar u ocultar las pistas de atajos +toggle_shortcut_hints = "HOME" + # Subgroup: Queue & Primary # ------------------------- @@ -457,6 +478,9 @@ show_artifacts = "SHIFT-I" # Mostrar equipo visitante show_awayteam = "T" +# Abrir la bandeja de informes de batalla +show_battlelogs = "SHIFT-B" + # Mostrar marcadores show_bookmarks = "B" @@ -496,6 +520,9 @@ show_galaxy_native = "CTRL-G" # Ir a regalos show_gifts = "/" +# Abrir Haven +show_haven = "ALT-H" + # Ir al inventario show_inventory = "I" @@ -520,22 +547,14 @@ show_scrapyard = "Y" # Ir a ajustes show_settings = "SHIFT-S" -# Alternar las indicaciones de atajos en pantalla (muestra los atajos compatibles del mod) -# NONE desactiva los hooks y la caché de indicaciones del mod; asigna una tecla para activar (requiere reiniciar). -toggle_shortcut_hints = "HOME" -# Los modificadores usan la notación de AutoHotkey: ^ Ctrl, ! Alt, + Mayús, # Win/Comando - -# Ir a administrar barcos -show_ships = "N" +# Abrir la selección de escudos de paz +show_shields = "CTRL-S" # Abrir la pantalla Construir nave del astillero show_shipconstruction = "SHIFT-N" -# Abrir la selección de escudos de paz -show_shields = "CTRL-S" - -# Abrir la bandeja de informes de batalla -show_battlelogs = "SHIFT-B" +# Ir a administrar barcos +show_ships = "N" # Exterior de la estación show_stationexterior = "SHIFT-G" @@ -681,6 +700,10 @@ disabled_banner_types = "" # Specifies banner types that should trigger notifications (comma separated) notify_banner_types = "" +# Fleet events that should trigger Windows notifications (comma separated, or "All") +# Options: ArrivedInSystem, ArrivedAtDestination, StartedMining, NodeDepleted, Docked, RepairComplete +notify_fleet_events = "" + # Subgroup: Chat # -------------- @@ -865,7 +888,7 @@ url = "" # ----------------- # Cuánto tiempo se conservan los elementos en caché (ms) -resolver_cache_ttl = "300" +resolver_cache_ttl = 300 # ¿Deben comprobarse los certificados SSL? verify_ssl = true diff --git a/example_community_patch_settings_fr.toml b/example_community_patch_settings_fr.toml index dd8e462ac..58522d9fd 100644 --- a/example_community_patch_settings_fr.toml +++ b/example_community_patch_settings_fr.toml @@ -7,7 +7,7 @@ # | #*** # # #*** # * #** #*** | # | | # +--------------------------------------------------------------+ -# Généré le 2026-09-05T13:04:20.403Z +# Généré le 2026-09-17T11:10:54.258Z # # Ce fichier de configuration a été généré par https://modconfig.pages.dev # et est fourni tel quel. En cas de problème avec le STFC Community Mod, @@ -17,7 +17,7 @@ # Discord : https://discord.gg/PrpHgs7Vjs # GitHub : https://github.com/netniV/stfc-mod # -# Le fichier de configuration du jour vous est présenté par la lettre Z et le nombre 6 +# Le fichier de configuration du jour vous est présenté par la lettre A et le nombre 7 # # <[=======================================]> @@ -31,12 +31,30 @@ # <[=======================================]> [audio] -# Comma-separated audio event names to suppress; leave blank to allow all audio events +# Comma-separated audio event names to suppress; use "All" as a wildcard for named events handled by this filter disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + +# Fleet cues are independent of desktop notifications and toastbannerhooks. +# Use the sound names above; "none" disables a cue. Windows and macOS. +alert_fleet_arrived_in_system = "none" +alert_fleet_arrived_at_destination = "none" +alert_fleet_started_mining = "none" +alert_fleet_node_depleted = "none" +alert_fleet_docked = "none" +alert_fleet_repair_complete = "none" + # <[=======================================]> # # **** ** ** @@ -403,6 +421,9 @@ set_hotkeys_disabled = "CTRL-ALT-MINUS" # CTRL-ALT-= set_hotkeys_enabled = "CTRL-ALT-=" +# Afficher ou masquer les indications de raccourcis +toggle_shortcut_hints = "HOME" + # Subgroup: Queue & Primary # ------------------------- @@ -457,6 +478,9 @@ show_artifacts = "SHIFT-I" # Afficher l'équipe de détachement show_awayteam = "T" +# Ouvrir la boîte de réception des rapports de combat +show_battlelogs = "SHIFT-B" + # Afficher les favoris show_bookmarks = "B" @@ -496,6 +520,9 @@ show_galaxy_native = "CTRL-G" # Aller aux cadeaux show_gifts = "/" +# Ouvrir Haven +show_haven = "ALT-H" + # Aller à l'inventaire show_inventory = "I" @@ -520,22 +547,14 @@ show_scrapyard = "Y" # Aller aux paramètres show_settings = "SHIFT-S" -# Afficher ou masquer les raccourcis à l’écran (affiche les raccourcis du mod pris en charge) -# NONE désactive les hooks et le cache des indications du mod ; attribuez une touche pour activer (redémarrage requis). -toggle_shortcut_hints = "HOME" -# Les modificateurs utilisent la notation AutoHotkey : ^ Ctrl, ! Alt, + Maj, # Win/Commande - -# Aller aux vaisseaux -show_ships = "N" +# Ouvrir la sélection des boucliers de paix +show_shields = "CTRL-S" # Ouvrir la construction de vaisseaux du chantier spatial show_shipconstruction = "SHIFT-N" -# Ouvrir la sélection des boucliers de paix -show_shields = "CTRL-S" - -# Ouvrir la boîte de réception des rapports de combat -show_battlelogs = "SHIFT-B" +# Aller aux vaisseaux +show_ships = "N" # Extérieur de la station show_stationexterior = "SHIFT-G" @@ -681,6 +700,10 @@ disabled_banner_types = "" # Specifies banner types that should trigger notifications (comma separated) notify_banner_types = "" +# Fleet events that should trigger Windows notifications (comma separated, or "All") +# Options: ArrivedInSystem, ArrivedAtDestination, StartedMining, NodeDepleted, Docked, RepairComplete +notify_fleet_events = "" + # Subgroup: Chat # -------------- @@ -865,7 +888,7 @@ url = "" # ----------------- # Durée de conservation des éléments en cache (ms) -resolver_cache_ttl = "300" +resolver_cache_ttl = 300 # Les certificats SSL doivent-ils être vérifiés ? verify_ssl = true diff --git a/example_community_patch_settings_nl.toml b/example_community_patch_settings_nl.toml index b0726f8a9..379029439 100644 --- a/example_community_patch_settings_nl.toml +++ b/example_community_patch_settings_nl.toml @@ -7,7 +7,7 @@ # | #*** # # #*** # * #** #*** | # | | # +--------------------------------------------------------------+ -# Gegenereerd op 2026-09-05T13:04:20.394Z +# Gegenereerd op 2026-09-17T11:10:54.248Z # # Dit configuratiebestand is gegenereerd door https://modconfig.pages.dev # en wordt geleverd zoals het is. Meld problemen met de STFC Community Mod, @@ -17,7 +17,7 @@ # Discord : https://discord.gg/PrpHgs7Vjs # GitHub : https://github.com/netniV/stfc-mod # -# Het configuratiebestand van vandaag wordt u aangeboden door de letter Z en het getal 8 +# Het configuratiebestand van vandaag wordt u aangeboden door de letter U en het getal 2 # # <[=======================================]> @@ -31,12 +31,30 @@ # <[=======================================]> [audio] -# Comma-separated audio event names to suppress; leave blank to allow all audio events +# Comma-separated audio event names to suppress; use "All" as a wildcard for named events handled by this filter disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + +# Fleet cues are independent of desktop notifications and toastbannerhooks. +# Use the sound names above; "none" disables a cue. Windows and macOS. +alert_fleet_arrived_in_system = "none" +alert_fleet_arrived_at_destination = "none" +alert_fleet_started_mining = "none" +alert_fleet_node_depleted = "none" +alert_fleet_docked = "none" +alert_fleet_repair_complete = "none" + # <[=======================================]> # # **** ** ** @@ -403,6 +421,9 @@ set_hotkeys_disabled = "CTRL-ALT-MINUS" # CTRL-ALT-= set_hotkeys_enabled = "CTRL-ALT-=" +# Sneltoetstips tonen of verbergen +toggle_shortcut_hints = "HOME" + # Subgroup: Queue & Primary # ------------------------- @@ -457,6 +478,9 @@ show_artifacts = "SHIFT-I" # Toon Expeditieteam show_awayteam = "T" +# Open het postvak met gevechtsrapporten +show_battlelogs = "SHIFT-B" + # Toon Bladwijzers show_bookmarks = "B" @@ -496,6 +520,9 @@ show_galaxy_native = "CTRL-G" # Ga naar Cadeaus show_gifts = "/" +# Open Haven +show_haven = "ALT-H" + # Ga naar Inventaris show_inventory = "I" @@ -520,22 +547,14 @@ show_scrapyard = "Y" # Ga naar Instellingen show_settings = "SHIFT-S" -# Schermtips voor sneltoetsen in- of uitschakelen (toont ondersteunde mod-sneltoetsen) -# NONE schakelt mod-tip-hooks en tekstcache uit; wijs een toets toe om in te schakelen (herstart vereist). -toggle_shortcut_hints = "HOME" -# Modificatietoetsen gebruiken AutoHotkey-notatie: ^ Ctrl, ! Alt, + Shift, # Win/Command - -# Ga naar Scheepsmanagement -show_ships = "N" +# Open de selectie voor vredesschilden +show_shields = "CTRL-S" # Open het scherm Schip bouwen van de scheepswerf show_shipconstruction = "SHIFT-N" -# Open de selectie voor vredesschilden -show_shields = "CTRL-S" - -# Open het postvak met gevechtsrapporten -show_battlelogs = "SHIFT-B" +# Ga naar Scheepsmanagement +show_ships = "N" # Station Exterieur show_stationexterior = "SHIFT-G" @@ -681,6 +700,10 @@ disabled_banner_types = "" # Specifies banner types that should trigger notifications (comma separated) notify_banner_types = "" +# Fleet events that should trigger Windows notifications (comma separated, or "All") +# Options: ArrivedInSystem, ArrivedAtDestination, StartedMining, NodeDepleted, Docked, RepairComplete +notify_fleet_events = "" + # Subgroup: Chat # -------------- @@ -865,7 +888,7 @@ url = "" # ----------------- # Hoelang je items gecaches wilt hebben (ms) -resolver_cache_ttl = "300" +resolver_cache_ttl = 300 # Zouden SSL certificaten geverifieerd moeten worden? verify_ssl = true diff --git a/example_community_patch_settings_ru.toml b/example_community_patch_settings_ru.toml index 1238fb805..2cf8e97a1 100644 --- a/example_community_patch_settings_ru.toml +++ b/example_community_patch_settings_ru.toml @@ -7,7 +7,7 @@ # | #*** # # #*** # * #** #*** | # | | # +--------------------------------------------------------------+ -# Создано 2026-09-05T13:04:20.420Z +# Создано 2026-09-17T11:10:54.278Z # # Этот файл конфигурации создан на https://modconfig.pages.dev # и предоставляется как есть. Если возникли проблемы с STFC Community Mod, @@ -17,7 +17,7 @@ # Discord : https://discord.gg/PrpHgs7Vjs # GitHub : https://github.com/netniV/stfc-mod # -# Сегодняшний файл конфигурации представлен буквой G и числом 2 +# Сегодняшний файл конфигурации представлен буквой X и числом 8 # # <[=======================================]> @@ -31,12 +31,30 @@ # <[=======================================]> [audio] -# Comma-separated audio event names to suppress; leave blank to allow all audio events +# Comma-separated audio event names to suppress; use "All" as a wildcard for named events handled by this filter disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + +# Fleet cues are independent of desktop notifications and toastbannerhooks. +# Use the sound names above; "none" disables a cue. Windows and macOS. +alert_fleet_arrived_in_system = "none" +alert_fleet_arrived_at_destination = "none" +alert_fleet_started_mining = "none" +alert_fleet_node_depleted = "none" +alert_fleet_docked = "none" +alert_fleet_repair_complete = "none" + # <[============================================================]> # # @@ -403,6 +421,9 @@ set_hotkeys_disabled = "CTRL-ALT-MINUS" # CTRL-ALT-= set_hotkeys_enabled = "CTRL-ALT-=" +# Показать или скрыть подсказки горячих клавиш +toggle_shortcut_hints = "HOME" + # Subgroup: Queue & Primary # ------------------------- @@ -457,6 +478,9 @@ show_artifacts = "SHIFT-I" # Показать выездную команду show_awayteam = "T" +# Открыть входящие боевые отчёты +show_battlelogs = "SHIFT-B" + # Показать закладки show_bookmarks = "B" @@ -496,6 +520,9 @@ show_galaxy_native = "CTRL-G" # Перейти к подаркам show_gifts = "/" +# Открыть Haven +show_haven = "ALT-H" + # Перейти к инвентарю show_inventory = "I" @@ -520,22 +547,14 @@ show_scrapyard = "Y" # Перейти к настройкам show_settings = "SHIFT-S" -# Показать или скрыть подсказки клавиш (показывает поддерживаемые сочетания мода) -# NONE отключает хуки и кэш подсказок мода; назначьте клавишу для включения (требуется перезапуск). -toggle_shortcut_hints = "HOME" -# Модификаторы используют нотацию AutoHotkey: ^ Ctrl, ! Alt, + Shift, # Win/Command - -# Перейти к управлению кораблями -show_ships = "N" +# Открыть окно выбора мирного щита +show_shields = "CTRL-S" # Открыть экран постройки корабля на верфи show_shipconstruction = "SHIFT-N" -# Открыть окно выбора мирного щита -show_shields = "CTRL-S" - -# Открыть входящие боевые отчёты -show_battlelogs = "SHIFT-B" +# Перейти к управлению кораблями +show_ships = "N" # Внешний вид станции show_stationexterior = "SHIFT-G" @@ -681,6 +700,10 @@ disabled_banner_types = "" # Specifies banner types that should trigger notifications (comma separated) notify_banner_types = "" +# Fleet events that should trigger Windows notifications (comma separated, or "All") +# Options: ArrivedInSystem, ArrivedAtDestination, StartedMining, NodeDepleted, Docked, RepairComplete +notify_fleet_events = "" + # Subgroup: Chat # -------------- @@ -865,7 +888,7 @@ url = "" # ----------------- # Срок хранения элементов в кэше (мс) -resolver_cache_ttl = "300" +resolver_cache_ttl = 300 # Проверять SSL-сертификаты? verify_ssl = true diff --git a/example_community_patch_settings_tlh.toml b/example_community_patch_settings_tlh.toml index 1c45a5157..3895434df 100644 --- a/example_community_patch_settings_tlh.toml +++ b/example_community_patch_settings_tlh.toml @@ -7,7 +7,7 @@ # | #*** # # #*** # * #** #*** | # | | # +--------------------------------------------------------------+ -# tlhIngan Hol: Generated on 2026-09-05T13:04:20.435Z +# tlhIngan Hol: Generated on 2026-09-17T11:10:54.299Z # # This ghun pat ta was generated by https://modconfig.pages.dev # 'ej is provided as-is. If there are any issues tlhej STFC Community Mod @@ -17,7 +17,7 @@ # Discord : https://discord.gg/PrpHgs7Vjs # GitHub : https://github.com/netniV/stfc-mod # -# Today's ghun pat ta is brought Daq you by letter K 'ej mI' 4 +# Today's ghun pat ta is brought Daq you by letter P 'ej mI' 6 # # <[=======================================]> @@ -31,12 +31,30 @@ # <[=======================================]> [audio] -# Comma-separated audio event names to suppress; leave blank to allow all audio events +# Comma-separated audio event names to suppress; use "All" as a wildcard for named events handled by this filter disabled_events = "" # Write audio event names to the trace log for troubleshooting trace_events = false +# Optional generated notification cues; "none" disables an alert. +# Available sounds: default, info, success, warning, alarm, arrival, soft, ping, repair +# Requires patches.toastbannerhooks = true. +alert_victory = "none" +alert_defeat = "none" +alert_armada_created = "none" +alert_armada_battle_won = "none" +alert_armada_battle_lost = "none" + +# Fleet cues are independent of desktop notifications and toastbannerhooks. +# Use the sound names above; "none" disables a cue. Windows and macOS. +alert_fleet_arrived_in_system = "none" +alert_fleet_arrived_at_destination = "none" +alert_fleet_started_mining = "none" +alert_fleet_node_depleted = "none" +alert_fleet_docked = "none" +alert_fleet_repair_complete = "none" + # <[=====================================================]> # # * * **** * * @@ -403,6 +421,9 @@ set_hotkeys_disabled = "CTRL-ALT-MINUS" # CTRL-ALT-= set_hotkeys_enabled = "CTRL-ALT-=" +# shortcut hints yI'ang pagh yISo' +toggle_shortcut_hints = "HOME" + # Subgroup: Queue & Primary # ------------------------- @@ -457,6 +478,9 @@ show_artifacts = "SHIFT-I" # 'ang Away Team show_awayteam = "T" +# Open the Battle Reports inbox +show_battlelogs = "SHIFT-B" + # 'ang Bookmarks show_bookmarks = "B" @@ -496,6 +520,9 @@ show_galaxy_native = "CTRL-G" # Go Daq Gifts show_gifts = "/" +# Haven yIpoSmoH +show_haven = "ALT-H" + # Go Daq Inventory show_inventory = "I" @@ -520,22 +547,14 @@ show_scrapyard = "Y" # Go Daq ghun show_settings = "SHIFT-S" -# Toggle on-screen shortcut hints (shows mod bindings where supported) -# NONE disables mod hint hooks and label caching; bind a key to enable (restart required). -toggle_shortcut_hints = "HOME" -# Badge modifiers use AutoHotkey notation: ^ Ctrl, ! Alt, + Shift, # Win/Command - -# Go Daq Manage Ships -show_ships = "N" +# Open the Peace Shield selection popup +show_shields = "CTRL-S" # Open the Shipyard Build Ship screen show_shipconstruction = "SHIFT-N" -# Open the Peace Shield selection popup -show_shields = "CTRL-S" - -# Open the Battle Reports inbox -show_battlelogs = "SHIFT-B" +# Go Daq Manage Ships +show_ships = "N" # 'ejyo'waw' Exterior show_stationexterior = "SHIFT-G" @@ -681,6 +700,10 @@ disabled_banner_types = "" # Specifies banner types that should trigger notifications (comma separated) notify_banner_types = "" +# Fleet events that should trigger Windows notifications (comma separated, or "All") +# Options: ArrivedInSystem, ArrivedAtDestination, StartedMining, NodeDepleted, Docked, RepairComplete +notify_fleet_events = "" + # Subgroup: Chat # -------------- @@ -865,7 +888,7 @@ url = "" # ----------------- # How long Daq keep cached items (ms) -resolver_cache_ttl = "300" +resolver_cache_ttl = 300 # tlhIngan Hol: Should SSL Certificates be checked? verify_ssl = true diff --git a/macos-launcher/src/Info.plist.template b/macos-launcher/src/Info.plist.template index d2382cc6b..b5c798787 100644 --- a/macos-launcher/src/Info.plist.template +++ b/macos-launcher/src/Info.plist.template @@ -25,7 +25,7 @@ MacOSX LSMinimumSystemVersion - 13.5 + 14.6 CFBundleIconFile launcher diff --git a/macos-launcher/xmake.lua b/macos-launcher/xmake.lua index 932153aba..537d9e3ff 100644 --- a/macos-launcher/xmake.lua +++ b/macos-launcher/xmake.lua @@ -49,6 +49,14 @@ target("macOSLauncher") -- Generate Info.plist from template during configuration on_config(function (target) + -- xmake invokes swift-frontend directly, bypassing the swiftc driver that + -- normally injects macro plugin search paths. Xcode 26+ SDKs implement + -- SwiftUI property wrappers (@State etc.) as external macros, so point the + -- frontend at the platform's macro plugins explicitly. + local sdk_platform = os.iorun("xcrun --show-sdk-platform-path"):trim() + target:add("scflags", "-external-plugin-path", + sdk_platform .. "/Developer/usr/lib/swift/host/plugins#" .. sdk_platform .. "/Developer/usr/bin/swift-plugin-server") + local version_file = path.join(os.scriptdir(), "../mods/src/version.h") if not os.isfile(version_file) then -- GitHub Actions warning diff --git a/mods/src/config.cc b/mods/src/config.cc index 25086345e..84b153d6b 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -32,6 +32,36 @@ namespace DCS = DefaultConfig::Sync; namespace DCSC = DefaultConfig::SystemConfig; namespace DCSH = DefaultConfig::Shortcuts; +namespace +{ +struct ToastAudioAlertConfig { + int toast_state; + std::string_view config_name; + std::string_view default_sound; + NotificationSound Config::* config_member; +}; + +constexpr auto kToastAudioAlerts = std::to_array({ + {ToastState::Victory, "alert_victory", DCA::alert_victory, &Config::alert_victory}, + {ToastState::Defeat, "alert_defeat", DCA::alert_defeat, &Config::alert_defeat}, + {ToastState::ArmadaCreated, "alert_armada_created", DCA::alert_armada_created, &Config::alert_armada_created}, + {ToastState::ArmadaBattleWon, "alert_armada_battle_won", DCA::alert_armada_battle_won, + &Config::alert_armada_battle_won}, + {ToastState::ArmadaBattleLost, "alert_armada_battle_lost", DCA::alert_armada_battle_lost, + &Config::alert_armada_battle_lost}, +}); + +constexpr bool is_all_audio_wildcard(std::string_view value) +{ + return value.size() == 3 && (value[0] == 'a' || value[0] == 'A') && (value[1] == 'l' || value[1] == 'L') + && (value[2] == 'l' || value[2] == 'L'); +} + +static_assert(is_all_audio_wildcard("aLl")); +static_assert(!is_all_audio_wildcard(std::string_view{"All\0suffix", 10})); +static_assert(!is_all_audio_wildcard(std::string_view{"\xffll", 3})); +} // namespace + static const eastl::tuple bannerTypes[] = { {"All", ToastState::All}, {"Standard", ToastState::Standard}, @@ -128,6 +158,12 @@ Config& Config::Get() return config; } +NotificationSound Config::NotificationSoundForToast(int toast_state) const +{ + const auto alert = std::ranges::find(kToastAudioAlerts, toast_state, &ToastAudioAlertConfig::toast_state); + return alert == kToastAudioAlerts.end() ? NotificationSound::None : this->*(alert->config_member); +} + MissionHudVisibility Config::MissionHudButtonVisibility(std::string_view button_name) const { const auto it = this->mission_hud_buttons.find(std::string(button_name)); @@ -330,6 +366,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) { @@ -1028,14 +1083,36 @@ void Config::Load() get_config_or_default(config, parsed, "audio", "trace_events", DCA::trace_events, write_config); auto disabled_audio_events = get_config_or_default( config, parsed, "audio", "disabled_events", DCA::disabled_events, write_config); + this->disable_all_audio_events = false; this->disabled_audio_events.clear(); for (const auto& event : StrSplit(disabled_audio_events, ',')) { auto stripped = StripAsciiWhitespace(event); - if (!stripped.empty()) { + if (is_all_audio_wildcard(stripped)) { + this->disable_all_audio_events = true; + } else if (!stripped.empty()) { this->disabled_audio_events.emplace_back(stripped); } } - this->installAudioEventHooks = this->trace_audio_events || !this->disabled_audio_events.empty(); + this->installAudioEventHooks = + this->trace_audio_events || this->disable_all_audio_events || !this->disabled_audio_events.empty(); + bool any_toast_audio_alert_configured = false; + for (const auto& alert : kToastAudioAlerts) { + const auto sound = get_notification_sound(config, parsed, alert.config_name, alert.default_sound, write_config); + this->*(alert.config_member) = sound; + any_toast_audio_alert_configured |= sound != NotificationSound::None; + } + if (!this->installToastBannerHooks && any_toast_audio_alert_configured) { + spdlog::warn("audio alerts require patches.toastbannerhooks = true"); + } + this->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); @@ -1270,6 +1347,50 @@ void Config::Load() spdlog::debug("Final notify banner types: {}", notifyString); parsed["ui"].as_table()->insert_or_assign("notify_banner_types", notifyString); + auto notify_fleet_events_str = get_config_or_default(config, parsed, "ui", "notify_fleet_events", + DCU::notify_fleet_events, write_log); + this->notify_fleet_events = 0; + for (const auto& event : StrSplit(notify_fleet_events_str, ',')) { + const std::string trimmed{StripAsciiWhitespace(event)}; + if (trimmed.empty()) { + continue; + } + const auto normalized = AsciiStrToUpper(trimmed); + if (normalized == "ALL") { + this->notify_fleet_events = kAllFleetNotifications; + break; + } + const auto match = std::ranges::find_if(kFleetNotificationCatalog, [&](const auto& entry) { + return normalized == AsciiStrToUpper(entry.config_name); + }); + if (match == kFleetNotificationCatalog.end()) { + spdlog::warn("Unknown fleet notification event '{}'; ignoring it", trimmed); + continue; + } + this->notify_fleet_events |= fleet_notification_bit(match->kind); + } + + std::string fleet_events_string; + for (const auto& entry : kFleetNotificationCatalog) { + if ((this->notify_fleet_events & fleet_notification_bit(entry.kind)) == 0) { + continue; + } + if (!fleet_events_string.empty()) { + fleet_events_string.append(", "); + } + fleet_events_string.append(entry.config_name); + } + spdlog::debug("Final fleet notification events: {}", fleet_events_string); + parsed["ui"].as_table()->insert_or_assign("notify_fleet_events", fleet_events_string); + +#if _WIN32 + this->installFleetNotificationHooks = (this->notify_fleet_events | this->audio_fleet_events) != 0; +#elif __APPLE__ + this->installFleetNotificationHooks = this->audio_fleet_events != 0; +#else + this->installFleetNotificationHooks = false; +#endif + spdlog::debug(""); parse_config_shortcut(config, parsed, "move_left", GameFunction::MoveLeft, DCSH::move_left); @@ -1370,6 +1491,7 @@ void Config::Load() DCSH::show_stationexterior); parse_config_shortcut(config, parsed, "show_stationinterior", GameFunction::ShowStationInterior, DCSH::show_stationinterior); + parse_config_shortcut(config, parsed, "show_haven", GameFunction::ShowHaven, DCSH::show_haven); parse_config_shortcut(config, parsed, "toggle_queue", GameFunction::ToggleQueue, DCSH::toggle_queue); parse_config_shortcut(config, parsed, "toggle_instant_warp", GameFunction::ToggleAutoConfirmInstantWarp, DCSH::toggle_instant_warp); diff --git a/mods/src/config.h b/mods/src/config.h index cd9d4de6d..2d3a725b4 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -8,6 +8,9 @@ #include +#include "patches/notification_audio.h" +#include "patches/fleet_notification_types.h" + #if _WIN32 #include #endif @@ -152,6 +155,7 @@ class Config final [[nodiscard]] MissionHudVisibility MissionHudButtonVisibility(std::string_view button_name) const; [[nodiscard]] bool MissionHudTweaksEnabled() const; + [[nodiscard]] NotificationSound NotificationSoundForToast(int toast_state) const; // Disallow copying/moving to enforce singleton Config(const Config&) = delete; @@ -196,6 +200,9 @@ class Config final bool borderless_fullscreen; 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; int extend_donation_max; @@ -210,7 +217,13 @@ class Config final bool disable_first_popup; bool disable_toast_banners; bool trace_audio_events; + bool disable_all_audio_events; std::vector disabled_audio_events; + NotificationSound alert_victory = NotificationSound::None; + NotificationSound alert_defeat = NotificationSound::None; + NotificationSound alert_armada_created = NotificationSound::None; + NotificationSound alert_armada_battle_won = NotificationSound::None; + NotificationSound alert_armada_battle_lost = NotificationSound::None; bool auto_open_bulk_claim_flyout; bool auto_confirm_ft_upgrade; @@ -253,6 +266,7 @@ class Config final bool installZoomHooks; bool installBuffFixHooks; bool installToastBannerHooks; + bool installFleetNotificationHooks; bool installPanHooks; bool installHotkeyHooks; bool installFreeResizeHooks; diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index de6bc7e27..1b3b4c799 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -11,6 +11,12 @@ 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"; + constexpr const char* alert_fleet_default = "none"; } // namespace Audio namespace SystemConfig @@ -168,6 +174,7 @@ namespace Shortcuts constexpr const char* show_battlelogs = "SHIFT-B"; constexpr const char* show_stationexterior = "SHIFT-G"; constexpr const char* show_stationinterior = "SHIFT-H"; + constexpr const char* show_haven = "ALT-H"; constexpr const char* show_system = "H"; constexpr const char* toggle_shortcut_hints = "HOME"; constexpr const char* toggle_cargo_armada = "ALT-5"; @@ -257,6 +264,7 @@ namespace UI constexpr const char* instant_warp_always_ask = ""; constexpr const char* pinned_ships = ""; constexpr const char* notify_banner_types = ""; + constexpr const char* notify_fleet_events = ""; constexpr auto extend_chest_purchase_max = 160; constexpr auto extend_donation_max = 80; constexpr bool extend_donation_slider = true; 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/fleet_arrival_tracker.h b/mods/src/patches/fleet_arrival_tracker.h new file mode 100644 index 000000000..5f859e824 --- /dev/null +++ b/mods/src/patches/fleet_arrival_tracker.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +// Only the phases relevant to identifying one arrival per observed journey. +enum class FleetArrivalPhase { Other, Charging, Warping, Impulsing }; + +struct FleetArrivalTracker { + uint64_t fleet_id = 0; + uint64_t epoch = 0; + bool journey_observed = false; + + bool Observe(uint64_t id, uint64_t observation_epoch, FleetArrivalPhase before, + FleetArrivalPhase after, bool native_previous_warping) + { + if (fleet_id != id || epoch != observation_epoch) { + fleet_id = id; + epoch = observation_epoch; + journey_observed = false; + } + if (before == FleetArrivalPhase::Warping || before == FleetArrivalPhase::Charging + || after == FleetArrivalPhase::Charging + || after == FleetArrivalPhase::Warping) + journey_observed = true; + + // A sampled intermediate state can hide Warping -> Impulsing even though + // native history still records it. Require an observed journey; native + // history alone can be stale after startup or baseline reseeding. + const bool arrival = after == FleetArrivalPhase::Impulsing + && (before == FleetArrivalPhase::Warping + || native_previous_warping); + if (!arrival || !journey_observed) + return false; + journey_observed = false; + return true; + } +}; diff --git a/mods/src/patches/fleet_notification_types.h b/mods/src/patches/fleet_notification_types.h new file mode 100644 index 000000000..bc66562cf --- /dev/null +++ b/mods/src/patches/fleet_notification_types.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include + +enum class FleetNotificationKind : uint8_t { + ArrivedInSystem = 0, + ArrivedAtDestination, + StartedMining, + NodeDepleted, + Docked, + RepairComplete, + Count, +}; + +using FleetNotificationMask = uint32_t; + +struct FleetNotificationCatalogEntry { + FleetNotificationKind kind; + std::string_view config_name; + std::string_view audio_config_name; +}; + +inline constexpr std::array kFleetNotificationCatalog{ + FleetNotificationCatalogEntry{FleetNotificationKind::ArrivedInSystem, "ArrivedInSystem", "alert_fleet_arrived_in_system"}, + FleetNotificationCatalogEntry{FleetNotificationKind::ArrivedAtDestination, "ArrivedAtDestination", "alert_fleet_arrived_at_destination"}, + FleetNotificationCatalogEntry{FleetNotificationKind::StartedMining, "StartedMining", "alert_fleet_started_mining"}, + FleetNotificationCatalogEntry{FleetNotificationKind::NodeDepleted, "NodeDepleted", "alert_fleet_node_depleted"}, + FleetNotificationCatalogEntry{FleetNotificationKind::Docked, "Docked", "alert_fleet_docked"}, + FleetNotificationCatalogEntry{FleetNotificationKind::RepairComplete, "RepairComplete", "alert_fleet_repair_complete"}, +}; + +static_assert(kFleetNotificationCatalog.size() == static_cast(FleetNotificationKind::Count)); + +constexpr FleetNotificationMask fleet_notification_bit(FleetNotificationKind kind) +{ return FleetNotificationMask{1} << static_cast(kind); } + +constexpr std::string_view fleet_notification_name(FleetNotificationKind kind) +{ + for (const auto& entry : kFleetNotificationCatalog) { + if (entry.kind == kind) { + return entry.config_name; + } + } + return "Unknown"; +} + +inline constexpr FleetNotificationMask kAllFleetNotifications = [] { + FleetNotificationMask result = 0; + for (const auto& entry : kFleetNotificationCatalog) { + result |= fleet_notification_bit(entry.kind); + } + return result; +}(); diff --git a/mods/src/patches/fleet_watch.cc b/mods/src/patches/fleet_watch.cc index aa3b27dc2..df0d7f71d 100644 --- a/mods/src/patches/fleet_watch.cc +++ b/mods/src/patches/fleet_watch.cc @@ -41,6 +41,7 @@ struct ObservedFleet { }; std::array s_slots{}; +uint64_t s_observation_epoch = 0; std::vector s_subscriptions; bool s_observer_installed = false; bool s_seed_pending = false; @@ -110,6 +111,7 @@ bool needs_fast_poll(FleetState state) void reset_observation() { + ++s_observation_epoch; s_slots = {}; s_seed_pending = true; s_seed_has_observation = false; @@ -152,6 +154,7 @@ void dispatch_transition(int slot, FleetPlayerData* fleet, FleetState before, Fl .before = fleet_watch::Snapshot{slot, fleet->Id, before}, .after = fleet_watch::Snapshot{slot, fleet->Id, after}, .fleet = fleet, + .observation_epoch = s_observation_epoch, }; CallbackScope callback_scope; for (const auto& subscription : s_subscriptions) { diff --git a/mods/src/patches/fleet_watch.h b/mods/src/patches/fleet_watch.h index f865d278c..2136c8327 100644 --- a/mods/src/patches/fleet_watch.h +++ b/mods/src/patches/fleet_watch.h @@ -14,6 +14,8 @@ struct Transition { Snapshot before; Snapshot after; FleetPlayerData* fleet = nullptr; + // Changes whenever the observer resets its baseline; consumers must discard journey history. + uint64_t observation_epoch = 0; }; using TransitionCallback = void (*)(const Transition& transition); diff --git a/mods/src/patches/gamefunctions.h b/mods/src/patches/gamefunctions.h index b9091305a..0f37406b5 100644 --- a/mods/src/patches/gamefunctions.h +++ b/mods/src/patches/gamefunctions.h @@ -38,6 +38,7 @@ enum GameFunction { ShowShips, ShowInventory, ShowStationInterior, + ShowHaven, ShoWStationExterior, ShowGalaxy, NativeShortcutGalaxy, diff --git a/mods/src/patches/notification_audio.cc b/mods/src/patches/notification_audio.cc new file mode 100644 index 000000000..1e2d5f7df --- /dev/null +++ b/mods/src/patches/notification_audio.cc @@ -0,0 +1,267 @@ +#include "patches/notification_audio.h" + +#include "patches/notification_audio_platform.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#if _WIN32 +#include +#include +#endif + +namespace +{ +struct ToneSegment { + double frequency_hz; + int duration_ms; +}; + +constexpr int kSampleRate = 44100; +constexpr double kTwoPi = 6.28318530717958647692; +constexpr double kAmplitude = 0.26; + +constexpr std::array kDefaultPattern{{{740.0, 70}, {0.0, 22}, {880.0, 85}, {0.0, 18}}}; +constexpr std::array kInfoPattern{{{659.0, 80}, {0.0, 24}, {880.0, 110}}}; +constexpr std::array kSuccessPattern{{{587.0, 70}, {0.0, 18}, {740.0, 70}, {0.0, 18}, {988.0, 120}}}; +constexpr std::array kWarningPattern{{{622.0, 90}, {0.0, 36}, {466.0, 110}, {0.0, 28}, {466.0, 90}}}; +constexpr std::array kAlarmPattern{{{880.0, 90}, {0.0, 42}, {880.0, 90}, {0.0, 42}, {698.0, 160}}}; +constexpr std::array kArrivalPattern{{{523.0, 65}, {0.0, 18}, {659.0, 70}, {0.0, 18}, {1046.0, 125}}}; +constexpr std::array kSoftPattern{{{523.0, 90}, {0.0, 26}, {659.0, 110}}}; +constexpr std::array kPingPattern{{{1046.0, 95}}}; +constexpr std::array kRepairPattern{{{440.0, 70}, {0.0, 18}, {554.0, 70}, {0.0, 18}, {740.0, 140}}}; + +struct SoundDefinition { + NotificationSound sound; + std::string_view name; + std::span pattern; +}; + +constexpr auto kSoundDefinitions = std::to_array({ + {NotificationSound::None, "none", {}}, + {NotificationSound::Default, "default", kDefaultPattern}, + {NotificationSound::Info, "info", kInfoPattern}, + {NotificationSound::Success, "success", kSuccessPattern}, + {NotificationSound::Warning, "warning", kWarningPattern}, + {NotificationSound::Alarm, "alarm", kAlarmPattern}, + {NotificationSound::Arrival, "arrival", kArrivalPattern}, + {NotificationSound::Soft, "soft", kSoftPattern}, + {NotificationSound::Ping, "ping", kPingPattern}, + {NotificationSound::Repair, "repair", kRepairPattern}, +}); + +struct SoundAlias { + std::string_view name; + NotificationSound sound; +}; + +constexpr auto kSoundAliases = std::to_array({ + {"off", NotificationSound::None}, + {"silent", NotificationSound::None}, + {"victory", NotificationSound::Success}, + {"warn", NotificationSound::Warning}, + {"attack", NotificationSound::Alarm}, + {"arrive", NotificationSound::Arrival}, + {"quiet", NotificationSound::Soft}, + {"repaired", NotificationSound::Repair}, +}); + +consteval bool SoundCatalogIsValid() +{ + if (kSoundDefinitions.size() != static_cast(NotificationSound::Count)) { + return false; + } + + for (size_t index = 0; index < kSoundDefinitions.size(); ++index) { + const auto& definition = kSoundDefinitions[index]; + if (static_cast(definition.sound) != index || definition.name.empty() + || (definition.sound == NotificationSound::None) != definition.pattern.empty()) { + return false; + } + for (size_t other = index + 1; other < kSoundDefinitions.size(); ++other) { + if (definition.name == kSoundDefinitions[other].name) { + return false; + } + } + } + + for (size_t index = 0; index < kSoundAliases.size(); ++index) { + const auto& alias = kSoundAliases[index]; + if (alias.name.empty() || static_cast(alias.sound) >= kSoundDefinitions.size()) { + return false; + } + for (const auto& definition : kSoundDefinitions) { + if (alias.name == definition.name) { + return false; + } + } + for (size_t other = index + 1; other < kSoundAliases.size(); ++other) { + if (alias.name == kSoundAliases[other].name) { + return false; + } + } + } + return true; +} + +static_assert(SoundCatalogIsValid()); + +constexpr const SoundDefinition* sound_definition(NotificationSound sound) +{ + const auto index = static_cast(sound); + if (index >= kSoundDefinitions.size() || kSoundDefinitions[index].sound != sound) { + return nullptr; + } + return &kSoundDefinitions[index]; +} + +static_assert(sound_definition(NotificationSound::Success)->name == "success"); +static_assert(sound_definition(NotificationSound::Count) == nullptr); + +std::string normalize_sound_name(std::string_view value) +{ + std::string normalized; + normalized.reserve(value.size()); + for (const auto ch : value) { + if (ch >= 'A' && ch <= 'Z') { + normalized.push_back(static_cast(ch - 'A' + 'a')); + } else if (ch == '-' || ch == ' ') { + normalized.push_back('_'); + } else { + normalized.push_back(ch); + } + } + return normalized; +} + +void append_u16(std::vector& buffer, uint16_t value) +{ + buffer.push_back(static_cast(value & 0xff)); + buffer.push_back(static_cast((value >> 8) & 0xff)); +} + +void append_u32(std::vector& buffer, uint32_t value) +{ + buffer.push_back(static_cast(value & 0xff)); + buffer.push_back(static_cast((value >> 8) & 0xff)); + buffer.push_back(static_cast((value >> 16) & 0xff)); + buffer.push_back(static_cast((value >> 24) & 0xff)); +} + +void append_ascii(std::vector& buffer, std::string_view value) +{ buffer.insert(buffer.end(), value.begin(), value.end()); } + +std::vector build_wav(std::span pattern) +{ + uint32_t sample_count = 0; + for (const auto& segment : pattern) { + sample_count += static_cast((static_cast(segment.duration_ms) * kSampleRate) / 1000); + } + + constexpr uint16_t channels = 1; + constexpr uint16_t bits_per_sample = 16; + const uint32_t data_bytes = sample_count * channels * (bits_per_sample / 8); + + std::vector buffer; + buffer.reserve(44 + data_bytes); + append_ascii(buffer, "RIFF"); + append_u32(buffer, 36 + data_bytes); + append_ascii(buffer, "WAVE"); + append_ascii(buffer, "fmt "); + append_u32(buffer, 16); + append_u16(buffer, 1); + append_u16(buffer, channels); + append_u32(buffer, kSampleRate); + append_u32(buffer, kSampleRate * channels * (bits_per_sample / 8)); + append_u16(buffer, channels * (bits_per_sample / 8)); + append_u16(buffer, bits_per_sample); + append_ascii(buffer, "data"); + append_u32(buffer, data_bytes); + + double phase = 0.0; + for (const auto& segment : pattern) { + const auto segment_samples = static_cast((static_cast(segment.duration_ms) * kSampleRate) / 1000); + const auto step = segment.frequency_hz > 0.0 ? kTwoPi * segment.frequency_hz / kSampleRate : 0.0; + for (int index = 0; index < segment_samples; ++index) { + const auto fade_in = std::min(1.0, static_cast(index) / 80.0); + const auto fade_out = std::min(1.0, static_cast(segment_samples - index) / 120.0); + const auto sample = segment.frequency_hz > 0.0 ? std::sin(phase) * kAmplitude * fade_in * fade_out : 0.0; + append_u16(buffer, static_cast(static_cast(sample * 32767.0))); + phase += step; + if (phase > kTwoPi) { + phase -= kTwoPi; + } + } + } + return buffer; +} + +using SoundBuffers = std::array, static_cast(NotificationSound::Count)>; + +const SoundBuffers& sound_buffers() +{ + static const auto buffers = [] { + SoundBuffers result; + for (const auto& definition : kSoundDefinitions) { + if (!definition.pattern.empty()) { + result[static_cast(definition.sound)] = build_wav(definition.pattern); + } + } + return result; + }(); + return buffers; +} +} // namespace + +std::string_view notification_sound_name(NotificationSound sound) +{ + const auto* definition = sound_definition(sound); + return definition ? definition->name : kSoundDefinitions.front().name; +} + +std::optional notification_sound_from_name(std::string_view name) +{ + const auto normalized = normalize_sound_name(name); + if (const auto definition = std::ranges::find(kSoundDefinitions, normalized, &SoundDefinition::name); + definition != kSoundDefinitions.end()) { + return definition->sound; + } + if (const auto alias = std::ranges::find(kSoundAliases, normalized, &SoundAlias::name); + alias != kSoundAliases.end()) { + return alias->sound; + } + return std::nullopt; +} + +void notification_audio_play(NotificationSound sound) +{ + if (sound == NotificationSound::None) + return; + + const auto index = static_cast(sound); + if (index >= kSoundDefinitions.size()) + return; + + const auto& buffer = sound_buffers()[index]; + if (buffer.empty()) + return; + + if (!notification_audio_platform_play(buffer.data(), buffer.size())) { + spdlog::warn("[NotifyAudio] Failed to play '{}' cue", notification_sound_name(sound)); + } +} + +#if _WIN32 +bool notification_audio_platform_play(const uint8_t* data, size_t) +{ return PlaySoundW(reinterpret_cast(data), nullptr, SND_ASYNC | SND_MEMORY | SND_NODEFAULT) != FALSE; } +#elif !defined(__APPLE__) +bool notification_audio_platform_play(const uint8_t*, size_t) +{ return false; } +#endif diff --git a/mods/src/patches/notification_audio.h b/mods/src/patches/notification_audio.h new file mode 100644 index 000000000..345fa9840 --- /dev/null +++ b/mods/src/patches/notification_audio.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +enum class NotificationSound : uint8_t { + None = 0, + Default, + Info, + Success, + Warning, + Alarm, + Arrival, + Soft, + Ping, + Repair, + Count, +}; + +[[nodiscard]] std::string_view notification_sound_name(NotificationSound sound); +[[nodiscard]] std::optional notification_sound_from_name(std::string_view name); +void notification_audio_play(NotificationSound sound); diff --git a/mods/src/patches/notification_audio_platform.h b/mods/src/patches/notification_audio_platform.h new file mode 100644 index 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 54edf2b39..c61a0cea0 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" @@ -24,6 +25,8 @@ static const MethodInfo* s_localize_ltc = nullptr; // LanguageManager.Localize(out string, LocaleTextContext) — instance static const MethodInfo* s_locale_utils_localize = nullptr; // LocaleUtilities.Localize(LocaleTextContext, bool, bool) — static static const MethodInfo* s_object_tostring = nullptr; +static bool s_initialized = false; +static thread_local int s_toast_notification_suppression_depth = 0; // --------------------------------------------------------------------------- // Toast state → human-readable title @@ -447,8 +450,20 @@ static std::string strip_unity_rich_text(const std::string& s) // Public API // --------------------------------------------------------------------------- +ScopedToastNotificationSuppression::ScopedToastNotificationSuppression() +{ ++s_toast_notification_suppression_depth; } + +ScopedToastNotificationSuppression::~ScopedToastNotificationSuppression() +{ --s_toast_notification_suppression_depth; } + void notification_init() { + if (s_initialized) { + return; + } + s_initialized = true; + +#if _WIN32 // Resolve LanguageManager::Localize(out string, LocaleTextContext) — the // 2-parameter overload that takes an LTC and returns a localized string. auto lm_helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Client.Localization", "LanguageManager"); @@ -499,7 +514,6 @@ void notification_init() spdlog::warn("[Notify] Could not resolve Object::ToString — placeholder formatting may be incomplete"); } -#if _WIN32 try { winrt::init_apartment(winrt::apartment_type::single_threaded); spdlog::info("[Notify] Windows notification service initialized"); @@ -509,20 +523,40 @@ void notification_init() } catch (...) { spdlog::warn("[Notify] Windows notification service failed (unknown error)"); } +#elif __APPLE__ + spdlog::info("[Notify] macOS audio notification service ready"); #else spdlog::info("[Notify] Notification service: platform not supported (no-op)"); #endif } -void notification_handle_toast(Toast* toast) +void notification_emit(std::string_view title, std::string_view body) { -#if !_WIN32 - return; // No notification delivery on non-Windows platforms yet +#if _WIN32 + const std::string owned_title{title}; + const std::string owned_body{body}; + show_system_notification(owned_title.c_str(), owned_body.c_str()); #else - auto state = toast->get_State(); + (void)title; + (void)body; +#endif +} + +void notification_handle_toast(Toast* toast) +{ + if (!toast) return; + + const auto& config = Config::Get(); + const auto state = toast->get_State(); + notification_audio_play(config.NotificationSoundForToast(state)); + +#if _WIN32 + if (s_toast_notification_suppression_depth > 0) { + return; + } // Check if this toast type is in the user's notify list - const auto& notify_types = Config::Get().notify_banner_types; + const auto& notify_types = config.notify_banner_types; if (std::ranges::find(notify_types, state) == notify_types.end()) { return; } diff --git a/mods/src/patches/notification_service.h b/mods/src/patches/notification_service.h index 2ebad087a..7a8776695 100644 --- a/mods/src/patches/notification_service.h +++ b/mods/src/patches/notification_service.h @@ -1,10 +1,27 @@ #pragma once +#include + struct Toast; +// Temporarily prevent game toasts from being forwarded as system notifications. +// The in-game toast pipeline is unaffected, and suppression is local to the calling thread. +class ScopedToastNotificationSuppression +{ +public: + ScopedToastNotificationSuppression(); + ~ScopedToastNotificationSuppression(); + + ScopedToastNotificationSuppression(const ScopedToastNotificationSuppression&) = delete; + ScopedToastNotificationSuppression& operator=(const ScopedToastNotificationSuppression&) = delete; +}; + // Initialize the notification service (resolve IL2CPP methods, init platform). -// Call once during InstallToastBannerHooks(). +// Idempotent; call from each feature installer that emits notifications. void notification_init(); +// Deliver a notification created by a mod feature rather than a Scopely toast. +void notification_emit(std::string_view title, std::string_view body); + // Called from toast hooks — checks config, formats, and delivers notification. void notification_handle_toast(Toast* toast); diff --git a/mods/src/patches/parts/audio_events.cc b/mods/src/patches/parts/audio_events.cc index 1d2c2c0e2..7e276d1cf 100644 --- a/mods/src/patches/parts/audio_events.cc +++ b/mods/src/patches/parts/audio_events.cc @@ -13,6 +13,40 @@ struct FabricEventManager { }; +namespace +{ +enum class FabricEventAction : int { + PlaySound = 0, + StopSound = 1, + UnpauseSound = 3, + AdvanceSequence = 16, + StopAll = 21, + UnloadAudio = 23, + PlayScheduled = 33, +}; + +constexpr bool can_start_audio(int event_action) +{ + switch (static_cast(event_action)) { + case FabricEventAction::PlaySound: + case FabricEventAction::UnpauseSound: + case FabricEventAction::AdvanceSequence: + case FabricEventAction::PlayScheduled: + return true; + default: + return false; + } +} + +constexpr bool should_suppress_audio_event(bool disable_all, bool event_is_disabled, int event_action) +{ return disable_all ? can_start_audio(event_action) : event_is_disabled; } + +static_assert(should_suppress_audio_event(true, false, static_cast(FabricEventAction::PlaySound))); +static_assert(!should_suppress_audio_event(true, false, static_cast(FabricEventAction::StopSound))); +static_assert(should_suppress_audio_event(false, true, static_cast(FabricEventAction::StopAll))); +static_assert(!should_suppress_audio_event(true, true, static_cast(FabricEventAction::UnloadAudio))); +} // namespace + // Fabric.EventManager.PostEvent(string, EventAction, object, GameObject, // InitialiseParameters, bool, OnEventNotify) bool FabricEventManager_PostEvent_Hook(auto original, FabricEventManager* _this, Il2CppString* event_name, @@ -25,14 +59,16 @@ bool FabricEventManager_PostEvent_Hook(auto original, FabricEventManager* _this, add_to_queue, on_event_notify); } - const auto event = to_string(event_name); + const auto event = to_string(event_name); auto& config = Config::Get(); if (config.trace_audio_events) { spdlog::info("Audio event: {}", event); } - if (std::ranges::find(config.disabled_audio_events, event) != config.disabled_audio_events.end()) { + const bool event_is_disabled = + std::ranges::find(config.disabled_audio_events, event) != config.disabled_audio_events.end(); + if (should_suppress_audio_event(config.disable_all_audio_events, event_is_disabled, event_action)) { spdlog::debug("Suppressed audio event: {}", event); return false; } @@ -49,10 +85,19 @@ void InstallAudioEventHooks() return; } - // The shorter PostEvent overloads funnel into this seven-argument overload. - const auto method = helper.GetMethodInfo("PostEvent", 7); + // The shorter string PostEvent overloads funnel into this seven-argument overload. Select it by type because + // Fabric also has a seven-argument integer-ID overload with an incompatible native signature. + auto string_event_filter = [](int count, const Il2CppType** params) { + return count == 7 && params != nullptr && params[0] != nullptr && params[5] != nullptr + && params[0]->type == IL2CPP_TYPE_STRING && params[5]->type == IL2CPP_TYPE_BOOLEAN; + }; + const auto method = helper.GetMethodInfoSpecial("PostEvent", string_event_filter); if (method == nullptr || method->methodPointer == nullptr) { - ErrorMsg::MissingMethod("EventManager", "PostEvent"); + ErrorMsg::MissingMethod("EventManager", "PostEvent(string, ..., bool, ...)"); + return; + } + if (method->return_type == nullptr || method->return_type->type != IL2CPP_TYPE_BOOLEAN) { + spdlog::error("Fabric EventManager string PostEvent route has an unexpected return type"); return; } diff --git a/mods/src/patches/parts/daily_faction_bulk_claim.cc b/mods/src/patches/parts/daily_faction_bulk_claim.cc index 3a3a9b7e7..35fcdb4ed 100644 --- a/mods/src/patches/parts/daily_faction_bulk_claim.cc +++ b/mods/src/patches/parts/daily_faction_bulk_claim.cc @@ -299,6 +299,15 @@ bool FactionMatches(const std::string& faction_name, const std::vectorklass == nullptr) { @@ -364,15 +373,17 @@ void FilterClaimableDailiesByFaction(IList* list, const std::vector int kept = 0; for (int32_t i = total - 1; i >= 0; --i) { - auto* tournament = list->Get(i); - auto faction_name = FactionNameOfTournament(tournament); - auto matches = FactionMatches(faction_name, configured); + auto* tournament = list->Get(i); + auto faction_name = FactionNameOfTournament(tournament); + const bool is_main_faction_daily = IsMainFactionDaily(faction_name); + auto matches = FactionMatches(faction_name, configured); spdlog::debug("[DailyFactionBulkClaim] [{}] faction: '{}' -> {}", i, faction_name.empty() ? "" : faction_name, - faction_name.empty() ? "keep (not a faction daily)" : (matches ? "keep (claim)" : "remove (skip)")); + !is_main_faction_daily ? "keep (not a main faction daily)" + : (matches ? "keep (claim)" : "remove (skip)")); - if (!faction_name.empty() && !matches) { + if (is_main_faction_daily && !matches) { list->RemoveAt(i); } else { ++kept; diff --git a/mods/src/patches/parts/fleet_notifications.cc b/mods/src/patches/parts/fleet_notifications.cc new file mode 100644 index 000000000..e7df7a32b --- /dev/null +++ b/mods/src/patches/parts/fleet_notifications.cc @@ -0,0 +1,311 @@ +#include "patches/fleet_notification_types.h" +#include "patches/fleet_arrival_tracker.h" + +#include "config.h" +#include "errormsg.h" +#include "patches/fleet_watch.h" +#include "patches/notification_service.h" +#include "str_utils.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ +constexpr int kFleetSlotCount = 10; + +using TransitionPredicate = bool (*)(FleetState before, FleetState after); + +struct TransitionRule { + FleetNotificationKind kind; + TransitionPredicate matches; + std::string_view title; + std::string_view message; +}; + +constexpr bool state_is_destination(FleetState state) +{ + switch (state) { + case FleetState::IdleInSpace: + case FleetState::CanRecall: + case FleetState::CanEngage: + case FleetState::CanLocate: + case FleetState::Deployed: + case FleetState::Capturing: + return true; + default: + return false; + } +} + +constexpr bool state_can_dock_from_space(FleetState state) +{ + switch (state) { + case FleetState::IdleInSpace: + case FleetState::Mining: + case FleetState::Battling: + case FleetState::WarpCharging: + case FleetState::Warping: + case FleetState::Impulsing: + case FleetState::Capturing: + case FleetState::CanRecall: + case FleetState::CanEngage: + case FleetState::CanLocate: + case FleetState::Deployed: + return true; + default: + return false; + } +} + +constexpr bool arrived_at_destination(FleetState before, FleetState after) +{ return before == FleetState::Impulsing && state_is_destination(after); } + +constexpr bool started_mining(FleetState before, FleetState after) +{ return before != FleetState::Mining && after == FleetState::Mining; } + +constexpr bool docked(FleetState before, FleetState after) +{ return before != FleetState::Repairing && after == FleetState::Docked && state_can_dock_from_space(before); } + +constexpr bool repair_complete(FleetState before, FleetState after) +{ return before == FleetState::Repairing && after == FleetState::Docked; } + +constexpr std::array kTransitionRules{ + TransitionRule{FleetNotificationKind::ArrivedAtDestination, arrived_at_destination, "Fleet Arrived", + "has reached its destination"}, + TransitionRule{FleetNotificationKind::StartedMining, started_mining, "Fleet Mining", "started mining"}, + TransitionRule{FleetNotificationKind::Docked, docked, "Fleet Docked", "docked"}, + TransitionRule{FleetNotificationKind::RepairComplete, repair_complete, "Repair Complete", "finished repairs"}, +}; + +static_assert(arrived_at_destination(FleetState::Impulsing, FleetState::IdleInSpace)); +static_assert(started_mining(FleetState::IdleInSpace, FleetState::Mining)); +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; } + +bool event_enabled(FleetNotificationKind kind) +{ return (s_enabled_events & fleet_notification_bit(kind)) != 0; } + +void play_event_audio(FleetNotificationKind kind) +{ + notification_audio_play(Config::Get().alert_fleet_events[static_cast(kind)]); +} + +std::string normalize_name(std::string name) +{ + constexpr std::string_view live_suffix = "_LIVE"; + if (name.ends_with(live_suffix)) { + name.erase(name.size() - live_suffix.size()); + } + for (auto& character : name) { + if (character == '_') { + character = ' '; + } + } + return name; +} + +std::string fleet_subject(FleetPlayerData* fleet) +{ + auto* hull = fleet ? fleet->Hull : nullptr; + return hull && hull->Name ? normalize_name(to_string(hull->Name)) : "fleet"; +} + +FleetArrivalPhase arrival_phase(FleetState state) +{ + switch (state) { + case FleetState::WarpCharging: return FleetArrivalPhase::Charging; + case FleetState::Warping: return FleetArrivalPhase::Warping; + case FleetState::Impulsing: return FleetArrivalPhase::Impulsing; + default: return FleetArrivalPhase::Other; + } +} + +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) { + 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)) { + play_event_audio(FleetNotificationKind::ArrivedInSystem); + if (notification_enabled(FleetNotificationKind::ArrivedInSystem)) + notification_emit("Fleet Arrived", "Your " + fleet_subject(transition.fleet) + " has arrived in-system"); + } + } + for (const auto& rule : kTransitionRules) { + if (!event_enabled(rule.kind) || !rule.matches(transition.before.state, transition.after.state)) { + continue; + } +#ifdef _MODDBG + spdlog::info("[FleetNotificationsProbe] event={} fleet={} oldState={} newState={}", + fleet_notification_name(rule.kind), transition.after.fleet_id, + static_cast(transition.before.state), static_cast(transition.after.state)); +#endif + play_event_audio(rule.kind); + if (notification_enabled(rule.kind)) { + notification_emit(rule.title, "Your " + fleet_subject(transition.fleet) + " " + std::string{rule.message}); + } + } +} + +constexpr bool needs_fast_poll(FleetNotificationMask enabled, FleetState state) +{ + constexpr auto arrival_events = fleet_notification_bit(FleetNotificationKind::ArrivedInSystem) + | fleet_notification_bit(FleetNotificationKind::ArrivedAtDestination); + constexpr auto post_warp_events = arrival_events | fleet_notification_bit(FleetNotificationKind::StartedMining) + | fleet_notification_bit(FleetNotificationKind::Docked); + constexpr auto post_impulse_events = fleet_notification_bit(FleetNotificationKind::ArrivedAtDestination) + | fleet_notification_bit(FleetNotificationKind::StartedMining) + | fleet_notification_bit(FleetNotificationKind::Docked); + constexpr auto post_activity_events = fleet_notification_bit(FleetNotificationKind::StartedMining) + | fleet_notification_bit(FleetNotificationKind::Docked); + + switch (state) { + case FleetState::WarpCharging: + case FleetState::Warping: + return (enabled & post_warp_events) != 0; + case FleetState::Impulsing: + return (enabled & post_impulse_events) != 0; + case FleetState::Battling: + case FleetState::Capturing: + return (enabled & post_activity_events) != 0; + case FleetState::Repairing: + return (enabled & fleet_notification_bit(FleetNotificationKind::RepairComplete)) != 0; + default: + return false; + } +} + +bool needs_enabled_fast_poll(FleetState state) +{ return needs_fast_poll(s_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)); +static_assert(needs_fast_poll(fleet_notification_bit(FleetNotificationKind::StartedMining), FleetState::Battling)); +static_assert(needs_fast_poll(fleet_notification_bit(FleetNotificationKind::RepairComplete), FleetState::Repairing)); +static_assert(!needs_fast_poll(0, FleetState::Warping)); +// Audio-only subscriptions use the same event mask and fast-poll path as desktop alerts. +static_assert(needs_fast_poll(0 | fleet_notification_bit(FleetNotificationKind::ArrivedInSystem), FleetState::Warping)); +static_assert(!needs_fast_poll(fleet_notification_bit(FleetNotificationKind::NodeDepleted), FleetState::Mining)); + +struct RecentNodeDepletion { + uint64_t fleet_id = 0; + std::chrono::steady_clock::time_point emitted_at{}; +}; + +std::array s_recent_node_depletions{}; +std::size_t s_next_node_depletion = 0; + +bool allow_node_depletion(uint64_t fleet_id) +{ + constexpr auto duplicate_window = std::chrono::seconds{2}; + const auto now = std::chrono::steady_clock::now(); + for (const auto& recent : s_recent_node_depletions) { + if (recent.emitted_at != std::chrono::steady_clock::time_point{} && recent.fleet_id == fleet_id + && now - recent.emitted_at < duplicate_window) { + return false; + } + } + s_recent_node_depletions[s_next_node_depletion] = {fleet_id, now}; + s_next_node_depletion = (s_next_node_depletion + 1) % s_recent_node_depletions.size(); + return true; +} + +FleetPlayerData* find_fleet(uint64_t fleet_id) +{ + auto* manager = FleetsManager::Instance(); + if (!manager || !manager->HasFleetService()) { + return nullptr; + } + for (int slot = 0; slot < kFleetSlotCount; ++slot) { + auto* fleet = manager->GetFleetPlayerData(slot); + if (fleet && fleet->Id == fleet_id) { + return fleet; + } + } + return nullptr; +} + +void ToastFleetObserver_HandleMiningDepleted_Hook(auto original, void* self, int64_t fleet_id) +{ + if (notification_enabled(FleetNotificationKind::NodeDepleted)) { + ScopedToastNotificationSuppression suppress_duplicate_forwarding; + original(self, fleet_id); + } else { + original(self, fleet_id); + } + const auto id = static_cast(fleet_id); + if (!allow_node_depletion(id)) { + return; + } +#ifdef _MODDBG + spdlog::info("[FleetNotificationsProbe] event={} fleet={}", + fleet_notification_name(FleetNotificationKind::NodeDepleted), id); +#endif + play_event_audio(FleetNotificationKind::NodeDepleted); + if (notification_enabled(FleetNotificationKind::NodeDepleted)) { + auto* fleet = find_fleet(id); + notification_emit("Node Depleted", fleet ? "Your " + fleet_subject(fleet) + " depleted its node." + : "A mining fleet depleted its node."); + } +} + +bool install_node_depletion_hook() +{ + auto helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.HUD", "ToastFleetObserver"); + if (!helper.isValidHelper()) { + ErrorMsg::MissingHelper("HUD", "ToastFleetObserver"); + return false; + } + auto* method = helper.GetMethod("HandleMiningDepleted", 1); + if (!method) { + ErrorMsg::MissingMethod("ToastFleetObserver", "HandleMiningDepleted"); + return false; + } + return SPUD_STATIC_DETOUR(method, ToastFleetObserver_HandleMiningDepleted_Hook) != nullptr; +} +} // namespace + +void InstallFleetNotificationHooks() +{ +#if _WIN32 + 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(); + + constexpr auto transition_notifications = + kAllFleetNotifications & ~fleet_notification_bit(FleetNotificationKind::NodeDepleted); + if ((s_enabled_events & transition_notifications) != 0 + && !fleet_watch::Subscribe({emit_transition, needs_enabled_fast_poll})) { + spdlog::warn("[FleetNotifications] Fleet Watch subscription failed"); + } + if (event_enabled(FleetNotificationKind::NodeDepleted) && !install_node_depletion_hook()) { + spdlog::warn("[FleetNotifications] node-depletion hook installation failed"); + } +#endif +} diff --git a/mods/src/patches/parts/hotkeys.cc b/mods/src/patches/parts/hotkeys.cc index 8428a9da0..124f53eba 100644 --- a/mods/src/patches/parts/hotkeys.cc +++ b/mods/src/patches/parts/hotkeys.cc @@ -30,6 +30,7 @@ #include "prime/LanguageManager.h" #include "prime/NavigationInteractionUIViewController.h" #include "prime/NavigationSectionManager.h" +#include "prime/PlanetaryBaseManager.h" #include "prime/PreScanTargetWidget.h" #include "prime/ScanEngageButtonsWidget.h" #include "prime/ScreenManager.h" @@ -675,6 +676,8 @@ void ScreenManager_Update_Hook(auto original, ScreenManager* _this) return ChangeNavigationSection(SectionID::Navigation_Galaxy); } else if (MapKey::IsDown(GameFunction::ShowStationInterior)) { return GotoSection(SectionID::Starbase_Interior); + } else if (MapKey::IsDown(GameFunction::ShowHaven)) { + return PlanetaryBaseManager::ViewOwnHaven(); } else if (MapKey::IsDown(GameFunction::ShowSystem)) { return ChangeNavigationSection(SectionID::Navigation_System); } else if (MapKey::IsDown(GameFunction::ShowArtifacts)) { diff --git a/mods/src/patches/patches.cc b/mods/src/patches/patches.cc index 9579b9692..5273307c5 100644 --- a/mods/src/patches/patches.cc +++ b/mods/src/patches/patches.cc @@ -25,6 +25,7 @@ void InstallBuffFixHooks(); void InstallFreeResizeHooks(); #endif void InstallToastBannerHooks(); +void InstallFleetNotificationHooks(); void InstallPanHooks(); void InstallHotkeyHooks(); void InstallGiftsBulkClaimHooks(); @@ -125,6 +126,7 @@ __int64 il2cpp_init_hook(auto original, const char* domain_name) {"ZoomHooks", {InstallZoomHooks, &cfg.installZoomHooks}}, {"BuffFixHooks", {InstallBuffFixHooks, &cfg.installBuffFixHooks}}, {"ToastBannerHooks", {InstallToastBannerHooks, &cfg.installToastBannerHooks}}, + {"FleetNotifications", {InstallFleetNotificationHooks, &cfg.installFleetNotificationHooks}}, {"PanHooks", {InstallPanHooks, &cfg.installPanHooks}}, {"HotkeyHooks", {InstallHotkeyHooks, &cfg.installHotkeyHooks}}, {"GiftsBulkClaimHooks", {InstallGiftsBulkClaimHooks, &cfg.installGiftsBulkClaimHooks}}, diff --git a/mods/src/prime/Hub.h b/mods/src/prime/Hub.h index b52249c77..3747f88b2 100644 --- a/mods/src/prime/Hub.h +++ b/mods/src/prime/Hub.h @@ -130,6 +130,7 @@ enum class SectionID { Missions_AvailableList = 1719419183, ShipManagement_Upgrade = 1742145916, GameSettings = 1754181205, + Starbase_Planetary = 1781357748, ShipScrapping_List = 1844785206, OnScreen_Station = 1883947040, RewardScreen = 1890775195, diff --git a/mods/src/prime/PlanetaryBaseManager.h b/mods/src/prime/PlanetaryBaseManager.h new file mode 100644 index 000000000..1e8e03019 --- /dev/null +++ b/mods/src/prime/PlanetaryBaseManager.h @@ -0,0 +1,64 @@ +#pragma once + +#include "Hub.h" +#include "MonoSingleton.h" +#include + +struct PlanetaryBaseManager : MonoSingleton { + friend struct MonoSingleton; + +public: + static void ViewOwnHaven() + { + // M95 v2: Haven is Starbase_Planetary, with a PlanetarySectionContext argument. + static auto& manager_class = get_class_helper(); + static auto context_class = + il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.Starbase", "PlanetarySectionContext"); + static auto is_ready = manager_class.GetMethod("IsPlanetaryBaseCenterReady", 1); + static auto get_user_id = manager_class.GetMethod("get_LocalUserId", 0); + static auto ctor = context_class.GetMethod(".ctor", 0); + static auto* owner_field = + context_class.get_cls() ? il2cpp_class_get_field_from_name(context_class.get_cls(), "OwnerUserId") : nullptr; + if (!manager_class.get_cls() || !is_ready || !get_user_id || !ctor || !owner_field) { + static bool warned = false; + if (!warned) { + warned = true; + spdlog::warn("[ShowHaven] Planetary navigation is unavailable in this client"); + } + return; + } + + auto* manager = Instance(); + auto* sections = Hub::get_SectionManager(); + if (!manager || !sections) { + return; + } + + // Preserve the game's Iconian Gateway prerequisite and its locked-building message. + if (!is_ready(manager, true)) { + return; + } + auto* owner = get_user_id(manager); + if (!owner || il2cpp_string_length(owner) == 0) { + return; + } + + auto* context = context_class.New(); + if (!context) { + return; + } + ctor(context); + // A fresh context targets our base even after visiting another player's Haven. + // Use the runtime setter so the managed reference receives the GC write barrier. + il2cpp_field_set_value_object(context, owner_field, reinterpret_cast(owner)); + sections->TriggerSectionChange(SectionID::Starbase_Planetary, context, false, false, true); + } + +private: + static IL2CppClassHelper& get_class_helper() + { + static auto helper = + il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.PlanetaryBase", "PlanetaryBaseManager"); + return helper; + } +}; diff --git a/mods/src/version.h b/mods/src/version.h index 766c39de6..6a8c3d849 100644 --- a/mods/src/version.h +++ b/mods/src/version.h @@ -3,8 +3,8 @@ // clang-format off #define VERSION_MAJOR 1 #define VERSION_MINOR 1 -#define VERSION_REVISION 7 -#define VERSION_PATCH 3 +#define VERSION_REVISION 8 +#define VERSION_PATCH 0 #define VERSION_COMMIT_HASH "" #define STRINGIFY_(s) #s 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 diff --git a/scripts/create-mac-dmg.sh b/scripts/create-mac-dmg.sh index 148cdf1d8..fc4811439 100755 --- a/scripts/create-mac-dmg.sh +++ b/scripts/create-mac-dmg.sh @@ -7,16 +7,16 @@ ARCH=arm64 xmake clean # Build the arm64 version -xmake f -y -p macosx -a "arm64" -m $CONFIG --target_minver=13.5 +xmake f -y -p macosx -a "arm64" -m $CONFIG --target_minver=14.6 xmake # Build the x86_64 version -xmake f -y -p macosx -a "x86_64" -m $CONFIG --target_minver=13.5 +xmake f -y -p macosx -a "x86_64" -m $CONFIG --target_minver=14.6 xmake # Rebuild the package app bundle after switching architectures so xcode.xcassets # regenerates Assets.car even when xmake clean leaves stale asset dependencies. -xmake f -y -p macosx -a "$ARCH" -m "$CONFIG" --target_minver=15.4 +xmake f -y -p macosx -a "$ARCH" -m "$CONFIG" --target_minver=14.6 xmake -r -y macOSLauncher test -f "build/macosx/$ARCH/$CONFIG/macOSLauncher.app/Contents/Resources/Assets.car" diff --git a/scripts/mac-build-test-debug.sh b/scripts/mac-build-test-debug.sh index 14ef8808e..9f1254027 100755 --- a/scripts/mac-build-test-debug.sh +++ b/scripts/mac-build-test-debug.sh @@ -143,46 +143,46 @@ PACKAGED_APP_PATH="${BUILD_DIR}/STFC Community Mod.app" # Configure the project configure_project() { print_info "Configuring project for ${ARCH} in ${BUILD_MODE} mode..." - + cd "$PROJECT_ROOT" - - local xmake_opts="-y -p macosx -a ${ARCH} -m ${BUILD_MODE} --target_minver=13.5" + + local xmake_opts="-y -p macosx -a ${ARCH} -m ${BUILD_MODE} --target_minver=14.6" if [[ "$VERBOSE" == true ]]; then xmake_opts="${xmake_opts} -v" fi - + xmake f ${xmake_opts} - + print_success "Configuration complete" } # Clean build artifacts clean_build() { print_info "Cleaning build artifacts..." - + cd "$PROJECT_ROOT" xmake clean -a - + print_success "Clean complete" } # Build the project build_project() { print_info "Building project for ${ARCH} in ${BUILD_MODE} mode..." - + cd "$PROJECT_ROOT" - + if [[ "$VERBOSE" == true ]]; then xmake -v else xmake fi - + print_success "Build complete" - + # Show build output location print_info "Build artifacts location: ${BUILD_DIR}" - + # List built files if [[ -d "$BUILD_DIR" ]]; then print_info "Built files:" @@ -249,7 +249,7 @@ package_app_bundle() { prepare_app() { if [[ "$USE_LAUNCHER" == true ]]; then print_info "Preparing launcher application bundle..." - + # Check if app was built if [[ ! -d "$PACKAGED_APP_PATH" ]]; then print_warning "Packaged app not found at: $PACKAGED_APP_PATH" @@ -265,21 +265,21 @@ prepare_app() { print_success "Launcher prepared at: $PACKAGED_APP_PATH" else print_info "Preparing loader..." - + # Check if loader was built if [[ ! -f "$LOADER_PATH" ]]; then print_error "Loader not found at: $LOADER_PATH" print_info "Make sure the loader was built successfully" exit 1 fi - + # Check if dylib was built local dylib_path="${BUILD_DIR}/libstfc-community-mod.dylib" if [[ ! -f "$dylib_path" ]]; then print_warning "Mod library not found at: $dylib_path" print_warning "The loader may not work correctly without the mod library" fi - + print_success "Loader prepared at: $LOADER_PATH" print_info "Loader will inject: $dylib_path" fi @@ -288,12 +288,12 @@ prepare_app() { # Run the application run_app() { prepare_app - + if [[ "$USE_LAUNCHER" == true ]]; then print_info "Launching application with launcher..." print_info "Crash dumps will be generated at: ~/Library/Logs/DiagnosticReports/" open "$PACKAGED_APP_PATH" - + print_success "Launcher launched" print_info "To view logs, use: log stream --predicate 'process == \"macOSLauncher\"' --level debug" print_info "To view crash logs after a crash, run: $(basename "$0") crashlogs" @@ -301,10 +301,10 @@ run_app() { print_info "Running loader directly..." print_info "The loader will launch the game with the mod injected" print_info "Crash dumps will be generated at: ~/Library/Logs/DiagnosticReports/" - + # Run the loader "$LOADER_PATH" - + print_success "Loader executed" print_info "To view game logs, use: log stream --predicate 'process == \"Star Trek Fleet Command\"' --level debug" print_info "To view crash logs after a crash, run: $(basename "$0") crashlogs" @@ -314,13 +314,13 @@ run_app() { # Show crash logs show_crash_logs() { local crash_dir="${HOME}/Library/Logs/DiagnosticReports" - + print_info "Searching for recent crash logs..." echo - + # Look for crash logs from our apps local found_crashes=false - + # Search for macOSLauncher crashes if compgen -G "${crash_dir}/macOSLauncher*.crash" > /dev/null 2>&1; then print_info "Recent macOSLauncher crashes:" @@ -330,7 +330,7 @@ show_crash_logs() { echo found_crashes=true fi - + # Search for loader crashes if compgen -G "${crash_dir}/stfc-community-mod-loader*.crash" > /dev/null 2>&1; then print_info "Recent stfc-community-mod-loader crashes:" @@ -340,7 +340,7 @@ show_crash_logs() { echo found_crashes=true fi - + # Search for game crashes if compgen -G "${crash_dir}/*Star Trek Fleet Command*.crash" > /dev/null 2>&1; then print_info "Recent Star Trek Fleet Command crashes:" @@ -350,14 +350,14 @@ show_crash_logs() { echo found_crashes=true fi - + if [[ "$found_crashes" == false ]]; then print_warning "No crash logs found in ${crash_dir}" echo else # Get the most recent crash file local most_recent=$(ls -t "${crash_dir}"/{macOSLauncher,stfc-community-mod-loader,*"Star Trek Fleet Command"}*.crash 2>/dev/null | head -1) - + if [[ -f "$most_recent" ]]; then print_info "Most recent crash log: ${most_recent}" echo @@ -368,7 +368,7 @@ show_crash_logs() { fi fi fi - + print_info "Crash logs location: ${crash_dir}" print_info "To view a specific crash: less '${crash_dir}/'" } @@ -376,10 +376,10 @@ show_crash_logs() { # Debug the application with lldb debug_app() { prepare_app - + local executable local exec_name - + if [[ "$USE_LAUNCHER" == true ]]; then executable="${PACKAGED_APP_PATH}/Contents/MacOS/macOSLauncher" exec_name="Launcher" @@ -387,12 +387,12 @@ debug_app() { executable="$LOADER_PATH" exec_name="Loader" fi - + if [[ ! -f "$executable" ]]; then print_error "Executable not found at: $executable" exit 1 fi - + print_warning "Running under lldb - system crash reports will NOT be generated" print_warning "Use 'run' action instead of 'debug' if you need crash dumps" echo @@ -400,7 +400,7 @@ debug_app() { print_info "The ${exec_name} will launch under lldb" print_info "Use 'continue' or 'c' to start execution" print_info "Use 'breakpoint set -n ' to set breakpoints" - + # Create lldb command file for better debugging experience local lldb_script=$(mktemp) cat > "$lldb_script" << 'LLDB_EOF' @@ -424,28 +424,28 @@ target create "%EXECUTABLE%" # Set any initial breakpoints here if needed # breakpoint set -n main LLDB_EOF - + # Replace placeholders with actual values sed -i '' "s|%EXECUTABLE%|${executable}|g" "$lldb_script" sed -i '' "s|%EXEC_NAME%|${exec_name}|g" "$lldb_script" - + lldb -s "$lldb_script" - + rm "$lldb_script" } # Main execution flow main() { print_info "STFC Community Mod Development Script" - + local exec_type="Loader" if [[ "$USE_LAUNCHER" == true ]]; then exec_type="Launcher" fi - + print_info "Action: ${ACTION}, Mode: ${BUILD_MODE}, Arch: ${ARCH}, Using: ${exec_type}" echo - + case $ACTION in config) configure_project @@ -490,11 +490,10 @@ main() { exit 1 ;; esac - + echo print_success "All operations completed successfully!" } # Run main function main - diff --git a/tests/fleet_arrival_tracker.cc b/tests/fleet_arrival_tracker.cc new file mode 100644 index 000000000..da8fb3da9 --- /dev/null +++ b/tests/fleet_arrival_tracker.cc @@ -0,0 +1,52 @@ +#include "patches/fleet_arrival_tracker.h" +#include +#include + +int main() +{ + using P = FleetArrivalPhase; + FleetArrivalTracker t; + auto step = [&](P before, P after, bool native = false, uint64_t epoch = 1, uint64_t fleet = 10) { + return t.Observe(fleet, epoch, before, after, native); + }; + // Normal arrival, followed by the same native history resurfacing: only once. + assert(!step(P::Other, P::Charging)); + assert(!step(P::Charging, P::Warping)); + assert(step(P::Warping, P::Impulsing, true)); + assert(!step(P::Impulsing, P::Other)); + assert(!step(P::Other, P::Impulsing, true)); + // Captured fleet A recall: native previous remains Warping through transient Docked. + assert(!step(P::Other, P::Charging)); + assert(!step(P::Charging, P::Warping)); + assert(!step(P::Warping, P::Other)); + assert(step(P::Other, P::Impulsing, true)); + // Vindicator sequence: polling skipped Warping. Native history is required. + assert(!step(P::Other, P::Charging)); + assert(!step(P::Charging, P::Other)); + assert(!step(P::Other, P::Impulsing, false)); + assert(step(P::Other, P::Impulsing, true)); + // Any intermediate state is accepted, not just the captured Docked value. + assert(!step(P::Other, P::Charging)); + assert(!step(P::Charging, P::Other)); + assert(step(P::Other, P::Impulsing, true)); + // Captured outbound fleet A: Warping was skipped, then arrival resurfaced. + assert(!step(P::Other, P::Charging)); + assert(!step(P::Charging, P::Other)); + assert(!step(P::Other, P::Charging)); + assert(step(P::Charging, P::Impulsing, true)); + assert(!step(P::Impulsing, P::Other)); + assert(!step(P::Other, P::Impulsing, true)); + // Startup/ordinary undock cannot replay a completed journey. + assert(!step(P::Other, P::Impulsing, true)); + // Reseeding and fleet replacement discard prior journey observations. + assert(!step(P::Other, P::Charging)); + assert(!step(P::Other, P::Impulsing, true, 2)); + assert(!step(P::Other, P::Charging, false, 2)); + assert(!step(P::Other, P::Impulsing, true, 2, 11)); + // Baseline already in warp: its eventual normal arrival still fires. + assert(step(P::Warping, P::Impulsing, true, 3, 12)); + // Baseline in charging also establishes an in-flight journey, even if warp is skipped. + assert(step(P::Charging, P::Impulsing, true, 4, 13)); + assert(!step(P::Charging, P::Impulsing, false, 5, 14)); + std::cout << "Fleet arrival regression cases passed\n"; +}