mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6424564e6 | ||
|
|
0871e93d49 | ||
|
|
cdb64fbc9b |
No files matched your search
@@ -213,6 +213,48 @@ impl Registries {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Only what [`Self::fill_from_export`] takes from an export, to keep
|
||||
/// in place of the export itself (a third of its memory).
|
||||
pub fn from_export(r: &serde_json::Value) -> Self {
|
||||
let mut out = Self::default();
|
||||
out.fill_from_export(r);
|
||||
out
|
||||
}
|
||||
/// [`Self::fill_from_export`] from an export already read with
|
||||
/// [`Self::from_export`]: each field the game left empty, each registry
|
||||
/// it did not list.
|
||||
pub fn fill_from_exported(&mut self, e: &Self) {
|
||||
let fill = |field: &mut Vec<String>, from: &Vec<String>| {
|
||||
if field.is_empty() {
|
||||
field.clone_from(from);
|
||||
}
|
||||
};
|
||||
fill(&mut self.effects, &e.effects);
|
||||
fill(&mut self.enchantments, &e.enchantments);
|
||||
fill(&mut self.attributes, &e.attributes);
|
||||
fill(&mut self.damage_types, &e.damage_types);
|
||||
fill(&mut self.templates, &e.templates);
|
||||
fill(&mut self.persistent_components, &e.persistent_components);
|
||||
fill(&mut self.learnable_recipes, &e.learnable_recipes);
|
||||
for (name, ids) in &e.by_registry {
|
||||
if !self.by_registry.contains_key(name) {
|
||||
self.by_registry.insert(name.clone(), ids.clone());
|
||||
}
|
||||
}
|
||||
if self.block_entities.is_empty() {
|
||||
self.block_entities.clone_from(&e.block_entities);
|
||||
}
|
||||
if self.block_tag_members.is_empty() {
|
||||
self.block_tag_members.clone_from(&e.block_tag_members);
|
||||
}
|
||||
if self.clock_time_markers.is_empty() {
|
||||
self.clock_time_markers.clone_from(&e.clock_time_markers);
|
||||
}
|
||||
if self.advancement_criteria.is_empty() {
|
||||
self.advancement_criteria
|
||||
.clone_from(&e.advancement_criteria);
|
||||
}
|
||||
}
|
||||
/// The element ids of a registry (sorted).
|
||||
pub fn ids(&self, registry: &str) -> &[String] {
|
||||
if let Some(r) = self.by_registry.get(registry) {
|
||||
|
||||
@@ -18723,10 +18723,12 @@ fn load_core(
|
||||
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(),
|
||||
)
|
||||
let library = render.model_library.clone();
|
||||
let item_renderer = if headless {
|
||||
horizon_client::item_render::ItemRenderer::load_for_server(root, library)
|
||||
} else {
|
||||
horizon_client::item_render::ItemRenderer::load_with_library(root, library)
|
||||
}
|
||||
.map_err(|e| e.to_string());
|
||||
Ok((render, item_renderer))
|
||||
});
|
||||
|
||||
@@ -252,13 +252,14 @@ fn block_properties(registry: &'static BlockRegistry) -> Arc<BlockProperties> {
|
||||
properties
|
||||
}
|
||||
|
||||
static COMMAND_EXPORTS: Mutex<Vec<(PathBuf, Option<Arc<serde_json::Value>>)>> =
|
||||
Mutex::new(Vec::new());
|
||||
static COMMAND_EXPORTS: Mutex<Vec<(PathBuf, Option<Arc<Registries>>)>> = Mutex::new(Vec::new());
|
||||
|
||||
/// A folder's `command-registries.json` (vanilla's built-in registries),
|
||||
/// read once like the resource ids: parsing its 1.6 MB again took about
|
||||
/// 15 ms of every world opening.
|
||||
fn command_export(dir: &Path) -> Option<Arc<serde_json::Value>> {
|
||||
/// What the registries take from a folder's `command-registries.json`
|
||||
/// (vanilla's built-in registries), read once like the resource ids: parsing
|
||||
/// its 1.6 MB again took about 15 ms of every world opening. Only the part
|
||||
/// the registries fill from stays, not the parsed export (4.2 MB, against
|
||||
/// 2.2 MB). A dedicated server opens its world once and keeps nothing.
|
||||
fn command_export(dir: &Path, keep: bool) -> Option<Arc<Registries>> {
|
||||
let mut cache = COMMAND_EXPORTS.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some((_, export)) = cache.iter().find(|(cached, _)| cached == dir) {
|
||||
return export.clone();
|
||||
@@ -266,8 +267,10 @@ fn command_export(dir: &Path) -> Option<Arc<serde_json::Value>> {
|
||||
let export = std::fs::read(dir.join("command-registries.json"))
|
||||
.ok()
|
||||
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
|
||||
.map(Arc::new);
|
||||
cache.push((dir.to_owned(), export.clone()));
|
||||
.map(|export| Arc::new(Registries::from_export(&export)));
|
||||
if keep {
|
||||
cache.push((dir.to_owned(), export.clone()));
|
||||
}
|
||||
export
|
||||
}
|
||||
|
||||
@@ -531,8 +534,8 @@ impl Game<'_> {
|
||||
}
|
||||
// Vanilla's built-in registries (mob effects, components, dialogs...).
|
||||
for dir in [&self.options.resources, &self.ui.vanilla_root] {
|
||||
if let Some(export) = command_export(dir) {
|
||||
self.ui.registries.fill_from_export(&export);
|
||||
if let Some(export) = command_export(dir, self.options.dedicated.is_none()) {
|
||||
self.ui.registries.fill_from_exported(&export);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1552,11 +1555,22 @@ mod tests {
|
||||
let dir = std::env::temp_dir().join(format!("horizon-export-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("command-registries.json"), r#"{"effects":["a"]}"#).unwrap();
|
||||
let first = command_export(&dir).unwrap();
|
||||
assert_eq!(first["effects"][0], "a");
|
||||
let first = command_export(&dir, true).unwrap();
|
||||
assert_eq!(first.effects, ["a"]);
|
||||
std::fs::write(dir.join("command-registries.json"), "{}").unwrap();
|
||||
assert!(Arc::ptr_eq(&first, &command_export(&dir).unwrap()));
|
||||
assert!(command_export(&dir.join("missing")).is_none());
|
||||
assert!(Arc::ptr_eq(&first, &command_export(&dir, true).unwrap()));
|
||||
assert!(command_export(&dir.join("missing"), true).is_none());
|
||||
// A dedicated server's read is not kept.
|
||||
let other = dir.join("other");
|
||||
std::fs::create_dir_all(&other).unwrap();
|
||||
std::fs::write(
|
||||
other.join("command-registries.json"),
|
||||
r#"{"effects":["b"]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let once = command_export(&other, false).unwrap();
|
||||
assert_eq!(once.effects, ["b"]);
|
||||
assert!(!Arc::ptr_eq(&once, &command_export(&other, false).unwrap()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -547,13 +547,34 @@ impl ItemRenderer {
|
||||
pub fn load_with_library(
|
||||
root: &Path,
|
||||
library: std::result::Result<Arc<ModelLibrary>, String>,
|
||||
) -> crate::Result<Self> {
|
||||
Self::load_tables(root, library, true)
|
||||
}
|
||||
/// What a dedicated server reads: the item prototypes (the stacks'
|
||||
/// default components) and ids, without the client item definitions,
|
||||
/// the special renderers' entity models and the grass colormap, which
|
||||
/// only drawing reads (1.4 MB and ~1,500 files).
|
||||
pub fn load_for_server(
|
||||
root: &Path,
|
||||
library: std::result::Result<Arc<ModelLibrary>, String>,
|
||||
) -> crate::Result<Self> {
|
||||
Self::load_tables(root, library, false)
|
||||
}
|
||||
fn load_tables(
|
||||
root: &Path,
|
||||
library: std::result::Result<Arc<ModelLibrary>, String>,
|
||||
drawn: bool,
|
||||
) -> crate::Result<Self> {
|
||||
let prototypes: BTreeMap<String, Components> = horizon_world::item_defaults::read(root)?
|
||||
.into_iter()
|
||||
.map(|(item, components)| Ok((item, serde_json::from_value(components)?)))
|
||||
.collect::<serde_json::Result<_>>()?;
|
||||
let models = ItemModels {
|
||||
definitions: load_definitions(root),
|
||||
definitions: if drawn {
|
||||
load_definitions(root)
|
||||
} else {
|
||||
BTreeMap::new()
|
||||
},
|
||||
prototypes,
|
||||
};
|
||||
let library = library?;
|
||||
@@ -561,10 +582,18 @@ impl ItemRenderer {
|
||||
&fs::read_to_string(root.join("assets/minecraft/atlases/items.json"))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let parts = special::load_parts(root);
|
||||
let grass = crate::hud::decode(&root.join("assets/minecraft/textures/colormap/grass.png"))
|
||||
.map(|(_, _, rgba)| argb_pixels(&rgba).into_iter().map(|p| p as i32).collect())
|
||||
.unwrap_or_default();
|
||||
let parts = if drawn {
|
||||
special::load_parts(root)
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
let grass = if drawn {
|
||||
crate::hud::decode(&root.join("assets/minecraft/textures/colormap/grass.png"))
|
||||
.map(|(_, _, rgba)| argb_pixels(&rgba).into_iter().map(|p| p as i32).collect())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let item_ids = horizon_protocol::registry::Builtin::find(root)
|
||||
.and_then(|p| horizon_protocol::registry::Builtin::load(&p).ok())
|
||||
.and_then(|b| {
|
||||
@@ -993,6 +1022,31 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A dedicated server keeps the stacks' default components but reads
|
||||
/// none of the client item definitions.
|
||||
#[test]
|
||||
fn a_server_keeps_prototypes_without_definitions() -> crate::Result<()> {
|
||||
let root = std::env::temp_dir().join(format!("horizon-item-server-{}", std::process::id()));
|
||||
let definitions = root.join("assets/horizon/items");
|
||||
fs::create_dir_all(&definitions)?;
|
||||
fs::write(
|
||||
definitions.join("top.json"),
|
||||
r#"{"model":{"type":"minecraft:empty"}}"#,
|
||||
)?;
|
||||
fs::write(
|
||||
root.join("items.json"),
|
||||
r#"{"horizon:top":{"minecraft:max_stack_size":16}}"#,
|
||||
)?;
|
||||
let library = crate::model_library::ParsedModels::load(&root).into_item_library();
|
||||
let client = ItemRenderer::load_with_library(&root, library.clone())?;
|
||||
let server = ItemRenderer::load_for_server(&root, library)?;
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
assert!(client.models.definitions.contains_key("horizon:top"));
|
||||
assert!(server.models.definitions.is_empty());
|
||||
assert_eq!(server.models.prototypes, client.models.prototypes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendered_amounts_follow_the_count_bands() {
|
||||
let bands: Vec<u32> = [0, 1, 2, 16, 17, 32, 33, 48, 49, 64]
|
||||
|
||||
@@ -19,8 +19,10 @@ pub struct ServerAdvancements {
|
||||
tree: std::sync::Arc<Tree>,
|
||||
kids: Vec<Vec<usize>>,
|
||||
layout: Vec<Option<(i32, f32)>>,
|
||||
/// Recipe files by id.
|
||||
recipes: BTreeMap<String, Value>,
|
||||
/// Recipe files by id, as compact JSON text: a book entry parses its
|
||||
/// recipe when a player gets it (a few µs), and the ~1,400 parsed files
|
||||
/// would keep about 5 MB alive for the server's whole run.
|
||||
recipes: BTreeMap<String, Box<str>>,
|
||||
displays: RecipeIndex,
|
||||
/// RecipeManager.unpackRecipeInfo's group numbers.
|
||||
groups: HashMap<String, i32>,
|
||||
@@ -48,6 +50,10 @@ impl ServerAdvancements {
|
||||
groups.entry(g.to_owned()).or_insert(next);
|
||||
}
|
||||
}
|
||||
let recipes = recipes
|
||||
.into_iter()
|
||||
.map(|(id, r)| (id, r.to_string().into_boxed_str()))
|
||||
.collect();
|
||||
Self {
|
||||
tree,
|
||||
kids,
|
||||
@@ -351,14 +357,19 @@ impl ServerAdvancements {
|
||||
/// ClientboundRecipeBookAddPacket.Entry for each display of a recipe:
|
||||
/// highlighted, and with a notification unless the recipe turns it off.
|
||||
fn book_entries(&self, id: &str, sync: &SyncData, flags: i8) -> Vec<Vec<u8>> {
|
||||
let Some(r) = self.recipes.get(id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let ids = self.displays.display_ids(id);
|
||||
if ids.len() != 1 {
|
||||
// Special recipes have no display; transmutations aren't unlocked on joining.
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(r) = self
|
||||
.recipes
|
||||
.get(id)
|
||||
.and_then(|text| serde_json::from_str::<Value>(text).ok())
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let r = &r;
|
||||
let kind = r["type"].as_str().unwrap_or("");
|
||||
let item = |n: &str| sync.id("minecraft:item", n).unwrap_or(0);
|
||||
let slot = |n: &str| sync.id("minecraft:slot_display", n).unwrap_or(0);
|
||||
|
||||
Reference in new issue
Block a user