mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7a6df3a8b | ||
|
|
5cc12e9973 | ||
|
|
d1a54c150a | ||
|
|
4f22b0bc66 | ||
|
|
28c6e890ac | ||
|
|
8babf37afd | ||
|
|
e1e5fac7cb | ||
|
|
027abe75ed |
No files matched your search
Generated
+1
@@ -1658,6 +1658,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libmimalloc-sys",
|
||||
"log",
|
||||
"mimalloc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -11247,6 +11247,7 @@ impl Game<'_> {
|
||||
/// integrated-server selection. Server startup must use this entry point.
|
||||
fn load_world(&mut self, request: world_loading::WorldRequest) -> Result<()> {
|
||||
self.join_world_rest()?;
|
||||
log::info!("{}", horizon_threads::mem_line("world_rest_joined"));
|
||||
self.perf.world_opening();
|
||||
let prepared = request.prepare(
|
||||
&self.res,
|
||||
@@ -11255,6 +11256,7 @@ impl Game<'_> {
|
||||
&self.settings,
|
||||
&horizon_world::datafix::progress::UpgradeProgress::new(),
|
||||
)?;
|
||||
log::info!("{}", horizon_threads::mem_line("world_prepared"));
|
||||
self.finish_open_world(prepared)
|
||||
}
|
||||
|
||||
@@ -17997,7 +17999,8 @@ fn load_core(
|
||||
parallel: bool,
|
||||
progress: &Progress,
|
||||
) -> std::result::Result<std::result::Result<CoreTables, String>, String> {
|
||||
let assets = match Assets::load(root) {
|
||||
let parallel = parallel && std::env::var_os("SP2_SERIAL").is_none();
|
||||
let assets = match horizon_threads::mem_step("core_assets", || Assets::load(root)) {
|
||||
Ok(assets) => assets,
|
||||
Err(e) => return Ok(Err(e.to_string())),
|
||||
};
|
||||
@@ -18007,20 +18010,23 @@ fn load_core(
|
||||
std::thread::scope(|scope| {
|
||||
let assets = &assets;
|
||||
let render = Step::spawn(scope, parallel, move || {
|
||||
// A server reads no block sprites' pixels, which only the
|
||||
// renderer draws (32 MB).
|
||||
let render = if headless {
|
||||
assets.reload_for_server(root)
|
||||
} else {
|
||||
assets.reload(root)
|
||||
}
|
||||
.map_err(|e| e.to_string())?;
|
||||
let item_renderer = horizon_client::item_render::ItemRenderer::load_with_library(
|
||||
root,
|
||||
render.model_library.clone(),
|
||||
)
|
||||
.map_err(|e| e.to_string());
|
||||
Ok((render, item_renderer))
|
||||
horizon_threads::mem_step("core_render", || {
|
||||
// A server reads no block sprites' pixels, which only the
|
||||
// renderer draws (32 MB).
|
||||
let render = if headless {
|
||||
assets.reload_for_server(root)
|
||||
} else {
|
||||
assets.reload(root)
|
||||
}
|
||||
.map_err(|e| e.to_string())?;
|
||||
let item_renderer =
|
||||
horizon_client::item_render::ItemRenderer::load_with_library(
|
||||
root,
|
||||
render.model_library.clone(),
|
||||
)
|
||||
.map_err(|e| e.to_string());
|
||||
Ok((render, item_renderer))
|
||||
})
|
||||
});
|
||||
// Nor the face visibility the mesher and the block entity
|
||||
// renderer cull with (19 MB).
|
||||
@@ -18031,13 +18037,16 @@ fn load_core(
|
||||
models::load_visibility(root, registry).map_err(|e| e.to_string())
|
||||
});
|
||||
let biomes = Step::spawn(scope, parallel, || {
|
||||
Biomes::load(root).map_err(|e| e.to_string())
|
||||
horizon_threads::mem_step("core_biomes", || Biomes::load(root))
|
||||
.map_err(|e| e.to_string())
|
||||
});
|
||||
let environment = Step::spawn(scope, parallel, || {
|
||||
Environment::load(root).map_err(|e| e.to_string())
|
||||
horizon_threads::mem_step("core_environment", || Environment::load(root))
|
||||
.map_err(|e| e.to_string())
|
||||
});
|
||||
let collision = Step::spawn(scope, parallel, || {
|
||||
collisions::load(root, registry).map_err(|e| e.to_string())
|
||||
horizon_threads::mem_step("core_collision", || collisions::load(root, registry))
|
||||
.map_err(|e| e.to_string())
|
||||
});
|
||||
let gameplay = gameplay_tables(root, assets, parallel, progress);
|
||||
(
|
||||
@@ -18075,13 +18084,18 @@ fn gameplay_tables(
|
||||
parallel: bool,
|
||||
progress: &Progress,
|
||||
) -> std::result::Result<GameplayTables, String> {
|
||||
let behaviour_json: serde_json::Value = serde_json::from_reader(std::io::BufReader::new(
|
||||
std::fs::File::open(root.join("block-behaviours.json")).map_err(|e| e.to_string())?,
|
||||
))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let (facts, gameplay_projection) =
|
||||
let behaviour_json: serde_json::Value =
|
||||
horizon_threads::mem_step("core_behaviour_json", || {
|
||||
serde_json::from_reader(std::io::BufReader::new(
|
||||
std::fs::File::open(root.join("block-behaviours.json"))
|
||||
.map_err(|e| e.to_string())?,
|
||||
))
|
||||
.map_err(|e| e.to_string())
|
||||
})?;
|
||||
let (facts, gameplay_projection) = horizon_threads::mem_step("core_facts", || {
|
||||
BlockFacts::load_with_projection(root, assets.registry, &behaviour_json)
|
||||
.map_err(|e| e.to_string())?;
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
horizon_client::block_contact::set_mod_blocks(
|
||||
facts
|
||||
.states
|
||||
@@ -18091,10 +18105,15 @@ fn gameplay_tables(
|
||||
progress.set(2);
|
||||
let (light, motion) = std::thread::scope(|scope| {
|
||||
let light = Step::spawn(scope, parallel, || {
|
||||
LightTables::load(root, assets.registry, &facts).map_err(|e| e.to_string())
|
||||
horizon_threads::mem_step("core_light", || {
|
||||
LightTables::load(root, assets.registry, &facts)
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
});
|
||||
let motion =
|
||||
MotionResources::load_with_facts(root, assets, &facts).map_err(|e| e.to_string());
|
||||
let motion = horizon_threads::mem_step("core_motion", || {
|
||||
MotionResources::load_with_facts(root, assets, &facts)
|
||||
})
|
||||
.map_err(|e| e.to_string());
|
||||
(light.join(), motion)
|
||||
});
|
||||
Ok(GameplayTables {
|
||||
@@ -18182,6 +18201,7 @@ fn load_resources(
|
||||
root = vanilla_root.to_path_buf();
|
||||
};
|
||||
log::info!("load core tables: {:.2}s", started.elapsed().as_secs_f32());
|
||||
log::info!("{}", horizon_threads::mem_line("core_tables"));
|
||||
let CoreTables {
|
||||
assets,
|
||||
render,
|
||||
@@ -18216,6 +18236,7 @@ fn load_resources(
|
||||
Arc::new(light),
|
||||
);
|
||||
log::info!("load pack tables: {:.2}s", started.elapsed().as_secs_f32());
|
||||
log::info!("{}", horizon_threads::mem_line("pack_tables"));
|
||||
progress.set(4);
|
||||
if !menu {
|
||||
horizon_client::ui::load_metadata(&root);
|
||||
@@ -18346,11 +18367,17 @@ fn load_world_rest(
|
||||
// Independent tables load together, within the worker budget the settings
|
||||
// allow. Only the recipes and the stations read another table: the items.
|
||||
let load_items = || -> Result<_> {
|
||||
let items = Items::load(root, registry, facts, overlay)?;
|
||||
let items = horizon_threads::mem_step("items_only", || {
|
||||
Items::load(root, registry, facts, overlay)
|
||||
})?;
|
||||
log::info!("load items: {:.2}s", started.elapsed().as_secs_f32());
|
||||
log::info!("{}", horizon_threads::mem_line("items"));
|
||||
progress.set(7);
|
||||
let recipes = Recipes::load(root, &items, overlay)?;
|
||||
let stations = horizon_client::stations::StationData::load(root, &items, overlay);
|
||||
let recipes =
|
||||
horizon_threads::mem_step("recipes", || Recipes::load(root, &items, overlay))?;
|
||||
let stations = horizon_threads::mem_step("stations", || {
|
||||
horizon_client::stations::StationData::load(root, &items, overlay)
|
||||
});
|
||||
Ok((items, recipes, stations))
|
||||
};
|
||||
let load_behaviours =
|
||||
@@ -18397,33 +18424,37 @@ fn load_world_rest(
|
||||
) -> std::result::Result<T, String> {
|
||||
result.map_err(|e| e.to_string())
|
||||
}
|
||||
let (items, behaviours, block_entity_render, animate, mobs, transformers) = if threads > 1 {
|
||||
std::thread::scope(|scope| {
|
||||
let items = scope.spawn(|| text(load_items()));
|
||||
let behaviours = scope.spawn(|| text(load_behaviours()));
|
||||
let block_entity_render = scope.spawn(|| text(load_block_entity_render()));
|
||||
let animate = scope.spawn(|| text(load_animate()));
|
||||
let mobs = scope.spawn(|| text(load_mobs()));
|
||||
let transformers = text(load_transformers());
|
||||
let serial = std::env::var_os("SP2_SERIAL").is_some();
|
||||
let (items, behaviours, block_entity_render, animate, mobs, transformers) =
|
||||
if threads > 1 && !serial {
|
||||
std::thread::scope(|scope| {
|
||||
let items = scope.spawn(|| text(load_items()));
|
||||
let behaviours = scope.spawn(|| text(load_behaviours()));
|
||||
let block_entity_render = scope.spawn(|| text(load_block_entity_render()));
|
||||
let animate = scope.spawn(|| text(load_animate()));
|
||||
let mobs = scope.spawn(|| text(load_mobs()));
|
||||
let transformers = text(load_transformers());
|
||||
(
|
||||
items.join().unwrap(),
|
||||
behaviours.join().unwrap(),
|
||||
block_entity_render.join().unwrap(),
|
||||
animate.join().unwrap(),
|
||||
mobs.join().unwrap(),
|
||||
transformers,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
(
|
||||
items.join().unwrap(),
|
||||
behaviours.join().unwrap(),
|
||||
block_entity_render.join().unwrap(),
|
||||
animate.join().unwrap(),
|
||||
mobs.join().unwrap(),
|
||||
transformers,
|
||||
horizon_threads::mem_step("rest_items", || text(load_items())),
|
||||
horizon_threads::mem_step("rest_behaviours", || text(load_behaviours())),
|
||||
horizon_threads::mem_step("rest_block_entity_render", || {
|
||||
text(load_block_entity_render())
|
||||
}),
|
||||
horizon_threads::mem_step("rest_animate", || text(load_animate())),
|
||||
horizon_threads::mem_step("rest_mobs", || text(load_mobs())),
|
||||
horizon_threads::mem_step("rest_transformers", || text(load_transformers())),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
(
|
||||
text(load_items()),
|
||||
text(load_behaviours()),
|
||||
text(load_block_entity_render()),
|
||||
text(load_animate()),
|
||||
text(load_mobs()),
|
||||
text(load_transformers()),
|
||||
)
|
||||
};
|
||||
};
|
||||
let (items, recipes, stations) = items?;
|
||||
let (animate, fog) = animate?;
|
||||
let (mob_types, mob_data) = mobs?;
|
||||
|
||||
@@ -768,6 +768,7 @@ impl Game<'_> {
|
||||
let lists = Arc::new(Mutex::new(lists));
|
||||
self.server_lists = Some(lists.clone());
|
||||
log("INFO", &format!("Preparing level \"{level_name}\""));
|
||||
log::info!("{}", horizon_threads::mem_line("preparing"));
|
||||
let folder = world_dir.to_string_lossy().into_owned();
|
||||
// MinecraftServer.loadLevel's start: nothing is loaded yet.
|
||||
self.native_server_about_to_start(true);
|
||||
@@ -781,6 +782,7 @@ impl Game<'_> {
|
||||
game_mode == 1,
|
||||
None,
|
||||
)?)?;
|
||||
log::info!("{}", horizon_threads::mem_line("load_world"));
|
||||
if new_world {
|
||||
self.level.hardcore = props.hardcore;
|
||||
}
|
||||
@@ -814,6 +816,7 @@ impl Game<'_> {
|
||||
// Nothing is kept around the spawn while nobody is on.
|
||||
self.streaming.radius = -1;
|
||||
self.settle_spawn()?;
|
||||
log::info!("{}", horizon_threads::mem_line("settle_spawn"));
|
||||
let gate_lists = lists.clone();
|
||||
let gate: LoginGate = Arc::new(move |address, uuid, name| {
|
||||
let user = NameAndId {
|
||||
@@ -922,6 +925,7 @@ impl Game<'_> {
|
||||
log("WARN", "Perhaps a server is already running on that port?");
|
||||
return Ok(());
|
||||
}
|
||||
log::info!("{}", horizon_threads::mem_line("lan_host"));
|
||||
self.sync_ops(&lists);
|
||||
let anticheat = load_anticheat();
|
||||
let chat_filter = load_chat_filter();
|
||||
@@ -959,6 +963,21 @@ impl Game<'_> {
|
||||
started.elapsed().as_secs_f64()
|
||||
),
|
||||
);
|
||||
log::info!("{}", horizon_threads::mem_line("done"));
|
||||
std::thread::spawn(|| {
|
||||
for t in [10, 30] {
|
||||
std::thread::sleep(std::time::Duration::from_secs(if t == 10 {
|
||||
10
|
||||
} else {
|
||||
20
|
||||
}));
|
||||
log::info!("{}", horizon_threads::mem_line(&format!("idle{t}s")));
|
||||
if t == 10 && std::env::var_os("SP2_RELEASE").is_some() {
|
||||
horizon_threads::release_free_memory();
|
||||
log::info!("{}", horizon_threads::mem_line("released"));
|
||||
}
|
||||
}
|
||||
});
|
||||
// DedicatedServer.initServer ends with saveEverything(false, true, true).
|
||||
self.save_game()?;
|
||||
self.log_saved_levels();
|
||||
|
||||
@@ -5,7 +5,7 @@ mod game;
|
||||
use horizon_client::{crash, logging};
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: horizon_threads::Allocator = horizon_threads::Allocator;
|
||||
static ALLOCATOR: horizon_threads::Counting = horizon_threads::Counting;
|
||||
|
||||
/// The mod API level this game serves, kept in the program for the launcher
|
||||
/// to read without running it (`horizon_mods::api_level::MARK`). `main`
|
||||
@@ -13,6 +13,11 @@ static ALLOCATOR: horizon_threads::Allocator = horizon_threads::Allocator;
|
||||
static MOD_API_LEVEL: [u8; 27] = horizon_mods::api_level::MARK;
|
||||
|
||||
fn main() {
|
||||
horizon_threads::T0.get_or_init(std::time::Instant::now);
|
||||
horizon_threads::COUNT.store(
|
||||
std::env::var_os("SP2_COUNT").is_some(),
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
horizon_threads::configure_allocator();
|
||||
std::hint::black_box(&MOD_API_LEVEL);
|
||||
horizon_client::telemetry::mark_start();
|
||||
|
||||
@@ -684,6 +684,7 @@ pub fn shared_data_with(
|
||||
let started = std::time::Instant::now();
|
||||
let data = GeneratorData::load_with(dir, registry, overrides)?;
|
||||
log::info!("Horizon: worldgen data loaded in {:?}", started.elapsed());
|
||||
log::info!("{}", horizon_threads::mem_line("worldgen_data"));
|
||||
for warning in data.warnings() {
|
||||
log::warn!("Horizon: data pack worldgen: {warning}");
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ libmimalloc-sys = { version = "0.1.49", default-features = false, features = ["e
|
||||
|
||||
[target.'cfg(any(target_vendor = "apple", target_os = "linux", target_os = "android"))'.dependencies]
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_Console", "Win32_System_IO", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_WindowsAndMessaging"] }
|
||||
|
||||
@@ -492,3 +492,67 @@ mod junction_tests {
|
||||
assert!(super::mount_point_buffer(&[0; 100]).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
/// Bench only: mimalloc with a count of the bytes held live.
|
||||
pub struct Counting;
|
||||
pub static LIVE: std::sync::atomic::AtomicIsize = std::sync::atomic::AtomicIsize::new(0);
|
||||
pub static PEAK: std::sync::atomic::AtomicIsize = std::sync::atomic::AtomicIsize::new(0);
|
||||
pub static COUNT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
fn grew(n: isize) {
|
||||
if !COUNT.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let now = LIVE.fetch_add(n, std::sync::atomic::Ordering::Relaxed) + n;
|
||||
PEAK.fetch_max(now, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
/// Bench only: run `f`, log its peak and kept live bytes.
|
||||
pub fn mem_step<T>(name: &str, f: impl FnOnce() -> T) -> T {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
let start = LIVE.load(Relaxed);
|
||||
PEAK.store(start, Relaxed);
|
||||
let began = std::time::Instant::now();
|
||||
let out = f();
|
||||
let took = began.elapsed().as_secs_f64() * 1000.0;
|
||||
let (end, peak) = (LIVE.load(Relaxed), PEAK.load(Relaxed));
|
||||
log::info!(
|
||||
"memstep {name} {took:.0} ms peak +{:.1} MB kept +{:.1} MB",
|
||||
(peak - start) as f64 / 1e6,
|
||||
(end - start) as f64 / 1e6
|
||||
);
|
||||
out
|
||||
}
|
||||
unsafe impl std::alloc::GlobalAlloc for Counting {
|
||||
unsafe fn alloc(&self, l: std::alloc::Layout) -> *mut u8 {
|
||||
grew(l.size() as isize);
|
||||
unsafe { mimalloc::MiMalloc.alloc(l) }
|
||||
}
|
||||
unsafe fn alloc_zeroed(&self, l: std::alloc::Layout) -> *mut u8 {
|
||||
grew(l.size() as isize);
|
||||
unsafe { mimalloc::MiMalloc.alloc_zeroed(l) }
|
||||
}
|
||||
unsafe fn dealloc(&self, p: *mut u8, l: std::alloc::Layout) {
|
||||
if COUNT.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
LIVE.fetch_sub(l.size() as isize, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
unsafe { mimalloc::MiMalloc.dealloc(p, l) }
|
||||
}
|
||||
unsafe fn realloc(&self, p: *mut u8, l: std::alloc::Layout, n: usize) -> *mut u8 {
|
||||
grew(n as isize - l.size() as isize);
|
||||
unsafe { mimalloc::MiMalloc.realloc(p, l, n) }
|
||||
}
|
||||
}
|
||||
/// Bench only: `memdbg STAGE live MB rss MB`.
|
||||
pub static T0: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
|
||||
pub fn mem_line(stage: &str) -> String {
|
||||
let t = T0
|
||||
.get_or_init(std::time::Instant::now)
|
||||
.elapsed()
|
||||
.as_secs_f64()
|
||||
* 1000.0;
|
||||
let rss = std::fs::read_to_string("/proc/self/statm")
|
||||
.ok()
|
||||
.and_then(|s| s.split_whitespace().nth(1)?.parse::<f64>().ok())
|
||||
.map_or(0.0, |p| p * 4096.0 / 1e6);
|
||||
let live = LIVE.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6;
|
||||
format!("memdbg {stage} t {t:.0} ms live {live:.1} MB rss {rss:.1} MB")
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
cmp::Ordering,
|
||||
collections::{BTreeMap, HashMap},
|
||||
collections::BTreeMap,
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
ops::Deref,
|
||||
@@ -48,9 +48,9 @@ impl Text {
|
||||
|
||||
/// Strings shared by the states of one registry ([`BlockProperties::share`]).
|
||||
#[derive(Default)]
|
||||
pub struct SharedStrings(HashMap<String, Arc<String>>);
|
||||
pub struct SharedStrings(horizon_core::fast_hash::FxHashMap<String, Arc<String>>);
|
||||
impl SharedStrings {
|
||||
fn get(&mut self, text: &str) -> Arc<String> {
|
||||
pub fn get(&mut self, text: &str) -> Arc<String> {
|
||||
if let Some(shared) = self.0.get(text) {
|
||||
return shared.clone();
|
||||
}
|
||||
@@ -131,13 +131,26 @@ impl BlockProperties {
|
||||
self.0.iter().map(|(_, v)| &**v)
|
||||
}
|
||||
/// These properties over the strings `strings` holds, which other states
|
||||
/// share (a registry's states, built once).
|
||||
/// share (a registry's states, built once). Strings already shared stay.
|
||||
pub fn share(&mut self, strings: &mut SharedStrings) {
|
||||
for (k, v) in self.0.iter_mut() {
|
||||
*k = Text::Shared(strings.get(k));
|
||||
*v = Text::Shared(strings.get(v));
|
||||
for text in self.0.iter_mut().flat_map(|(k, v)| [k, v]) {
|
||||
if let Text::Owned(owned) = text {
|
||||
*text = Text::Shared(strings.get(owned));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Shared names and values, collected as a `BTreeMap` collects them.
|
||||
pub fn from_shared(mut pairs: Vec<(Arc<String>, Arc<String>)>) -> Self {
|
||||
pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
let mut kept: Vec<(Text, Text)> = Vec::with_capacity(pairs.len());
|
||||
for (k, v) in pairs {
|
||||
match kept.last_mut() {
|
||||
Some(last) if *last.0 == *k => last.1 = Text::Shared(v),
|
||||
_ => kept.push((Text::Shared(k), Text::Shared(v))),
|
||||
}
|
||||
}
|
||||
Self(kept.into_boxed_slice())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Iter<'a>(std::slice::Iter<'a, (Text, Text)>);
|
||||
|
||||
@@ -191,7 +191,7 @@ impl BlockState {
|
||||
let Some(Tag::String(name)) = tag.get("id") else {
|
||||
return Err(invalid("block state is missing id"));
|
||||
};
|
||||
let mut properties = BlockProperties::new();
|
||||
let mut properties = Vec::new();
|
||||
if let Some(tag) = tag.get("properties") {
|
||||
let Tag::Compound(map) = tag else {
|
||||
return Err(invalid("block properties are not a compound"));
|
||||
@@ -200,15 +200,15 @@ impl BlockState {
|
||||
let Tag::String(v) = v else {
|
||||
return Err(invalid("block property is not a string"));
|
||||
};
|
||||
properties.insert(
|
||||
properties.push((
|
||||
k.to_string().map_err(|e| invalid(e.to_string()))?,
|
||||
v.to_string().map_err(|e| invalid(e.to_string()))?,
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
name: name.to_string().map_err(|e| invalid(e.to_string()))?,
|
||||
properties,
|
||||
properties: properties.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,9 @@ impl BlockRegistry {
|
||||
pub fn from_facts_json_with_flags(
|
||||
reader: impl io::Read,
|
||||
) -> io::Result<(Self, Vec<(bool, bool)>)> {
|
||||
let facts: facts::Facts = serde_json::from_reader(io::BufReader::new(reader))
|
||||
.map_err(|e| invalid(e.to_string()))?;
|
||||
let facts: facts::Facts =
|
||||
facts::with_shared_strings(|| serde_json::from_reader(io::BufReader::new(reader)))
|
||||
.map_err(|e| invalid(e.to_string()))?;
|
||||
let flags = facts.states.iter().map(|r| (r.air, r.fluid)).collect();
|
||||
let states = facts.states.into_iter().map(|r| r.state).collect();
|
||||
Ok((Self::from_states(states, facts.defaults)?, flags))
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
//! The part of `facts.json` the block registry reads, deserialized as it
|
||||
//! streams: each row's state (its fifth column) and the defaults.
|
||||
use crate::chunk::BlockState;
|
||||
use crate::{
|
||||
block_state_properties::SharedStrings,
|
||||
chunk::{BlockProperties, BlockState},
|
||||
};
|
||||
use serde::{
|
||||
Deserialize, Deserializer,
|
||||
de::{self, IgnoredAny, SeqAccess, Visitor},
|
||||
de::{self, IgnoredAny, MapAccess, SeqAccess, Visitor},
|
||||
};
|
||||
use std::{collections::BTreeMap, fmt};
|
||||
use std::{cell::RefCell, collections::BTreeMap, fmt, sync::Arc};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct Facts {
|
||||
@@ -48,31 +51,101 @@ impl<'de> Deserialize<'de> for Row {
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// The strings the states being read share (`with_shared_strings`).
|
||||
static STRINGS: RefCell<Option<SharedStrings>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Runs `read` with the states it reads sharing their property strings.
|
||||
pub(super) fn with_shared_strings<T>(read: impl FnOnce() -> T) -> T {
|
||||
STRINGS.with(|s| *s.borrow_mut() = Some(SharedStrings::default()));
|
||||
let out = read();
|
||||
STRINGS.with(|s| s.borrow_mut().take());
|
||||
out
|
||||
}
|
||||
|
||||
/// A property name or value, shared with the other states' equal ones.
|
||||
struct Shared(Arc<String>);
|
||||
impl<'de> Deserialize<'de> for Shared {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
struct SharedVisitor;
|
||||
impl Visitor<'_> for SharedVisitor {
|
||||
type Value = Shared;
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("a string")
|
||||
}
|
||||
fn visit_str<E: de::Error>(self, text: &str) -> Result<Shared, E> {
|
||||
Ok(Shared(STRINGS.with(|s| match s.borrow_mut().as_mut() {
|
||||
Some(strings) => strings.get(text),
|
||||
None => Arc::new(text.to_owned()),
|
||||
})))
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_str(SharedVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// A state's `properties` object, read straight into shared strings.
|
||||
struct Properties(BlockProperties);
|
||||
impl<'de> Deserialize<'de> for Properties {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
struct PropertiesVisitor;
|
||||
impl<'de> Visitor<'de> for PropertiesVisitor {
|
||||
type Value = Properties;
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("block state properties")
|
||||
}
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Properties, A::Error> {
|
||||
let mut pairs = Vec::new();
|
||||
while let Some((Shared(k), Shared(v))) = map.next_entry()? {
|
||||
pairs.push((k, v));
|
||||
}
|
||||
Ok(Properties(BlockProperties::from_shared(pairs)))
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_map(PropertiesVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// A state as BlockState::from_nbt reads it: a name, or `{"id", "properties"}`.
|
||||
struct State(BlockState);
|
||||
|
||||
impl<'de> Deserialize<'de> for State {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Shape {
|
||||
Name(String),
|
||||
Full {
|
||||
id: String,
|
||||
#[serde(default)]
|
||||
properties: BTreeMap<String, String>,
|
||||
},
|
||||
struct StateVisitor;
|
||||
impl<'de> Visitor<'de> for StateVisitor {
|
||||
type Value = State;
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("a block state name or {\"id\", \"properties\"}")
|
||||
}
|
||||
fn visit_str<E: de::Error>(self, name: &str) -> Result<State, E> {
|
||||
Ok(State(BlockState {
|
||||
name: name.to_owned(),
|
||||
properties: BlockProperties::new(),
|
||||
}))
|
||||
}
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<State, A::Error> {
|
||||
let (mut id, mut properties) = (None, None);
|
||||
while let Some(key) = map.next_key::<std::borrow::Cow<'de, str>>()? {
|
||||
match &*key {
|
||||
"id" if id.is_some() => return Err(de::Error::duplicate_field("id")),
|
||||
"id" => id = Some(map.next_value::<String>()?),
|
||||
"properties" if properties.is_some() => {
|
||||
return Err(de::Error::duplicate_field("properties"));
|
||||
}
|
||||
"properties" => properties = Some(map.next_value::<Properties>()?.0),
|
||||
_ => {
|
||||
map.next_value::<IgnoredAny>()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(State(BlockState {
|
||||
name: id.ok_or_else(|| de::Error::missing_field("id"))?,
|
||||
properties: properties.unwrap_or_default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
Ok(State(match Shape::deserialize(deserializer)? {
|
||||
Shape::Name(name) => BlockState {
|
||||
name,
|
||||
properties: Default::default(),
|
||||
},
|
||||
Shape::Full { id, properties } => BlockState {
|
||||
name: id,
|
||||
properties: properties.into(),
|
||||
},
|
||||
}))
|
||||
deserializer.deserialize_any(StateVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user