From c0323c7707bd9816daa3d49c54910b7d4445a87f Mon Sep 17 00:00:00 2001 From: Horizon agent Date: Thu, 5 Nov 2026 16:32:54 +0100 Subject: [PATCH 1/7] Mods: browse Horizon's registry and install content mods from inside the game Options > Mods > Browse Mods lists the registry's content mods for this game (the index the launcher reads), searchable by name, tag and category, with a page per project: authors, licence, categories, description and changelog, Install or Update, its switch once installed, and its page on the website. An install is checked as the launcher's (size, SHA-256, signature, manifest against the index) and recorded in mod-browser.json, so the launcher lists, updates and removes it. Co-Authored-By: Claude Opus 5.5 --- crates/horizon-client-render/src/lib.rs | 1 + .../horizon-client-render/src/mod_browser.rs | 180 ++++++++++ .../horizon-client-render/src/options_ui.rs | 213 ++++++++++- .../assets/horizon/lang/en_us.json | 18 + .../assets/horizon/lang/fr_fr.json | 18 + crates/horizon-client/src/client_lib.rs | 3 +- crates/horizon-client/src/game.rs | 1 + crates/horizon-client/src/game/mod_browser.rs | 158 ++++++++ crates/horizon-client/src/game/mods.rs | 2 + crates/horizon-client/src/game/options.rs | 10 + .../horizon-client/src/game/server_packs.rs | 1 + .../horizon-client/src/game/startup_menu.rs | 22 ++ crates/horizon-client/src/mod_registry.rs | 336 ++++++++++++++++++ .../horizon-client/src/mod_registry/tests.rs | 165 +++++++++ 14 files changed, 1116 insertions(+), 12 deletions(-) create mode 100644 crates/horizon-client-render/src/mod_browser.rs create mode 100644 crates/horizon-client/src/game/mod_browser.rs create mode 100644 crates/horizon-client/src/mod_registry.rs create mode 100644 crates/horizon-client/src/mod_registry/tests.rs diff --git a/crates/horizon-client-render/src/lib.rs b/crates/horizon-client-render/src/lib.rs index a14fd35337..4c946e1207 100644 --- a/crates/horizon-client-render/src/lib.rs +++ b/crates/horizon-client-render/src/lib.rs @@ -63,6 +63,7 @@ pub mod menu_blur; pub mod mesher; pub mod minimap; pub mod mipmap; +pub mod mod_browser; pub mod mod_list; pub mod model_library; pub mod models; diff --git a/crates/horizon-client-render/src/mod_browser.rs b/crates/horizon-client-render/src/mod_browser.rs new file mode 100644 index 0000000000..29f563bab5 --- /dev/null +++ b/crates/horizon-client-render/src/mod_browser.rs @@ -0,0 +1,180 @@ +//! What Options > Mods > Browse mods shows (docs/design/mod-hotload.md): the +//! registry's content mods this game installs, a search over them, and the one +//! whose page is open. The game fills it from a worker +//! (`horizon_client::mod_registry`, `game/mod_browser.rs`); the options screen +//! only reads it and asks for installs through its events. + +/// One project of the registry, as the list and its page show it. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Listing { + pub id: String, + pub name: String, + pub summary: String, + /// Markdown, shown as plain text. + pub description: String, + pub authors: Vec, + pub license: String, + pub categories: Vec, + pub tags: Vec, + /// The version this game would install, its date and changelog. + pub version: String, + pub published: String, + pub changelog: String, + pub downloads: u64, + /// The project's page on the website. + pub page: String, + /// The version installed in this game folder, if any. + pub installed: Option, +} + +impl Listing { + /// Whether a search for `query` (already lower case) finds it: its name, + /// id, summary, authors, categories or tags. + pub fn matches(&self, query: &str) -> bool { + let query = query.trim(); + query.is_empty() + || [&self.name, &self.id, &self.summary] + .into_iter() + .chain(&self.authors) + .chain(&self.categories) + .chain(&self.tags) + .any(|field| field.to_lowercase().contains(query)) + } + + /// Installed, but not the version the registry offers now. + pub fn has_update(&self) -> bool { + self.installed + .as_ref() + .is_some_and(|installed| *installed != self.version) + } +} + +/// Where the browser is. +#[derive(Clone, Debug, Default, PartialEq)] +pub enum Status { + /// Not asked yet. + #[default] + Idle, + /// The index is being read. + Loading, + /// The projects are listed. + Ready, + /// The index couldn't be read: why, in English (shown inside a translated line). + Failed(String), +} + +#[derive(Clone, Debug, Default)] +pub struct ModBrowser { + pub status: Status, + pub listings: Vec, + /// The project an install runs for. + pub installing: Option, + /// The last install's outcome, already translated, for the page it ran on. + pub message: Option<(String, bool)>, +} + +impl ModBrowser { + /// The listings a search for `query` finds, by index into `listings`. + pub fn found(&self, query: &str) -> Vec { + let query = query.to_lowercase(); + (0..self.listings.len()) + .filter(|&i| self.listings[i].matches(&query)) + .collect() + } + + pub fn listing(&self, id: &str) -> Option<&Listing> { + self.listings.iter().find(|l| l.id == id) + } +} + +/// A project's markdown description as lines of plain text for the page: +/// heading marks, emphasis and list bullets dropped, links shown as their text, +/// blank lines and fences left out. +pub fn plain_lines(markdown: &str) -> Vec { + let mut out = Vec::new(); + for line in markdown.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with("```") || line.starts_with("