Launcher and horizon-server reach the engine through horizon-platform

Layer plan M6: horizon-platform gains feature-gated re-exports (version,
no_ai, prepared_files always; world, protocol, prepare, server-files behind
features) and the two Horizon-layer crates use them instead of depending on
horizon-core, horizon-prepare, horizon-protocol, horizon-world and
horizon-server-files. Seven layers.json exceptions go.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Horizon agentandClaude Opus 5.5 committed 2026-11-09 15:14:02 +01:00
1 parent 21a750533e
commit 92886cd964
37 files changed
+155 -118

No files matched your search

+16
View File
@@ -6,8 +6,24 @@ license.workspace = true
rust-version.workspace = true
description = "The platform's public API: what the Horizon layer and mods may use of the engine (docs/layers.md#platform)"
[features]
# Each part of the engine a Horizon crate may reach, so a crate builds only the engine it uses.
# level.dat, servers.dat and region files: NBT, compression, region read and write.
world = ["dep:horizon-world"]
# A server's address after its SRV record, and the offline-mode UUID of a name.
protocol = ["world", "dep:horizon-protocol"]
# Preparing the game's files from the player's own Minecraft install.
prepare = ["dep:horizon-prepare"]
# A server's own files: server.properties, the op, white and ban lists, the user cache.
server-files = ["dep:horizon-server-files"]
[dependencies]
horizon-core = { path = "../horizon-core" }
horizon-system = { path = "../horizon-system" }
horizon-world = { path = "../horizon-world", optional = true }
horizon-protocol = { path = "../horizon-protocol", optional = true }
horizon-prepare = { path = "../horizon-prepare", optional = true }
horizon-server-files = { path = "../horizon-server-files", optional = true }
[lints]
workspace = true
+33 -1
View File
@@ -4,8 +4,40 @@
//! comes through here.
//!
//! Today this re-exports engine modules whole. Each one gets its own stable types as the split
//! goes on (docs/layers.md#plan, M6).
//! goes on (docs/layers.md#plan, M6). The parts that pull in large engine crates sit behind
//! features (`world`, `protocol`, `prepare`, `server-files`), so a crate builds only what it uses.
/// The machine the game runs on (`horizon-system`): OS, CPU, memory, drive, power, GPU maker and
/// memory.
pub use horizon_system as system;
/// The pinned Minecraft version: its id, protocol and data version.
pub use horizon_core::version;
/// The player's "no AI" switch, which the launcher, the game and their agent tools share.
pub use horizon_core::no_ai;
/// The prepared folder's file of files (`prepared_files::FILE_NAME`).
pub use horizon_core::prepared_files;
/// Vanilla's world files, as tools outside the game read and write them.
#[cfg(feature = "world")]
pub mod world {
pub use horizon_core::position::ChunkPos;
pub use horizon_world::{compression, nbt, region};
}
/// What a server list and a profile head need of the network protocol.
#[cfg(feature = "protocol")]
pub mod protocol {
pub use horizon_protocol::crypto::offline_uuid;
pub use horizon_protocol::server_redirect;
}
/// The preparer: the game's files from the player's own Minecraft install.
#[cfg(feature = "prepare")]
pub use horizon_prepare as prepare;
/// A server's own files: `server.properties`, the op, white and ban lists, the user cache.
#[cfg(feature = "server-files")]
pub use horizon_server_files as server_files;
+5 -6
View File
@@ -7,7 +7,10 @@ rust-version.workspace = true
description = "Host a Horizon server: server.toml, the CLI and the supervisor that runs the game headless (docs/server.md)"
[dependencies]
horizon-server-files = { path = "../horizon-server-files" }
# The engine, through the platform's public API (docs/layers.md): server.properties and the user lists, the
# version `server/status` reports, and region files for [prune_chunks] (prune.rs), read and rewritten as vanilla's
# RegionFile does.
horizon-platform = { path = "../horizon-platform", features = ["server-files", "world"] }
# `horizon-server mcp`: the Model Context Protocol, shared with horizon-ctl and the launcher.
horizon-mcp = { path = "../horizon-mcp" }
# server.toml's [anticheat], which the game reads: checked here before every start.
@@ -30,13 +33,9 @@ toml_edit = "0.25"
# lockfile, ureq through horizon-server-files).
horizon-release-sign = { path = "../horizon-release-sign" }
sha2 = "0.10"
# Vanilla's management protocol: the WebSocket handshake's SHA-1 and Base64 (both already in the lockfile), and
# the version `server/status` reports.
# Vanilla's management protocol: the WebSocket handshake's SHA-1 and Base64 (both already in the lockfile).
sha1 = "0.10"
base64 = "0.22"
horizon-core = { path = "../horizon-core" }
# [prune_chunks]: region files read and rewritten as vanilla's RegionFile does (prune.rs).
horizon-world = { path = "../horizon-world" }
# Its TLS (on by default, as vanilla's): rustls on ring, both already in the lockfile through ureq; the keystore
# is a PKCS12 file, as vanilla's, and one is generated (self-signed) when the owner sets none.
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
+3 -3
View File
@@ -10,7 +10,7 @@ use crate::{
engine,
http::Response,
};
use horizon_server_files::user_lists::{
use horizon_platform::server_files::user_lists::{
BanList, IpBanList, OpList, UserList, WhiteList, format_date,
};
use serde_json::{Value, json};
@@ -189,7 +189,7 @@ fn stopped_command(action: Action, body: &Value) -> Option<(&'static str, Vec<&s
}
/// A list from its file in the server folder; missing or broken reads as empty, as the game would start.
fn load<E: horizon_server_files::user_lists::Entry>(dir: &Path, file: &str) -> UserList<E> {
fn load<E: horizon_platform::server_files::user_lists::Entry>(dir: &Path, file: &str) -> UserList<E> {
let mut list = UserList::new(dir.join(file));
let _ = list.load();
list
@@ -313,7 +313,7 @@ fn uuid(id: u128) -> String {
fn whitelist_on(dir: &Path) -> bool {
std::fs::read(dir.join(crate::config::PROPERTIES))
.ok()
.and_then(|b| horizon_server_files::java_properties::Properties::load_file(&b).ok())
.and_then(|b| horizon_platform::server_files::java_properties::Properties::load_file(&b).ok())
.and_then(|p| p.get("white-list").map(|v| v == "true"))
.or_else(|| {
crate::config::Config::load(dir)
+2 -2
View File
@@ -583,8 +583,8 @@ pub fn same_token(a: &str, b: &str) -> bool {
}
/// Writes a file only its owner can read: through a private temporary file renamed over it
/// (`horizon_server_files::private_file`), so an older, wider-readable file never keeps the token.
pub use horizon_server_files::private_file::write_private;
/// (`horizon_platform::server_files::private_file`), so an older, wider-readable file never keeps the token.
pub use horizon_platform::server_files::private_file::write_private;
/// Removes the endpoint file when the supervisor ends.
pub struct Endpoint(PathBuf);
+1 -1
View File
@@ -30,7 +30,7 @@ fn stamp(time: SystemTime) -> String {
let secs = time
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs() as i64);
let (y, m, d) = horizon_server_files::java_properties::civil_from_days(secs.div_euclid(86_400));
let (y, m, d) = horizon_platform::server_files::java_properties::civil_from_days(secs.div_euclid(86_400));
let s = secs.rem_euclid(86_400);
format!(
"{y:04}-{m:02}-{d:02}_{:02}-{:02}-{:02}",
+1 -1
View File
@@ -4,7 +4,7 @@
//! start the supervisor writes them into `server.properties`, which the game reads exactly as vanilla's
//! server does; what the game changes there itself (`/whitelist on`) is copied back afterwards.
use horizon_server_files::{
use horizon_platform::server_files::{
java_properties::{Properties, date_string, now_millis},
properties::{DIFFICULTIES, GAME_MODES, ServerProperties, SystemEntropy},
};
+1 -1
View File
@@ -115,7 +115,7 @@ fn start_failure(program: &Path, code: Option<i32>, stderr: &str) -> String {
}
/// The Minecraft version this server is pinned to; prepared game files must be stamped with it.
pub const GAME_VERSION: &str = horizon_core::version::VERSION_ID;
pub const GAME_VERSION: &str = horizon_platform::version::VERSION_ID;
/// Where the prepared game files may be, in the order they are tried.
pub fn resource_candidates(config: &Config, dir: &Path, program: Option<&Path>) -> Vec<PathBuf> {
+1 -1
View File
@@ -3,7 +3,7 @@
//! Every question has a flag, so it also runs unattended (Docker, scripts).
use crate::{config, config::Config, engine};
use horizon_server_files::{
use horizon_platform::server_files::{
eula,
java_properties::now_millis,
user_lists::{OpEntry, OpList, WhiteEntry, WhiteList},
+5 -5
View File
@@ -8,7 +8,7 @@
//! reloads it (`whitelist reload`), as it would be written while the server is stopped.
use crate::{config, supervisor};
use horizon_server_files::user_lists::{
use horizon_platform::server_files::user_lists::{
BanEntry, BanList, IpBanList, NameAndId, OpEntry, OpList, WhiteEntry, WhiteList,
};
use std::path::Path;
@@ -244,7 +244,7 @@ fn add_offline(dir: &Path, name: &str) -> Result<(), String> {
/// The profile a running server would store for `name`: the offline one, or in online mode the account the
/// services know by that name.
pub fn account(name: &str, online: bool) -> Result<NameAndId, String> {
horizon_server_files::profiles::resolve(name, online).ok_or_else(|| {
horizon_platform::server_files::profiles::resolve(name, online).ok_or_else(|| {
if online {
format!("That player does not exist (no Minecraft account is named {name}, or the services didn't answer)")
} else {
@@ -259,9 +259,9 @@ fn listed<'a>(mut users: impl Iterator<Item = &'a NameAndId>, name: &str) -> Opt
}
/// A list from its file; a missing file is an empty list, a broken one an error (vanilla would replace it).
fn load<E: horizon_server_files::user_lists::Entry>(
mut list: horizon_server_files::user_lists::UserList<E>,
) -> Result<horizon_server_files::user_lists::UserList<E>, String> {
fn load<E: horizon_platform::server_files::user_lists::Entry>(
mut list: horizon_platform::server_files::user_lists::UserList<E>,
) -> Result<horizon_platform::server_files::user_lists::UserList<E>, String> {
// UserList::load deliberately tolerates I/O errors for the running game's vanilla behavior.
// A stopped command must not edit an empty replacement for a file it could not read.
let text = match std::fs::read_to_string(&list.file) {
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::{
management_schema as schema, management_tls,
websocket::{Link, Socket},
};
use horizon_server_files::properties::{ServerProperties, SystemEntropy, generate_secret_key};
use horizon_platform::server_files::properties::{ServerProperties, SystemEntropy, generate_secret_key};
use serde_json::{Value, json};
use std::{
net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs},
+10 -10
View File
@@ -9,7 +9,7 @@ use crate::{
engine,
management::{self, Failure, INTERNAL_ERROR, invalid_params},
};
use horizon_server_files::properties::{ServerProperties, SystemEntropy};
use horizon_platform::server_files::properties::{ServerProperties, SystemEntropy};
use serde_json::{Value, json};
use std::path::Path;
@@ -1007,8 +1007,8 @@ impl Game<'_> {
/// `ServerStateService.status`.
fn status(&self) -> Value {
let version = json!({
"name": horizon_core::version::VERSION_ID,
"protocol": horizon_core::version::PROTOCOL_VERSION,
"name": horizon_platform::version::VERSION_ID,
"protocol": horizon_platform::version::PROTOCOL_VERSION,
});
let players = if self.running() {
self.online().ok()
@@ -1089,17 +1089,17 @@ impl Game<'_> {
struct Lists<'a>(&'a Path);
impl Lists<'_> {
fn load<E: horizon_server_files::user_lists::Entry>(
fn load<E: horizon_platform::server_files::user_lists::Entry>(
&self,
file: &str,
) -> horizon_server_files::user_lists::UserList<E> {
let mut list = horizon_server_files::user_lists::UserList::new(self.0.join(file));
) -> horizon_platform::server_files::user_lists::UserList<E> {
let mut list = horizon_platform::server_files::user_lists::UserList::new(self.0.join(file));
let _ = list.load();
list
}
fn allowlist(&self) -> Value {
let list: horizon_server_files::user_lists::WhiteList = self.load("whitelist.json");
let list: horizon_platform::server_files::user_lists::WhiteList = self.load("whitelist.json");
list.entries()
.iter()
.map(|e| json!({ "id": uuid(e.user.id), "name": e.user.name }))
@@ -1107,7 +1107,7 @@ impl Lists<'_> {
}
fn operators(&self) -> Value {
let list: horizon_server_files::user_lists::OpList = self.load("ops.json");
let list: horizon_platform::server_files::user_lists::OpList = self.load("ops.json");
list.entries()
.iter()
.map(|e| {
@@ -1121,7 +1121,7 @@ impl Lists<'_> {
}
fn bans(&self) -> Value {
let list: horizon_server_files::user_lists::BanList = self.load("banned-players.json");
let list: horizon_platform::server_files::user_lists::BanList = self.load("banned-players.json");
list.entries()
.iter()
.map(|b| {
@@ -1132,7 +1132,7 @@ impl Lists<'_> {
}
fn ip_bans(&self) -> Value {
let list: horizon_server_files::user_lists::IpBanList = self.load("banned-ips.json");
let list: horizon_platform::server_files::user_lists::IpBanList = self.load("banned-ips.json");
list.entries()
.iter()
.map(|b| ban(("ip", json!(b.user)), &b.reason, &b.source, b.expires))
+1 -1
View File
@@ -5,7 +5,7 @@
//! one instead (a self-signed certificate in `.horizon/`), so turning the protocol on is enough, and logs the
//! certificate's fingerprint for clients to pin.
use horizon_server_files::properties::{SystemEntropy, generate_secret_key};
use horizon_platform::server_files::properties::{SystemEntropy, generate_secret_key};
use p12_keystore::{Certificate, KeyStore, KeyStoreEntry, PrivateKeyChain};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use sha2::{Digest, Sha256};
+4 -4
View File
@@ -12,7 +12,7 @@
//! copied as they were) and a file left empty is deleted. Nothing else in the world is touched: not
//! level.dat, player data, stats or advancements.
use horizon_world::{
use horizon_platform::world::{
nbt::{self, Limits, Tag},
region::{ChunkRecord, RegionFile, RegionWriter, position_from_path},
};
@@ -344,7 +344,7 @@ struct Tally {
fn spawn_chunk(world: &Path) -> Chunk {
let read = || -> Option<Chunk> {
let bytes = fs::read(world.join("level.dat")).ok()?;
let bytes = horizon_world::compression::Compression::Gzip
let bytes = horizon_platform::world::compression::Compression::Gzip
.decode(&bytes, Limits::saved().max_bytes)
.ok()?;
let (root, _) = nbt::read_named(&bytes, Limits::saved()).ok()?;
@@ -873,8 +873,8 @@ pub fn write_report(dir: &Path, outcome: &Outcome) -> io::Result<PathBuf> {
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use horizon_core::position::ChunkPos;
use horizon_world::{compression::Compression, nbt::NamedTag};
use horizon_platform::world::ChunkPos;
use horizon_platform::world::{compression::Compression, nbt::NamedTag};
fn temp(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("horizon-prune-{name}-{}", std::process::id()));
@@ -93,7 +93,7 @@ pub(super) fn recover_with(
return clean(folder);
}
let journal: Journal = serde_json::from_slice(&fs::read(journal)?)?;
let valid_name = horizon_world::region::position_from_path(Path::new(&journal.name)).is_ok()
let valid_name = horizon_platform::world::region::position_from_path(Path::new(&journal.name)).is_ok()
&& Path::new(&journal.name)
.file_name()
.is_some_and(|n| n == journal.name.as_str());
+2 -2
View File
@@ -15,7 +15,7 @@ use crate::{
engine,
supervisor::note,
};
use horizon_server_files::properties::{ServerProperties, SystemEntropy};
use horizon_platform::server_files::properties::{ServerProperties, SystemEntropy};
use std::{
collections::HashMap,
io,
@@ -308,7 +308,7 @@ fn read_info(shared: &Shared) -> Option<Info> {
Some(Info {
motd: p.motd,
world: p.level_name,
version: horizon_core::version::VERSION_ID.to_owned(),
version: horizon_platform::version::VERSION_ID.to_owned(),
players,
max_players: status["max_players"]
.as_i64()
+1 -1
View File
@@ -15,7 +15,7 @@ use crate::{
engine,
supervisor::note,
};
use horizon_server_files::properties::{ServerProperties, SystemEntropy};
use horizon_platform::server_files::properties::{ServerProperties, SystemEntropy};
use serde_json::Value;
use std::{
io::{self, Read, Write},
+3 -6
View File
@@ -40,12 +40,13 @@ sys-locale = "0.3"
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
horizon-theme = { path = "../crates/horizon-theme" }
horizon-core = { path = "../crates/horizon-core" }
# The engine, through the platform's public API (docs/layers.md): the pinned version, No AI, the preparer,
# level.dat and servers.dat (the worlds and server lists of instances), and the official server's SRV record.
horizon-platform = { path = "../crates/horizon-platform", features = ["prepare", "protocol", "world"] }
horizon-cosmetics = { path = "../crates/horizon-cosmetics" }
glam = "0.30"
# `horizon-launcher mcp`: the Model Context Protocol, shared with horizon-ctl and horizon-server.
horizon-mcp = { path = "../crates/horizon-mcp" }
horizon-prepare = { path = "../crates/horizon-prepare" }
# Join a friend: invite codes (the game does the joining).
horizon-friends = { path = "../crates/horizon-friends" }
horizon-bench = { path = "../crates/horizon-bench" }
@@ -59,10 +60,6 @@ horizon-shader-api = { path = "../crates/horizon-shader-api" }
horizon-release-sign = { path = "../crates/horizon-release-sign" }
# Opt-in data collection: the consent, the outbox and its upload (docs/telemetry.md).
horizon-telemetry = { path = "../crates/horizon-telemetry" }
# The official server's count follows its SRV record, as the game does (server_redirect).
horizon-protocol = { path = "../crates/horizon-protocol" }
# level.dat and servers.dat (the worlds and server lists of instances)
horizon-world = { path = "../crates/horizon-world" }
# Deleting a world moves it to the system's trash
trash = "5"
# Backup names carry the local time, as vanilla's do
+1 -1
View File
@@ -1,4 +1,4 @@
//! Settings' AI rows: the No AI switch (`horizon_core::no_ai`, shared with the game) and "Connect
//! Settings' AI rows: the No AI switch (`horizon_platform::no_ai`, shared with the game) and "Connect
//! an AI agent", the ready-made MCP config of each common client with Copy buttons, a one-click
//! add where the client has a CLI (run only on that click, its command shown first), and a test
//! that starts the MCP server and lists its tools. With No AI on, only the switch shows.
+39 -39
View File
@@ -1388,7 +1388,7 @@ fn read_json(path: &Path) -> Option<serde_json::Value> {
fn game_version_at(dir: &Path) -> String {
read_json(&dir.join("research-lock.json"))
.and_then(|v| v["minecraft"]["version"].as_str().map(String::from))
.unwrap_or_else(|| horizon_core::VANILLA_VERSION.into())
.unwrap_or_else(|| horizon_platform::version::VANILLA_VERSION.into())
}
pub fn resources(tag: &str) -> PathBuf {
@@ -1620,11 +1620,11 @@ const LEGACY_EXPORTERS: [&str; 18] = [
];
/// A version folder's preparer as the launchers before `OUTPUT_VERSION` compared it: the digest
/// of its sources (`horizon_prepare::sources_sha256`) and of the exporters it ran, line endings
/// of its sources (`horizon_platform::prepare::sources_sha256`) and of the exporters it ran, line endings
/// as checked out ignored.
fn legacy_fingerprint(dir: &Path) -> Option<String> {
use sha2::Digest;
let sources = horizon_prepare::sources_sha256(&dir.join(PREPARER_CRATE))?;
let sources = horizon_platform::prepare::sources_sha256(&dir.join(PREPARER_CRATE))?;
let mut hash = sha2::Sha256::new();
hash.update(sources.as_bytes());
for name in LEGACY_EXPORTERS {
@@ -1726,7 +1726,7 @@ pub(crate) fn copy_dir_except(from: &Path, to: &Path, skip: Option<&Path>) -> st
if entry.file_type()?.is_dir() {
copy_dir_except(&entry.path(), &target, skip)?;
} else {
horizon_prepare::clone_or_copy(&entry.path(), &target)?;
horizon_platform::prepare::clone_or_copy(&entry.path(), &target)?;
}
}
Ok(())
@@ -1762,7 +1762,7 @@ fn reuse_prepared(versions: &Path, tag: &str) -> Option<String> {
let partial = to.with_file_name(format!(".{version}.partial-{}-{n}", std::process::id()));
let _ = fs::remove_dir_all(&partial);
// Without the data file: it names its own root, so the copy could not use it.
let data_file = from.join(horizon_core::prepared_files::FILE_NAME);
let data_file = from.join(horizon_platform::prepared_files::FILE_NAME);
let copied = copy_dir_except(&from, &partial, Some(&data_file)).and_then(|_| {
if to.join(RESOURCES_MANIFEST).exists() {
return Ok(false); // Prepared meanwhile, by another copy or the player.
@@ -1806,7 +1806,7 @@ pub(crate) fn reuse_prepared_for(tag: &str) {
/// The game still writes it when this didn't finish. Only looks for the file, as
/// `prepared_files::open` keeps it open for the whole run, which would stop a removal on Windows.
fn write_data_file(resources: &Path) -> std::io::Result<()> {
use horizon_core::prepared_files;
use horizon_platform::prepared_files;
if !prepared_at(resources) || resources.join(prepared_files::FILE_NAME).exists() {
return Ok(());
}
@@ -1905,12 +1905,12 @@ fn complete_reports(versions: &Path, tag: &str) -> bool {
/// What a prepared version may lack that the launcher adds without preparing again, each copied
/// from the player's own Minecraft asset store as `horizon-prepare <kind>` does: what the
/// player misses without it, its kind (`horizon_prepare::missing_extras`, the same check as
/// player misses without it, its kind (`horizon_platform::prepare::missing_extras`, the same check as
/// `horizon-prepare --check`), and the copy.
type Completion = (
&'static str,
&'static str,
fn(&Path, Option<&Path>, horizon_prepare::Log) -> Result<PathBuf, String>,
fn(&Path, Option<&Path>, horizon_platform::prepare::Log) -> Result<PathBuf, String>,
);
const COMPLETIONS: [Completion; 5] = [
@@ -1918,15 +1918,15 @@ const COMPLETIONS: [Completion; 5] = [
(
"Programmer Art and High Contrast",
"packs",
horizon_prepare::packs,
horizon_platform::prepare::packs,
),
// A preparation that could not read the asset store keeps the client jar's 1x1 grey
// placeholders, its English alone, and no sounds (#1867: Prism Launcher installs prepared
// since 0.2.48).
("The title panorama", "panorama", horizon_prepare::panorama),
("The Unifont glyphs", "fonts", horizon_prepare::fonts),
("The languages", "languages", horizon_prepare::languages),
("The sounds", "sounds", horizon_prepare::sounds),
("The title panorama", "panorama", horizon_platform::prepare::panorama),
("The Unifont glyphs", "fonts", horizon_platform::prepare::fonts),
("The languages", "languages", horizon_platform::prepare::languages),
("The sounds", "sounds", horizon_platform::prepare::sounds),
];
/// Adds what a prepared version lacks of `COMPLETIONS`: a new version reuses the resources of the
@@ -1948,7 +1948,7 @@ fn refill_own_store(tag: &str) {
let own = data_dir().join("minecraft");
let resources = resources_at(&versions_dir().join(tag));
if !own.join("assets/indexes").is_dir()
|| !horizon_prepare::missing_extras(&resources)
|| !horizon_platform::prepare::missing_extras(&resources)
.iter()
.any(|(kind, _)| matches!(*kind, "languages" | "sounds"))
|| ASKED.swap(true, std::sync::atomic::Ordering::Relaxed)
@@ -1989,7 +1989,7 @@ fn complete_assets_in(
if !resources.join(RESOURCES_MANIFEST).exists() {
return Vec::new();
}
let missing = horizon_prepare::missing_extras(&resources);
let missing = horizon_platform::prepare::missing_extras(&resources);
COMPLETIONS
.iter()
.filter(|(_, kind, _)| missing.iter().any(|(m, _)| m == kind))
@@ -2025,7 +2025,7 @@ fn complete_assets_in(
// The data file answers "missing" for what it lacks, so one from before these copies would
// hide them. Only when something was added: a version that stays lacking keeps its file.
if lacking().len() < lacked {
let data_file = resources.join(horizon_core::prepared_files::FILE_NAME);
let data_file = resources.join(horizon_platform::prepared_files::FILE_NAME);
if let Err(e) = fs::remove_file(&data_file) {
if e.kind() != std::io::ErrorKind::NotFound {
log(format!("{}: not removed: {e}", data_file.display()));
@@ -2041,7 +2041,7 @@ fn complete_assets_in(
fn minecraft_folders() -> Vec<PathBuf> {
let mut folders: Vec<PathBuf> = Settings::load().minecraft_dir.into_iter().collect();
folders.extend([
horizon_prepare::artifacts::minecraft_dir(None),
horizon_platform::prepare::artifacts::minecraft_dir(None),
data_dir().join("linked-minecraft"),
data_dir().join("minecraft"),
]);
@@ -2092,7 +2092,7 @@ fn python() -> Option<&'static str> {
/// How a version's resources are prepared.
pub enum Preparer {
/// horizon-prepare, linked into the launcher: the version ships the same preparer sources.
InProcess(horizon_prepare::Options),
InProcess(horizon_platform::prepare::Options),
/// The version's own preparer: its horizon-prepare binary when its sources differ from the
/// launcher's (a newer or older release), or the Python script of versions made before it.
Process(Command),
@@ -2308,9 +2308,9 @@ fn java(
log: &dyn Fn(String),
) -> Result<PathBuf, String> {
let find = if compiler {
horizon_prepare::java::find
horizon_platform::prepare::java::find
} else {
horizon_prepare::java::find_runtime
horizon_platform::prepare::java::find_runtime
};
if let Ok(jdk) = find(major, &[]) {
return Ok(jdk.home());
@@ -2318,7 +2318,7 @@ fn java(
let mut runtimes: Vec<PathBuf> = Vec::new();
for home in games
.iter()
.flat_map(|game| horizon_prepare::java::minecraft_runtimes(&game.join("runtime")))
.flat_map(|game| horizon_platform::prepare::java::minecraft_runtimes(&game.join("runtime")))
{
if !runtimes.contains(&home) {
runtimes.push(home);
@@ -2459,7 +2459,7 @@ pub fn preparer(
set_prepare_step(PrepareStep::Files);
let version = game_version(tag);
let dir = version_dir(tag);
let reused = horizon_prepare::artifacts::reuse_imported(&dir, &|line| log(line.to_string()))?;
let reused = horizon_platform::prepare::artifacts::reuse_imported(&dir, &|line| log(line.to_string()))?;
let chosen = minecraft.to_path_buf();
// Resolved even when the game files are reused: the assets still come from the install.
let minecraft = {
@@ -2531,7 +2531,7 @@ pub fn preparer(
set_prepare_step(PrepareStep::Java);
let java_home = java(
major,
!horizon_prepare::exporters::available(&dir),
!horizon_platform::prepare::exporters::available(&dir),
&games,
&manifest,
log,
@@ -2559,10 +2559,10 @@ pub fn preparer(
.map_err(|e| e.to_string())?;
}
if let Some(binary) = binary {
if horizon_prepare::sources_sha256(&dir.join(PREPARER_CRATE)).as_deref()
== Some(horizon_prepare::SOURCES_SHA256)
if horizon_platform::prepare::sources_sha256(&dir.join(PREPARER_CRATE)).as_deref()
== Some(horizon_platform::prepare::SOURCES_SHA256)
{
let options = horizon_prepare::Options {
let options = horizon_platform::prepare::Options {
root: dir,
import: !reused,
minecraft_dir: Some(minecraft.clone()),
@@ -2600,8 +2600,8 @@ pub fn preparer(
/// the sounds where the launcher's preparer puts them.
pub fn sounds(tag: &str, minecraft: &Path, log: &dyn Fn(String)) {
let dir = version_dir(tag);
let own = horizon_prepare::sources_sha256(&dir.join(PREPARER_CRATE)).as_deref()
== Some(horizon_prepare::SOURCES_SHA256);
let own = horizon_platform::prepare::sources_sha256(&dir.join(PREPARER_CRATE)).as_deref()
== Some(horizon_platform::prepare::SOURCES_SHA256);
let binary = preparer_binary(&dir).filter(|binary| {
!own && crate::process::command(binary)
.arg("--help")
@@ -2631,7 +2631,7 @@ pub fn sounds(tag: &str, minecraft: &Path, log: &dyn Fn(String)) {
}
}),
None => std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
horizon_prepare::sounds(&dir, Some(minecraft), &|line| log(line.to_string()))
horizon_platform::prepare::sounds(&dir, Some(minecraft), &|line| log(line.to_string()))
}))
.unwrap_or_else(|_| Err("The sound copy stopped unexpectedly".into()))
.map(|_| ()),
@@ -2644,11 +2644,11 @@ pub fn sounds(tag: &str, minecraft: &Path, log: &dyn Fn(String)) {
/// Runs `Preparer::InProcess` on this thread, each progress line to `log`. A panic is an
/// error like any other, so the window never waits on a preparation that died.
pub fn prepare_in_process(
options: &horizon_prepare::Options,
options: &horizon_platform::prepare::Options,
log: &dyn Fn(String),
) -> Result<(), String> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
horizon_prepare::prepare(options, &|line| log(line.to_string()))
horizon_platform::prepare::prepare(options, &|line| log(line.to_string()))
}))
.unwrap_or_else(|_| Err("The preparation stopped unexpectedly".into()))
.map(|_| ())
@@ -2671,15 +2671,15 @@ pub fn game_home() -> PathBuf {
.join("horizon")
}
/// Whether No AI is on: every AI feature hidden and off (`horizon_core::no_ai`, kept in the
/// Whether No AI is on: every AI feature hidden and off (`horizon_platform::no_ai`, kept in the
/// game's `horizon-extras.txt` so the game reads the same switch).
pub fn no_ai() -> bool {
horizon_core::no_ai::is_on(&game_home())
horizon_platform::no_ai::is_on(&game_home())
}
/// Turns No AI on or off for the launcher and the game.
pub fn set_no_ai(on: bool) -> Result<(), String> {
horizon_core::no_ai::set(&game_home(), on).map_err(|e| format!("Could not save No AI: {e}"))
horizon_platform::no_ai::set(&game_home(), on).map_err(|e| format!("Could not save No AI: {e}"))
}
// ----- Logs and crash reports ---------------------------------------------------------------------
@@ -3140,7 +3140,7 @@ pub fn minecraft_dir(requested: Option<PathBuf>) -> Result<PathBuf, String> {
Ok(requested
.or(Settings::load().minecraft_dir)
.or_else(default_minecraft_dir)
.unwrap_or_else(|| horizon_prepare::artifacts::minecraft_dir(None)))
.unwrap_or_else(|| horizon_platform::prepare::artifacts::minecraft_dir(None)))
}
/// The server folder: the one given, the one chosen in the launcher, or `server/` in the data folder.
@@ -6431,7 +6431,7 @@ pub fn {f}("
#[test]
fn a_prepared_version_gets_a_data_file_that_holds_its_files() {
use horizon_core::prepared_files::FILE_NAME;
use horizon_platform::prepared_files::FILE_NAME;
let resources =
std::env::temp_dir().join(format!("horizon-launcher-data-file-{}", std::process::id()));
let _ = fs::remove_dir_all(&resources);
@@ -6500,7 +6500,7 @@ pub fn {f}("
release("v2026.10.12", "26.2");
prepared("v2026.10.12", "26.2");
// A data file names its own root: the new version writes its own.
let data_file = horizon_core::prepared_files::FILE_NAME;
let data_file = horizon_platform::prepared_files::FILE_NAME;
write(
"v2026.10.13",
&format!(".local/horizon-client/26.3/{data_file}"),
@@ -6747,7 +6747,7 @@ pub fn {f}("
data.push(n);
data
};
let mut assets: Vec<(String, Vec<u8>)> = horizon_prepare::BUILT_IN_PACKS
let mut assets: Vec<(String, Vec<u8>)> = horizon_platform::prepare::BUILT_IN_PACKS
.iter()
.map(|name| (name.to_string(), name.as_bytes().to_vec()))
.collect();
@@ -6822,7 +6822,7 @@ pub fn {f}("
// Resources prepared without any of them: a folder without an asset store is skipped.
release("v0.2.1", true);
let data_file = horizon_core::prepared_files::FILE_NAME;
let data_file = horizon_platform::prepared_files::FILE_NAME;
write(
resources_at(&versions.join("v0.2.1")).join(data_file),
b"older",
+1 -1
View File
@@ -23,7 +23,7 @@ use serde::{Deserialize, Serialize};
/// The Minecraft version Horizon plays: the pinned version, so a port's regenerated
/// `crates/horizon-core/src/version.rs` moves it with everything else (docs/version-ports.md).
pub const GAME: &str = horizon_core::version::VERSION_ID;
pub const GAME: &str = horizon_platform::version::VERSION_ID;
/// What a project is, as far as Horizon is concerned.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+1 -1
View File
@@ -1173,7 +1173,7 @@ pub fn run() -> Option<i32> {
let cli = Cli::from_arg_matches(&definition.get_matches()).unwrap_or_else(|e| e.exit());
let command = cli.command?;
let result = match command {
Cmd::Mcp | Cmd::AgentConfig { .. } if no_ai => Err(horizon_core::no_ai::REFUSAL.into()),
Cmd::Mcp | Cmd::AgentConfig { .. } if no_ai => Err(horizon_platform::no_ai::REFUSAL.into()),
Cmd::Mcp => crate::mcp::serve(),
command => {
let result = execute(command, cli.json);
+1 -1
View File
@@ -93,7 +93,7 @@ pub fn launcher_entry(exe: &Path) -> String {
StartupWMClass=horizon-launcher\n",
quote(exe),
exe.display(),
minecraft = horizon_core::VANILLA_VERSION,
minecraft = horizon_platform::version::VANILLA_VERSION,
)
}
+1 -1
View File
@@ -12,7 +12,7 @@
//! rest goes under `overrides/`, which a launcher copies into the game folder.
use crate::browser::installed::{Entry, Installed};
use crate::browser::{FileRef, Kind, SourceId, GAME};
use horizon_world::nbt::{self, Limits, Tag};
use horizon_platform::world::nbt::{self, Limits, Tag};
use sha2::Digest;
use std::fs;
use std::io::{Read, Write};
+3 -3
View File
@@ -704,9 +704,9 @@ fn follow_player(game_home: &Path, home: &Path) {
let _ = fs::create_dir_all(to.parent().unwrap());
let _ = fs::copy(consent, to);
}
let no_ai = horizon_core::no_ai::is_on(game_home);
if horizon_core::no_ai::is_on(home) != no_ai {
let _ = horizon_core::no_ai::set(home, no_ai);
let no_ai = horizon_platform::no_ai::is_on(game_home);
if horizon_platform::no_ai::is_on(home) != no_ai {
let _ = horizon_platform::no_ai::set(home, no_ai);
}
}
+4 -4
View File
@@ -205,7 +205,7 @@ fn an_instance_follows_the_players_answers() {
put(&main.join("options.txt"), "fov:90\n");
put(&main.join("horizon-player.txt"), "name:Player123\n");
put(&main.join("telemetry/consent.json"), r#"{"version":1}"#);
horizon_core::no_ai::set(&main, true).unwrap();
horizon_platform::no_ai::set(&main, true).unwrap();
put(&own.join("options.txt"), "fov:70\n");
follow_player(&main, &own);
assert_eq!(
@@ -220,10 +220,10 @@ fn an_instance_follows_the_players_answers() {
fs::read_to_string(own.join("telemetry/consent.json")).unwrap(),
r#"{"version":1}"#
);
assert!(horizon_core::no_ai::is_on(&own));
horizon_core::no_ai::set(&main, false).unwrap();
assert!(horizon_platform::no_ai::is_on(&own));
horizon_platform::no_ai::set(&main, false).unwrap();
follow_player(&main, &own);
assert!(!horizon_core::no_ai::is_on(&own));
assert!(!horizon_platform::no_ai::is_on(&own));
fs::remove_dir_all(&root).unwrap();
}
+1 -1
View File
@@ -140,7 +140,7 @@ pub fn runtimes_dir() -> PathBuf {
/// The runtimes in a Mojang `runtime` folder.
pub fn mojang_runtimes(dir: &Path) -> Vec<Runtime> {
horizon_prepare::java::minecraft_runtimes(dir)
horizon_platform::prepare::java::minecraft_runtimes(dir)
.into_iter()
.filter_map(|home| runtime(home, Source::Mojang))
.collect()
+1 -1
View File
@@ -1783,7 +1783,7 @@ impl Launcher {
.settings
.minecraft_dir
.clone()
.unwrap_or_else(|| horizon_prepare::artifacts::minecraft_dir(None));
.unwrap_or_else(|| horizon_platform::prepare::artifacts::minecraft_dir(None));
self.prepare_log.clear();
self.prepare_error = None;
self.prepare_log
+1 -1
View File
@@ -278,7 +278,7 @@ impl Session {
}
// No AI turned on during the session: nothing more runs.
"tools/call" if backend::no_ai() => {
Err((-32000, horizon_core::no_ai::REFUSAL.to_string()))
Err((-32000, horizon_platform::no_ai::REFUSAL.to_string()))
}
"tools/call" => {
let name = params
+1 -1
View File
@@ -351,7 +351,7 @@ fn java_runtime_lays_out_mojangs_runtime_with_its_executables_and_links() {
LICENSE
);
// horizon-prepare finds it where the official launcher would have put it.
assert_eq!(horizon_prepare::java::minecraft_runtimes(&dir), [home]);
assert_eq!(horizon_platform::prepare::java::minecraft_runtimes(&dir), [home]);
// A second call keeps the files: only the index and manifest are asked again.
server.asked();
java_runtime(
+2 -2
View File
@@ -2907,7 +2907,7 @@ fn copy_runtime(from: &Path, to: &Path) -> std::io::Result<()> {
if entry.file_type()?.is_dir() {
copy_runtime(&entry.path(), &destination)?;
} else {
horizon_prepare::clone_or_copy(&entry.path(), &destination)?;
horizon_platform::prepare::clone_or_copy(&entry.path(), &destination)?;
fs::set_permissions(&destination, entry.metadata()?.permissions())?;
}
}
@@ -2967,7 +2967,7 @@ pub(crate) fn copy_base(from: &Path, to: &Path, target: Target) -> Result<(), St
backend::copy_dir(&entry.path(), &to)
} else {
// A clone is a new file: horizon-ctl and horizon-server keep their permissions.
horizon_prepare::clone_or_copy(&entry.path(), &to)
horizon_platform::prepare::clone_or_copy(&entry.path(), &to)
.and_then(|()| fs::set_permissions(&to, entry.metadata()?.permissions()))
};
copied.map_err(|e| format!("Could not copy {}: {e}", entry.path().display()))?;
+1 -1
View File
@@ -26,7 +26,7 @@ struct Head {
/// The key an offline name's head is kept under: its offline UUID, as the game makes it, whose
/// default skin it wears without one of its own.
pub fn offline_key(name: &str) -> String {
format!("{:032x}", horizon_protocol::crypto::offline_uuid(name))
format!("{:032x}", horizon_platform::protocol::offline_uuid(name))
}
impl Heads {
+1 -1
View File
@@ -5,7 +5,7 @@
//! The launcher only adds and removes entries: every other field of every entry is written back
//! as it was read.
use base64::Engine;
use horizon_world::nbt::{self, Compound, JavaString, Limits, NamedTag, Tag};
use horizon_platform::world::nbt::{self, Compound, JavaString, Limits, NamedTag, Tag};
use std::fs;
use std::path::Path;
+1 -1
View File
@@ -224,7 +224,7 @@ fn prism_native_jars_are_found_through_the_linked_folder() {
);
let official = Path::new("com/mojang/jtracy/1.14.38/jtracy-1.14.38-natives-windows.jar");
assert!(!linked.join("libraries").join(official).exists());
let found = horizon_prepare::artifacts::launcher_library(
let found = horizon_platform::prepare::artifacts::launcher_library(
&linked,
"com.mojang:jtracy:1.14.38:natives-windows",
official,
+2 -2
View File
@@ -95,7 +95,7 @@ fn official_server() -> String {
/// (through the address's SRV record), with a 3-second timeout. None when it doesn't answer.
pub fn server_players(address: &str) -> Option<horizon_telemetry::server_status::Players> {
let (host, port) = horizon_telemetry::server_status::split_address(address)?;
let (host, port) = horizon_protocol::server_redirect(&host, port);
let (host, port) = horizon_platform::protocol::server_redirect(&host, port);
horizon_telemetry::server_status::players_at(
address,
(&host, port),
@@ -418,7 +418,7 @@ mod tests {
#[ignore = "asks the live official server"]
fn official_count_follows_the_srv_record() {
let (host, port) =
horizon_protocol::server_redirect(crate::server_list::OFFICIAL_ADDRESS, 25565);
horizon_platform::protocol::server_redirect(crate::server_list::OFFICIAL_ADDRESS, 25565);
assert_eq!(
(host.trim_end_matches('.'), port),
("play.playhorizon.dev", 24454)
+1 -1
View File
@@ -2,7 +2,7 @@
//! up, copying and deleting them. Backups are made as vanilla's Make Backup makes them
//! (`LevelStorageAccess.makeWorldBackup`): `backups/<yyyy-MM-dd_HH-mm-ss>_<folder>.zip` in the
//! game folder, every file under `<folder>/` but the world's `session.lock`.
use horizon_world::nbt::{self, Limits, Tag};
use horizon_platform::world::nbt::{self, Limits, Tag};
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
+1 -8
View File
@@ -113,13 +113,6 @@
"horizon-client-render -> horizon-polytone",
"horizon-client-render -> horizon-shader-api",
"horizon-client-render -> horizon-shaders",
"horizon-client-render -> horizon-theme",
"horizon-launcher -> horizon-core",
"horizon-launcher -> horizon-prepare",
"horizon-launcher -> horizon-protocol",
"horizon-launcher -> horizon-world",
"horizon-server -> horizon-core",
"horizon-server -> horizon-server-files",
"horizon-server -> horizon-world"
"horizon-client-render -> horizon-theme"
]
}