mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 17:19:15 -10:00
Merge pull request #3596 from horizon-rust/fix/launcher-native-mods-auto-on
🤖 Agent: Ludovic's decision 10-29 (native mods on when one is installed, notice once). Diff reviewed; verify green; build matrix 40848959744 green on Windows, macOS and Linux.
This commit is contained in:
10 files changed
+170
-9
No files matched your search
+3
-2
@@ -40,8 +40,9 @@ mod shows one.
|
||||
|
||||
## What the launcher does
|
||||
|
||||
In the window: install a native mod from the Mods page, turn on Settings → Game → **Native mods (experimental)**,
|
||||
then **Build**. The launcher names the mods, warns in red, and builds only from "I trust these mods: build"; the
|
||||
In the window: install a native mod from the Mods page. The first one shows a notice once (the first build downloads
|
||||
Rust and can take 20 to 30 minutes; later builds are faster) and turns on Settings → Game → **Native mods
|
||||
(experimental)**, which stays off for players who never install one; `mods install` does the same. Then **Build**. The launcher names the mods, warns in red, and builds only from "I trust these mods: build"; the
|
||||
row shows each step, the error if one fails, and the build the instance plays. From the command line:
|
||||
`horizon-launcher native-build [FOLDER] [--instance NAME] --trust-native-code`. FOLDER is a bundle, an unpacked
|
||||
`.hnmod`, or a folder of them; without it, the native mods turned on in the instance. It is not an MCP tool:
|
||||
|
||||
@@ -181,9 +181,11 @@
|
||||
"These mods will run with full access to your computer": "Diese Mods erhalten vollen Zugriff auf deinen Computer",
|
||||
"They can read, change or delete your files, and use your accounts and network, like any program you run. Build them only if you trust their authors: the Mods page names them.": "Sie können deine Dateien lesen, ändern oder löschen und deine Konten sowie dein Netzwerk nutzen wie jedes andere Programm, das du ausführst. Kompiliere sie nur, wenn du ihren Autoren vertraust: Die Mods-Seite nennt sie.",
|
||||
"Mods: {mods}": "Mods: {mods}",
|
||||
"The build compiles the game: about ten minutes and a few GB of disk the first time, a few minutes after that.": "Der Build kompiliert das Spiel: beim ersten Mal etwa zehn Minuten und einige GB Speicherplatz, danach einige Minuten.",
|
||||
"The build compiles the game: 20 to 30 minutes and a few GB of disk the first time (it downloads Rust), a few minutes after that.": "Der Build kompiliert das Spiel: beim ersten Mal 20 bis 30 Minuten und einige GB Speicherplatz (er lädt Rust herunter), danach wenige Minuten.",
|
||||
"Delete old versions automatically": "Alte Versionen automatisch löschen",
|
||||
"Keeps the {count} newest, the one in use and the ones your instances use. Deleted versions install again from the Versions page.": "Behält die {count} neuesten, die verwendete und die von deinen Instanzen genutzten. Gelöschte Versionen lassen sich auf der Seite Versionen wieder installieren.",
|
||||
"Installed versions use {size}.": "Installierte Versionen belegen {size}.",
|
||||
"{size} on disk": "{size} auf dem Datenträger"
|
||||
"{size} on disk": "{size} auf dem Datenträger",
|
||||
"Install and turn on native mods": "Installieren und native Mods einschalten",
|
||||
"Native mods compile into a game of their own. Installing one turns Native mods on: you then build the game with it in Settings > Native mods. The first build downloads Rust and can take 20 to 30 minutes; later builds are faster.": "Native Mods werden in ein eigenes Spiel kompiliert. Wenn du eine installierst, werden native Mods eingeschaltet: Danach baust du das Spiel damit unter Einstellungen > Native Mods. Der erste Build lädt Rust herunter und kann 20 bis 30 Minuten dauern; spätere Builds sind schneller."
|
||||
}
|
||||
@@ -185,7 +185,9 @@
|
||||
"These mods will run with full access to your computer": "Ces mods auront un accès complet à votre ordinateur",
|
||||
"They can read, change or delete your files, and use your accounts and network, like any program you run. Build them only if you trust their authors: the Mods page names them.": "Ils peuvent lire, modifier ou supprimer vos fichiers, et utiliser vos comptes et votre réseau, comme tout programme que vous lancez. Ne les compilez que si vous faites confiance à leurs auteurs : la page Mods les nomme.",
|
||||
"Mods: {mods}": "Mods : {mods}",
|
||||
"The build compiles the game: about ten minutes and a few GB of disk the first time, a few minutes after that.": "La compilation construit le jeu : une dizaine de minutes et quelques Go de disque la première fois, quelques minutes ensuite.",
|
||||
"The build compiles the game: 20 to 30 minutes and a few GB of disk the first time (it downloads Rust), a few minutes after that.": "La compilation construit le jeu : 20 à 30 minutes et quelques Go de disque la première fois (elle télécharge Rust), quelques minutes ensuite.",
|
||||
"Install and turn on native mods": "Installer et activer les mods natifs",
|
||||
"Native mods compile into a game of their own. Installing one turns Native mods on: you then build the game with it in Settings > Native mods. The first build downloads Rust and can take 20 to 30 minutes; later builds are faster.": "Les mods natifs sont compilés dans un jeu à part. En installer un active les mods natifs : vous compilez ensuite le jeu avec lui dans Paramètres > Mods natifs. La première compilation télécharge Rust et peut prendre 20 à 30 minutes ; les suivantes sont plus rapides.",
|
||||
"Delete…": "Supprimer…",
|
||||
"Keep": "Garder",
|
||||
"Move to Trash": "Mettre à la corbeille",
|
||||
|
||||
@@ -185,7 +185,9 @@
|
||||
"These mods will run with full access to your computer": "Эти моды получат полный доступ к вашему компьютеру",
|
||||
"They can read, change or delete your files, and use your accounts and network, like any program you run. Build them only if you trust their authors: the Mods page names them.": "Они могут читать, изменять и удалять ваши файлы, использовать ваши аккаунты и сеть, как любая запущенная вами программа. Собирайте их, только если доверяете авторам: они указаны на странице «Моды».",
|
||||
"Mods: {mods}": "Моды: {mods}",
|
||||
"The build compiles the game: about ten minutes and a few GB of disk the first time, a few minutes after that.": "Сборка компилирует игру: около десяти минут и несколько ГБ на диске в первый раз, потом несколько минут.",
|
||||
"The build compiles the game: 20 to 30 minutes and a few GB of disk the first time (it downloads Rust), a few minutes after that.": "Сборка компилирует игру: 20–30 минут и несколько ГБ на диске в первый раз (она скачивает Rust), затем несколько минут.",
|
||||
"Install and turn on native mods": "Установить и включить нативные моды",
|
||||
"Native mods compile into a game of their own. Installing one turns Native mods on: you then build the game with it in Settings > Native mods. The first build downloads Rust and can take 20 to 30 minutes; later builds are faster.": "Нативные моды компилируются в отдельную игру. Установка такого мода включает нативные моды: затем вы собираете с ним игру в «Настройки > Нативные моды». Первая сборка скачивает Rust и может занять 20–30 минут; следующие быстрее.",
|
||||
"Delete…": "Удалить…",
|
||||
"Keep": "Оставить",
|
||||
"Move to Trash": "В корзину",
|
||||
|
||||
@@ -2865,10 +2865,14 @@ pub struct Settings {
|
||||
/// The window's language. Unset follows the system's (`i18n::Language::system`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub language: Option<crate::i18n::Language>,
|
||||
/// The player turned Native mods (experimental) on (`native_mods`): mixin mods compiled into
|
||||
/// a game of their own. Off by default.
|
||||
/// Native mods (experimental) are on (`native_mods`): mixin mods compiled into a game of
|
||||
/// their own. Off until the player installs a native mod (`native_mods::turns_on`), or turns
|
||||
/// them on in Settings.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub native_mods: bool,
|
||||
/// The player has read what installing a native mod starts (`native_mods::needs_notice`).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub native_mods_noticed: bool,
|
||||
/// Developer options (hidden: seven clicks on the sidebar's launcher version reveal them):
|
||||
/// offline names play without an account that owns the game, in the window only
|
||||
/// (`account::Accounts::play_as_with`, `account::launch_for_game`). Never a download: the game still
|
||||
|
||||
@@ -60,6 +60,14 @@ pub fn known_index() -> Option<Arc<Index>> {
|
||||
.map(|(_, index)| index.clone())
|
||||
}
|
||||
|
||||
/// Whether the registry's project `id` is a native mod, by the index already read (never the
|
||||
/// network); false when none was read.
|
||||
pub fn is_native_mod(id: &str) -> bool {
|
||||
known_index()
|
||||
.and_then(|index| Some(index.project(id)?.kind == index::Kind::NativeMod))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// The index: the one fetched in the last ten minutes, else fetched again (and kept in the game
|
||||
/// folder), else the copy kept last time.
|
||||
pub fn index() -> Result<Arc<Index>, String> {
|
||||
|
||||
@@ -4,6 +4,7 @@ use clap::{Parser, Subcommand};
|
||||
use horizon_launcher::account::{self, Accounts};
|
||||
use horizon_launcher::backend::{self, Release};
|
||||
use horizon_launcher::instances;
|
||||
use horizon_launcher::native_mods;
|
||||
use horizon_launcher::server_list;
|
||||
use std::io::{IsTerminal, Write};
|
||||
use std::path::PathBuf;
|
||||
@@ -2111,6 +2112,17 @@ fn mods(action: ModsCmd, as_json: bool) -> Result<(), String> {
|
||||
println!(" not ported: {s}");
|
||||
}
|
||||
}
|
||||
// A native mod turns Native mods on, with the window's notice, once.
|
||||
let mut settings = backend::Settings::load();
|
||||
let list = installed::Installed::load(&home).entries;
|
||||
if !settings.native_mods && native_mods::turns_on(false, &list) {
|
||||
if !settings.native_mods_noticed {
|
||||
eprintln!("{}", native_mods::NOTICE);
|
||||
}
|
||||
settings.native_mods = true;
|
||||
settings.native_mods_noticed = true;
|
||||
settings.save();
|
||||
}
|
||||
}
|
||||
ModsCmd::Update { project, check, .. } => {
|
||||
let list = installed::Installed::load(&home).entries;
|
||||
|
||||
@@ -3244,7 +3244,7 @@ impl Launcher {
|
||||
.child(div().text_color(t.danger).child(tr("These mods will run with full access to your computer").to_string()))
|
||||
.child(tr("They can read, change or delete your files, and use your accounts and network, like any program you run. Build them only if you trust their authors: the Mods page names them.").to_string())
|
||||
.child(trf("Mods: {mods}", &[("mods", &list)]))
|
||||
.child(tr("The build compiles the game: about ten minutes and a few GB of disk the first time, a few minutes after that.").to_string());
|
||||
.child(tr("The build compiles the game: 20 to 30 minutes and a few GB of disk the first time (it downloads Rust), a few minutes after that.").to_string());
|
||||
let mut buttons = div().flex().gap(px(8.)).mt(px(4.));
|
||||
if mods.is_ok() {
|
||||
buttons = buttons.child(
|
||||
|
||||
@@ -83,6 +83,8 @@ pub(super) struct Mods {
|
||||
message: Option<Result<String, String>>,
|
||||
updates: HashMap<(SourceId, String), String>,
|
||||
confirm_remove: Option<String>,
|
||||
/// A native mod's Install waiting for the one-time notice (`native_mods::needs_notice`).
|
||||
native_notice: Option<(SourceId, String, Option<Project>)>,
|
||||
/// Java mods' checks, by source and project.
|
||||
pub(super) checks: HashMap<(SourceId, String), Checking>,
|
||||
/// Horizon registry projects' trust and capabilities, by id ([`horizon::trust_and_capabilities`]).
|
||||
@@ -164,6 +166,7 @@ impl Mods {
|
||||
world: None,
|
||||
busy: None,
|
||||
message: None,
|
||||
native_notice: None,
|
||||
updates: HashMap::new(),
|
||||
confirm_remove: None,
|
||||
checks: HashMap::new(),
|
||||
@@ -269,6 +272,7 @@ impl Launcher {
|
||||
let mut to_check = Vec::new();
|
||||
let mut loaded = Vec::new();
|
||||
let mut done = false;
|
||||
let mut native_on = false;
|
||||
while let Ok(event) = self.mods.rx.try_recv() {
|
||||
changed = true;
|
||||
let m = &mut self.mods;
|
||||
@@ -299,6 +303,7 @@ impl Launcher {
|
||||
m.busy = None;
|
||||
m.message = Some(result);
|
||||
m.installed = installed::Installed::load(&m.home()).entries;
|
||||
native_on = native_mods::turns_on(native_on, &m.installed);
|
||||
m.installed_list.reset();
|
||||
if modpack {
|
||||
// The Modpacks page reads its list again.
|
||||
@@ -326,6 +331,12 @@ impl Launcher {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A native mod installed: Native mods turn on, so Settings offers its Build.
|
||||
if native_on && !self.settings.native_mods {
|
||||
self.settings.native_mods = true;
|
||||
self.settings.native_mods_noticed = true;
|
||||
self.settings.save();
|
||||
}
|
||||
// `HORIZON_LAUNCHER_PROJECT=<slug>` (with `HORIZON_LAUNCHER_PAGE=mods`): opens that
|
||||
// project's page once a search finds it, for screenshots.
|
||||
if self.mods.browse.open.is_none() {
|
||||
@@ -578,6 +589,18 @@ impl Launcher {
|
||||
if self.mods.busy.is_some() {
|
||||
return;
|
||||
}
|
||||
// The first native mod: what it starts, before anything is installed.
|
||||
let first_native = source == SourceId::Horizon
|
||||
&& native_mods::needs_notice(
|
||||
self.settings.native_mods,
|
||||
self.settings.native_mods_noticed,
|
||||
)
|
||||
&& horizon::is_native_mod(&id);
|
||||
if first_native {
|
||||
self.mods.native_notice = Some((source, id, known));
|
||||
return;
|
||||
}
|
||||
self.mods.native_notice = None;
|
||||
self.mods.message = None;
|
||||
self.mods.busy = Some((id.clone(), format!("Asking {}…", source.label()), 0.));
|
||||
self.mods.updates.remove(&(source, id.clone()));
|
||||
@@ -849,6 +872,7 @@ impl Launcher {
|
||||
.child(header)
|
||||
.child(div().flex().flex_col().child(kinds).child(body))
|
||||
};
|
||||
let body = body.children(self.native_notice(cx));
|
||||
if let Some(project) = self.mods_project_page(cx) {
|
||||
return page(body.children(self.mods_busy_plate()).child(project));
|
||||
}
|
||||
@@ -926,6 +950,56 @@ impl Launcher {
|
||||
}
|
||||
|
||||
/// The install under way and the last install's answer.
|
||||
/// The one-time notice before the first native mod is installed: what the build takes. Its
|
||||
/// button installs the mod; Native mods then turn on.
|
||||
fn native_notice(&self, cx: &mut Context<Self>) -> Option<Div> {
|
||||
self.mods.native_notice.as_ref()?;
|
||||
let t = self.theme;
|
||||
Some(
|
||||
plate(&t)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(10.))
|
||||
.p(px(16.))
|
||||
.text_size(px(theme::UI.px))
|
||||
.text_color(t.ink)
|
||||
.child(tr(native_mods::NOTICE).to_string())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap(px(8.))
|
||||
.child(
|
||||
button(
|
||||
"native-notice-install",
|
||||
"Install and turn on native mods",
|
||||
Kind::Primary,
|
||||
&t,
|
||||
)
|
||||
.on_click(cx.listener(
|
||||
|this, _: &ClickEvent, _, cx| {
|
||||
this.settings.native_mods_noticed = true;
|
||||
this.settings.save();
|
||||
if let Some((source, id, known)) =
|
||||
this.mods.native_notice.take()
|
||||
{
|
||||
this.mods_install(source, id, known);
|
||||
}
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
button("native-notice-cancel", "Cancel", Kind::Secondary, &t).on_click(
|
||||
cx.listener(|this, _: &ClickEvent, _, cx| {
|
||||
this.mods.native_notice = None;
|
||||
cx.notify();
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn mods_busy_plate(&self) -> Vec<Div> {
|
||||
let t = self.theme;
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -33,6 +33,26 @@ pub const MIXINS_REV: &str = "0d6b103c70633d8fd9443d8d0acd3aadaacedb06";
|
||||
const PROFILE: &str = "release";
|
||||
const FEATURES: &str = "horizon-client/mods";
|
||||
|
||||
/// Whether Native mods are on after an install into a game folder whose list is `installed`:
|
||||
/// installing a native mod turns them on, so the player only has to build. A player who never
|
||||
/// installs one keeps them off; one already on stays on.
|
||||
pub fn turns_on(on: bool, installed: &[crate::browser::installed::Entry]) -> bool {
|
||||
on || installed.iter().any(|e| {
|
||||
e.file
|
||||
.strip_prefix(INSTALLED)
|
||||
.is_some_and(|rest| rest.starts_with('/'))
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether installing a native mod shows its notice first (what the first build takes): once,
|
||||
/// for a player whose Native mods are still off.
|
||||
pub fn needs_notice(on: bool, noticed: bool) -> bool {
|
||||
!on && !noticed
|
||||
}
|
||||
|
||||
/// The notice before the first native mod is installed.
|
||||
pub const NOTICE: &str = "Native mods compile into a game of their own. Installing one turns Native mods on: you then build the game with it in Settings > Native mods. The first build downloads Rust and can take 20 to 30 minutes; later builds are faster.";
|
||||
|
||||
/// Whether an installed version is a native-mods build.
|
||||
pub fn is_native(version: &str) -> bool {
|
||||
version.starts_with(PREFIX)
|
||||
@@ -778,6 +798,42 @@ pub fn prune(keep: &str) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Ludovic's decision (2026-10-29): installing a native mod turns Native mods on, after a
|
||||
/// notice shown once; players who never install one keep them off.
|
||||
#[test]
|
||||
fn installing_a_native_mod_turns_native_mods_on() {
|
||||
let entry = |file: &str| {
|
||||
serde_json::from_value::<crate::browser::installed::Entry>(serde_json::json!({
|
||||
"source": "horizon", "project": "p", "slug": "p", "title": "P", "kind": "mod",
|
||||
"version": "1", "number": "1", "file": file, "checksum": "", "page": ""
|
||||
}))
|
||||
.unwrap()
|
||||
};
|
||||
let plain = [
|
||||
entry("horizon-mods/zoom.hmod"),
|
||||
entry("resourcepacks/x.zip"),
|
||||
];
|
||||
assert!(!turns_on(false, &[]));
|
||||
assert!(!turns_on(false, &plain), "no native mod: they stay off");
|
||||
assert!(!turns_on(
|
||||
false,
|
||||
&[entry("horizon-native-mods-old/x.hnmod")]
|
||||
));
|
||||
let native = [
|
||||
plain[0].clone(),
|
||||
entry("horizon-native-mods/veinminer.hnmod"),
|
||||
];
|
||||
assert!(turns_on(false, &native));
|
||||
assert!(turns_on(true, &plain), "on stays on");
|
||||
// The notice: before the first one only.
|
||||
assert!(needs_notice(false, false));
|
||||
assert!(!needs_notice(false, true));
|
||||
assert!(!needs_notice(true, false));
|
||||
// Settings without the fields read as off and not noticed.
|
||||
let s: backend::Settings = serde_json::from_str("{}").unwrap();
|
||||
assert!(!s.native_mods && !s.native_mods_noticed);
|
||||
}
|
||||
|
||||
fn dir(name: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("horizon-native-{name}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
|
||||
Reference in new issue
Block a user