Crash reports list the build's native mods

A game the launcher built with native mods reads their ids and versions from
the native-mods.json beside it and adds them to the crash report's details as
Native Mods. The line stays out of crash telemetry, which keeps only its listed
details.
This commit is contained in:
arismoko committed 2026-11-02 12:32:16 +00:00
1 parent 9eb0e230e3
commit c029313d6c
2 files changed
+33 -5

No files matched your search

+31 -4
View File
@@ -237,10 +237,20 @@ fn register_menu_types(mods: &[horizon_client::mod_content::ContentMod]) {
/// What the launcher writes beside a game it built with native mods: their ids under `mods`.
const BUILD_INFO: &str = "native-mods.json";
/// Whether the launcher built this game with native mods (`built`: its [`BUILD_INFO`]).
fn built_with_mods(built: &str) -> bool {
/// The native mods the launcher built this game with, `id version, id version`, for the crash
/// report (`built`: its [`BUILD_INFO`]). None for a release or Nightly, which have no mods.
fn native_mods_detail(built: &str) -> Option<String> {
let info: serde_json::Value = serde_json::from_str(built).unwrap_or_default();
info["mods"].as_array().is_some_and(|mods| !mods.is_empty())
let list = info["mods"]
.as_array()?
.iter()
.map(|m| {
let text = |key: &str| m[key].as_str().unwrap_or("?").to_owned();
format!("{} {}", text("id"), text("version"))
})
.collect::<Vec<_>>()
.join(", ");
(!list.is_empty()).then_some(list)
}
/// A row for each native mod the launcher installed in `dir` (`<id>.hnmod`) that this game wasn't
@@ -330,8 +340,13 @@ impl Game<'_> {
.ok()
.and_then(|exe| std::fs::read_to_string(exe.with_file_name(BUILD_INFO)).ok())
.unwrap_or_default();
let built_mods = native_mods_detail(&built);
let built_with_mods = built_mods.is_some();
if let Some(list) = built_mods {
horizon_client::crash::set_detail("Native Mods", list);
}
horizon_client::gfx::native_render::set_mods_built_in(
built_with_mods(&built)
built_with_mods
|| self
.settings
.mods
@@ -392,6 +407,18 @@ mod tests {
zip.finish().unwrap();
}
#[test]
fn the_crash_report_lists_the_builds_native_mods() {
let built = r#"{"mods":[{"id":"machine_sample","version":"0.1.0","sha256":null},{"id":"pipes","version":"2.0"}]}"#;
assert_eq!(
native_mods_detail(built).as_deref(),
Some("machine_sample 0.1.0, pipes 2.0")
);
// A release has no file; a build without mods has an empty list.
assert_eq!(native_mods_detail(""), None);
assert_eq!(native_mods_detail(r#"{"mods":[]}"#), None);
}
#[test]
fn native_mods_installed_but_not_built_get_a_row_saying_so() {
let dir = std::env::temp_dir().join(format!("horizon-not-built-{}", std::process::id()));
+2 -1
View File
@@ -1045,7 +1045,8 @@ trusting native code is the player's decision, never an agent's.
7. **Install**: a new version, `native-<commit>-<key>`, a copy of the starting version's folder (resources
included, cloned on APFS, Btrfs and ReFS) with the built game in place of its own, and a `native-mods.json`
saying which release or Nightly it started from (`base`) and which commit, mods, horizon-mixins and Rust made
it. The instance the mods are installed in plays it, the default **Horizon** instance included: the build
it. The game lists its `mods` with their versions in a crash report's details (`Native Mods: id version, …`;
they stay in the local report, not in crash telemetry). The instance the mods are installed in plays it, the default **Horizon** instance included: the build
pins its version, as picking a version does. Updates of the launcher's version leave a pinned instance where
it is, and their clean-up never removes a native build. An instance built before this (the **Native mods**
instance the default home's mods used to go to) keeps its worlds and its build.