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";
+}