Startup: keep the behaviour and registry snapshots across rebuilds

The snapshots in .horizon-cache keyed on the executable's modification
time, so every rebuild of the game threw them away and the first start
parsed facts.json and block-behaviours.json again. They now key on a hash
of the code that resolves them (horizon-world and horizon-core sources,
their manifests, Cargo.lock, profile, target and rustflags), computed by
horizon-world's build.rs.

Fixes #9522

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
valkdzxandClaude Opus 5.5 committed 2026-11-05 00:37:31 +02:00
1 parent 63eccfc355
commit 56e06994d3
2 files changed
+80 -18

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
//! `HORIZON_WORLD_SOURCES`: a hash of the code that resolves the behaviour and
//! block-registry snapshots (`src/level/data/cache.rs`). This crate's sources,
//! `horizon-core`'s, both manifests, the workspace's `Cargo.lock` and the build
//! profile and target. A rebuild that changes none of them (an edit in the
//! renderer or in `game.rs`) keeps the snapshots; the executable's own
//! modification time, the key before, threw them away on every build.
use std::{
collections::hash_map::DefaultHasher,
fs,
hash::{Hash, Hasher},
path::{Path, PathBuf},
};
fn main() {
let manifest = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap());
let core = manifest.join("../horizon-core");
let lock = manifest.join("../../Cargo.lock");
let mut files = Vec::new();
for dir in [manifest.join("src"), core.join("src")] {
walk(&dir, &mut files);
println!("cargo:rerun-if-changed={}", dir.display());
}
for file in [manifest.join("Cargo.toml"), core.join("Cargo.toml"), lock] {
if file.is_file() {
println!("cargo:rerun-if-changed={}", file.display());
files.push(file);
}
}
files.sort();
// Two differently seeded hashes: 128 bits, so two builds never share a key by chance.
let mut hashers = [DefaultHasher::new(), DefaultHasher::new()];
1u8.hash(&mut hashers[1]);
for hasher in &mut hashers {
for name in ["PROFILE", "TARGET", "OPT_LEVEL", "CARGO_ENCODED_RUSTFLAGS"] {
std::env::var(name).unwrap_or_default().hash(hasher);
}
}
for file in &files {
let relative = file.strip_prefix(&manifest).unwrap_or(file);
let bytes = fs::read(file).unwrap_or_default();
for hasher in &mut hashers {
relative.to_string_lossy().replace('\\', "/").hash(hasher);
bytes.hash(hasher);
}
}
println!(
"cargo:rustc-env=HORIZON_WORLD_SOURCES={:016x}{:016x}",
hashers[0].finish(),
hashers[1].finish()
);
}
fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk(&path, out);
} else {
out.push(path);
}
}
}
+12 -18
View File
@@ -5,8 +5,10 @@
//! versions, corrupt payloads and write failures always use the normal loader.
//! Snapshots live in the game's own cache folder ([`set_dir`]), never in the
//! resource folder: that one is shared by worktrees and copied between installed
//! versions by the launcher. Its key includes the running executable, so a build
//! that resolves the tables differently never reads another build's snapshot.
//! versions by the launcher. Its key includes a hash of the code that resolves
//! the tables (this crate, `horizon-core`, their dependencies; see `build.rs`),
//! so a build that resolves them differently never reads another build's
//! snapshot, while a rebuild that only touched other crates keeps them.
use super::*;
use crate::registry::snapshot as registry_snapshot;
use serde::{Deserialize, Serialize};
@@ -18,7 +20,7 @@ use std::{
Mutex, OnceLock,
atomic::{AtomicU32, Ordering},
},
time::{SystemTime, UNIX_EPOCH},
time::SystemTime,
};
const MAGIC: &[u8; 8] = b"HZBEHAV\0";
@@ -141,26 +143,18 @@ struct Item {
game_master: bool,
}
/// The running executable's stamp: the version constant alone misses loader
/// changes that keep a snapshot's layout.
fn hash_executable(hasher: &mut Sha256) -> io::Result<()> {
let exe = std::env::current_exe()?;
let built = fs::metadata(&exe)?;
hash_str(hasher, &exe.to_string_lossy());
hasher.update(built.len().to_le_bytes());
let modified = built
.modified()?
.duration_since(UNIX_EPOCH)
.map_err(|e| invalid(e.to_string()))?;
hasher.update(modified.as_nanos().to_le_bytes());
Ok(())
/// The build that resolves the tables: a hash of this crate's and
/// `horizon-core`'s sources, `Cargo.lock` and the profile (`build.rs`). The
/// version constant alone misses loader changes that keep a snapshot's layout.
fn hash_build(hasher: &mut Sha256) {
hash_str(hasher, env!("HORIZON_WORLD_SOURCES"));
}
fn fingerprint(registry: &BlockRegistry, root: &Path) -> io::Result<[u8; 32]> {
let mut hasher = Sha256::new();
hasher.update(b"horizon resolved behaviours 2");
// The build that resolved the tables.
hash_executable(&mut hasher)?;
hash_build(&mut hasher);
hasher.update(sources_digest(root)?);
// The registry argument may be modified independently of facts.json.
hasher.update((registry.len() as u64).to_le_bytes());
@@ -865,7 +859,7 @@ fn write(
fn registry_fingerprint(root: &Path) -> io::Result<[u8; 32]> {
let mut hasher = Sha256::new();
hasher.update(b"horizon block registry 1");
hash_executable(&mut hasher)?;
hash_build(&mut hasher);
hasher.update(paths_digest(root, &[root.join("facts.json")])?);
Ok(hasher.finalize().into())
}