Files
RustNotMC/crates/horizon-client/src/game/world_loading.rs
T
SashegDev 808fd7924e Клиент: Rust-интерфейс и протокол RSX
Чекпойнт: Rust-меню, настройки, Play, пауза, ресурспаки, кейбинды, RSX-протокол, шрифты и иконки. Перед финальным пушем история будет разбита на логические коммиты.
2026-10-29 22:06:05 +03:00

425 lines
15 KiB
Rust

//! Prepare local worlds without blocking the window's event loop.
use super::*;
pub(super) struct WorldRequest {
pub save_root: PathBuf,
pub folder: String,
pub name: Option<String>,
pub seed: Option<i64>,
pub creative: bool,
pub create: Option<horizon_client::title::WorldOptions>,
}
pub(super) struct PreparedWorld {
pub save_root: PathBuf,
pub level: Level,
pub world: World,
pub streaming: Streaming,
pub save: Arc<SaveDir>,
pub session_lock: horizon_client::save::SessionLock,
pub rand_sequences: horizon_client::random_sequences::RandomSequences,
pub locator: Option<Arc<VanillaTerrain>>,
}
impl WorldRequest {
pub fn new(
folder: &str,
name: Option<&str>,
seed: Option<i64>,
creative: bool,
create: Option<&horizon_client::title::WorldOptions>,
) -> Result<Self> {
Ok(Self {
save_root: std::env::current_dir()?.join("saves").join(folder),
folder: folder.to_owned(),
name: name.map(str::to_owned),
seed,
creative,
create: create.cloned(),
})
}
pub fn prepare(
self,
res: &Resources,
pack: &PackResources,
options: &Options,
settings: &Settings,
) -> Result<PreparedWorld> {
// Before any chunk is read: a loaded container with a loot table is rolled as its chunk
// is decoded (`save::roll_loaded_loot`), and the spawn chunks are read while the world
// opens. Every world opens here: in-frame, on the server thread, on a dedicated server.
horizon_client::structure_blocks::loot_data(&options.resources);
let Self {
save_root,
folder,
name,
seed,
creative,
create,
} = self;
let name = name.as_deref();
let create = create.as_ref();
horizon_client::save::upgrade_older_world(&save_root)?;
horizon_client::save::migrate_legacy(&save_root)?;
let session_lock = horizon_client::save::SessionLock::acquire(&save_root)?;
// RandomSequences: the world's data/minecraft/random_sequences.dat.
let rand_sequences =
horizon_client::save::read_dat(&save_root.join("data/minecraft/random_sequences.dat"))
.and_then(|t| t.get("data").cloned())
.map(|d| horizon_client::random_sequences::RandomSequences::from_nbt(&d))
.unwrap_or_default();
let existing = Level::read(&save_root);
let created = existing.is_none();
let level = existing.unwrap_or_else(|| Level {
name: name.unwrap_or(&folder).to_owned(),
// WorldOptions.randomSeed when no seed was given.
seed: seed.unwrap_or_else(|| {
let t = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos() as u64);
(t ^ (t >> 29)).wrapping_mul(0x9E37_79B9_7F4A_7C15) as i64
}),
creative,
day_time: 0,
health: 20.0,
food: 20,
saturation: 5.0,
..Level::default()
});
let mut level = level;
// CreateWorldScreen's choices, before streaming reads the generator.
if created && let Some(o) = create {
level.hardcore = o.hardcore;
level.difficulty = horizon_client::title::DIFFICULTIES[o.difficulty()].into();
level.allow_commands = Some(o.commands(creative));
horizon_client::save::write_world_gen_settings_with(
&save_root,
level.seed,
o.world_gen(),
o.flat_settings(),
)?;
// WorldCreationUiState.getGameRules, written with the level.
level.game_rules = o.rules.by_name();
}
// Worlds made without the Create New World screen (--world, the
// control socket) take its default: commands on for Creative only.
if created {
level.allow_commands.get_or_insert(creative);
}
if let Some(d) = &options.dimension {
level.dimension = d.clone();
level.position = None;
}
let dim = Dim::from_id(&level.dimension);
let (mut streaming, save, locator) =
make_streaming(res, pack, options, settings, &save_root, dim, level.seed)?;
streaming.radius = settings.render_distance;
let mut world = World::new(res.facts().air, level.seed);
world.fast_lava = dim == Dim::Nether;
world.game_time = level.time;
world.day_time = options.time.unwrap_or(level.day_time);
world.weather = level.weather;
if let Some(w) = options.weather.as_deref() {
// /weather: clear, rain or thunder for a long duration.
let (rain, thunder) = (w != "clear", w == "thunder");
world.weather = horizon_client::world::Weather {
clear_time: if w == "clear" { 6000 } else { 0 },
rain_time: 6000,
thunder_time: 6000,
raining: rain,
thundering: thunder,
rain_level: if rain { (1.0, 1.0) } else { (0.0, 0.0) },
thunder_level: if thunder { (1.0, 1.0) } else { (0.0, 0.0) },
};
}
growth::install_live_features(res, pack, options);
// Persist new metadata on the worker too, before publishing the world.
if !save_root.join("level.dat").exists() {
level.write(&save_root)?;
}
Ok(PreparedWorld {
save_root,
level,
world,
streaming,
save,
session_lock,
rand_sequences,
locator,
})
}
}
pub(super) struct WorldLoader {
pub cancelled: bool,
handle: JoinHandle<std::result::Result<PreparedWorld, String>>,
}
impl WorldLoader {
pub fn start(
request: WorldRequest,
res: Arc<Resources>,
pack: PackResources,
options: Options,
settings: Settings,
) -> Result<Self> {
let handle = std::thread::Builder::new()
.name("World initialization".into())
.spawn(move || {
request
.prepare(&res, &pack, &options, &settings)
.map_err(|e| e.to_string())
})?;
Ok(Self {
cancelled: false,
handle,
})
}
// Wait for construction and disposal off the window thread. Keeping this
// handle prevents a queued reopen racing the old session lock.
pub fn cancel(self) -> Self {
Self {
cancelled: true,
handle: std::thread::spawn(move || {
drop(self.finish());
Err("world initialization cancelled".into())
}),
}
}
pub fn ready(&self) -> bool {
self.handle.is_finished()
}
pub fn finish(self) -> Result<PreparedWorld> {
self.handle
.join()
.map_err(|_| "world initialization worker panicked")?
.map_err(Into::into)
}
}
/// How long the preloaded world data outlives the world screens: a player
/// who goes back to the title or to Multiplayer and opens no world for this
/// long gets the memory back.
const PRELOAD_KEPT: std::time::Duration = std::time::Duration::from_secs(60);
enum Preload {
Idle,
/// Read, or being read, on the "World data preload" thread. `left`: when
/// the player last left the world screens.
Started {
thread: std::thread::JoinHandle<()>,
left: Option<std::time::Instant>,
},
/// A world opened: the data is the world's now, and stays.
Used,
}
static PRELOAD: std::sync::Mutex<Preload> = std::sync::Mutex::new(Preload::Idle);
/// Each frame of the title: read what every world opening loads and keeps
/// for the rest of the process (the worldgen bundle, the loot tables and the
/// registries the server sends) on a helper thread while the player is on the
/// world screens (`on_world_screens`: Singleplayer, Create World and their
/// dialogs), so it overlaps the time spent choosing a world. A world opened
/// meanwhile waits for the part being read instead of reading it again. Not
/// at the title screen itself: many players only go to Multiplayer, and
/// small machines need the memory. Dropped after `PRELOAD_KEPT` away from
/// the world screens without a world opening.
pub(super) fn preload_world_data(
on_world_screens: bool,
resources: &std::path::Path,
registry: &'static horizon_world::registry::BlockRegistry,
) {
let mut preload = PRELOAD.lock().unwrap_or_else(|e| e.into_inner());
match &mut *preload {
Preload::Used => {}
Preload::Idle if on_world_screens => {
let resources = resources.to_owned();
let spawned = std::thread::Builder::new()
.name("World data preload".into())
.spawn(move || {
horizon_threads::behind_server();
// Errors are left for the world opening to report.
let _ = horizon_client::vanillagen::shared_data(
&resources.join("worldgen"),
registry,
);
let _ = horizon_client::structure_blocks::loot_data(&resources);
let _ = super::lan::sync_data(&resources);
});
match spawned {
Ok(thread) => *preload = Preload::Started { thread, left: None },
Err(e) => log::warn!("RustNotMC: world data preload: {e}"),
}
}
Preload::Idle => {}
Preload::Started { left, .. } if on_world_screens => *left = None,
Preload::Started { thread, left } => {
let left = *left.get_or_insert_with(std::time::Instant::now);
if left.elapsed() >= PRELOAD_KEPT && thread.is_finished() {
horizon_client::vanillagen::forget_shared_data();
horizon_client::structure_blocks::forget_loot_data();
super::lan::forget_sync_data();
horizon_threads::release_free_memory();
*preload = Preload::Idle;
log::info!("RustNotMC: preloaded world data dropped, no world opened");
}
}
}
}
/// A world opens: what `preload_world_data` read is the world's from now on.
pub(super) fn world_data_used() {
*PRELOAD.lock().unwrap_or_else(|e| e.into_inner()) = Preload::Used;
}
pub(super) fn make_streaming(
res: &Resources,
pack: &PackResources,
options: &Options,
settings: &Settings,
world_root: &std::path::Path,
dim: Dim,
seed: i64,
) -> Result<(Streaming, Arc<SaveDir>, Option<Arc<VanillaTerrain>>)> {
let save = Arc::new(SaveDir::new(horizon_client::save::dimension_dir(
world_root,
dim.id(),
)));
let mut locator = None;
let flat = (dim == Dim::Overworld)
.then(|| horizon_client::save::world_gen_settings(world_root))
.flatten()
.and_then(|s| {
horizon_client::flatgen::FlatTerrain::from_settings(
&s,
pack.assets.registry,
res.biomes(),
)
});
let mut streaming = if let Some(inner) = flat {
Streaming::new(
Arc::new(Persistent {
inner: Arc::new(inner),
save: save.clone(),
registry: pack.assets.clone(),
biomes: res.biomes().clone(),
}),
pack.context.clone(),
res.threads(),
)
} else if options.test_terrain && dim == Dim::Overworld {
let inner = Arc::new(TestTerrain::new(pack.assets.registry, res.biomes())?);
Streaming::new(
Arc::new(Persistent {
inner,
save: save.clone(),
registry: pack.assets.clone(),
biomes: res.biomes().clone(),
}),
pack.context.clone(),
res.threads(),
)
} else {
// WorldOptions.generateStructures, from the world's settings.
let structures =
horizon_client::save::WorldGen::read(world_root).is_none_or(|w| w.structures);
let inner = Arc::new(VanillaTerrain::for_world(&options.resources.join("worldgen"), pack.assets.registry, res.biomes(), seed, dim.id(), structures).map_err(|e| {
format!("{e}\nPrepare the worldgen bundle with the launcher, or: horizon-prepare --prepare")
})?);
if dim == Dim::Overworld && structures {
inner.search_rings_behind();
}
locator = Some(inner.clone());
Streaming::new(
Arc::new(Persistent {
inner,
save: save.clone(),
registry: pack.assets.clone(),
biomes: res.biomes().clone(),
}),
pack.context.clone(),
res.threads(),
)
};
streaming.forced = save.load_forced_chunks();
streaming.radius = settings.render_distance;
streaming.biome_blend = settings.biome_blend;
streaming.smooth_lighting = settings.smooth_lighting;
streaming.cutout_leaves = settings.cutout_leaves;
streaming.biome_zoom = BiomeZoom::new(obfuscate_seed(seed));
streaming.sky_light = dim != Dim::Nether;
streaming.nether_shading = dim == Dim::Nether;
// A server draws nothing: neither the integrated server nor the
// dedicated one meshes sections.
streaming.mesh = options.execution_role.is_client();
Ok((streaming, save, locator))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn polling_does_not_wait_for_world_initialization() {
let (release, wait) = std::sync::mpsc::channel();
let loader = WorldLoader {
cancelled: false,
handle: std::thread::spawn(move || {
wait.recv().unwrap();
Err("test world failure".into())
}),
};
assert!(!loader.ready());
release.send(()).unwrap();
assert_eq!(
loader.finish().err().unwrap().to_string(),
"test world failure"
);
}
#[test]
fn cancellation_keeps_initialization_tracked_until_it_has_stopped() {
let (release, wait) = std::sync::mpsc::channel();
let loader = WorldLoader {
cancelled: false,
handle: std::thread::spawn(move || {
wait.recv().unwrap();
Err("cancelled test world".into())
}),
}
.cancel();
assert!(loader.cancelled);
assert!(!loader.ready());
release.send(()).unwrap();
assert_eq!(
loader.finish().err().unwrap().to_string(),
"world initialization cancelled"
);
}
#[test]
fn worker_panics_reach_the_client() {
let loader = WorldLoader {
cancelled: false,
handle: std::thread::spawn(|| panic!("broken world initialization")),
};
assert_eq!(
loader.finish().err().unwrap().to_string(),
"world initialization worker panicked"
);
}
#[test]
fn prepared_world_can_cross_threads() {
fn send<T: Send>() {}
send::<PreparedWorld>();
}
}