808fd7924e
Чекпойнт: Rust-меню, настройки, Play, пауза, ресурспаки, кейбинды, RSX-протокол, шрифты и иконки. Перед финальным пушем история будет разбита на логические коммиты.
425 lines
15 KiB
Rust
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>();
|
|
}
|
|
}
|