mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge upstream main into the Setup PR
Re-export list conflict in client_lib.rs: main's list (json_folder) plus split_map. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
afe39ff137
8 files changed
+213
-91
No files matched your search
@@ -9,7 +9,7 @@ use crate::{
|
||||
};
|
||||
use horizon_world::loot::PackOverlay;
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashSet, fs, path::Path};
|
||||
use std::{collections::HashSet, path::Path};
|
||||
|
||||
pub use horizon_client_core::potions::{
|
||||
BASE_POTION_COLOR, Category, EFFECTS, EffectDef, POTIONS, PotionDef, all_effects,
|
||||
@@ -158,6 +158,28 @@ pub struct Brewing {
|
||||
/// Items accepted in the bottle slots (`PotionIngredient.isPotionInput`).
|
||||
pub inputs: HashSet<String>,
|
||||
}
|
||||
/// The files right in `recipe/brewing/`, parsed, by id: taken from the whole
|
||||
/// recipe folder's read when another table holds it (the start loads them
|
||||
/// together), else read alone. A file that doesn't parse is left out.
|
||||
fn brewing_sources(root: &Path) -> Vec<(String, Value)> {
|
||||
let recipe = root.join("data/minecraft/recipe");
|
||||
let (folder, prefix) = match crate::json_folder::held(&recipe) {
|
||||
Some(folder) => (folder, "brewing/"),
|
||||
None => match crate::json_folder::read(&recipe.join("brewing")) {
|
||||
Ok(folder) => (folder, ""),
|
||||
Err(_) => return Vec::new(),
|
||||
},
|
||||
};
|
||||
folder
|
||||
.files
|
||||
.iter()
|
||||
.filter_map(|f| {
|
||||
let name = f.name.strip_prefix(prefix).filter(|n| !n.contains('/'))?;
|
||||
let v = serde_json::from_slice::<Value>(f.bytes().ok()?).ok()?;
|
||||
Some((format!("minecraft:brewing/{name}"), v))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
impl Brewing {
|
||||
pub fn load(root: &Path, items: &Items, overlay: &PackOverlay) -> Self {
|
||||
// NeoForge's load conditions: a file whose don't hold is silently
|
||||
@@ -172,23 +194,7 @@ impl Brewing {
|
||||
let conditions_hold = |v: &Value| {
|
||||
horizon_world::conditions::conditions_hold(v, &|id: &str| mods.iter().any(|m| m == id))
|
||||
};
|
||||
let mut sources: Vec<(String, Value)> =
|
||||
fs::read_dir(root.join("data/minecraft/recipe/brewing"))
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.filter_map(|e| {
|
||||
let bytes = fs::read(e.path()).ok()?;
|
||||
let v = serde_json::from_slice::<Value>(&bytes).ok()?;
|
||||
Some((
|
||||
format!(
|
||||
"minecraft:brewing/{}",
|
||||
e.file_name().to_string_lossy().strip_suffix(".json")?
|
||||
),
|
||||
v,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
let mut sources: Vec<(String, Value)> = brewing_sources(root);
|
||||
// The world's packs' brewing recipes over the game's own: the top
|
||||
// pack's file for each id replaces it (`SimpleJsonResourceReloadListener`).
|
||||
for (id, files) in overlay.kind("recipe") {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
//! A data folder's files read from the disk once while a load holds them.
|
||||
//! Several tables are built from the same folder (the recipes: the crafting
|
||||
//! tables, the stations, the recipe book, brewing), and each used to list and
|
||||
//! read every file again: on a hard disk or under a virus scanner, each open
|
||||
//! costs. The first reader lists and reads the folder; later readers share
|
||||
//! those bytes for as long as one of them holds the [`Folder`], then it is
|
||||
//! gone (nothing is kept past the load).
|
||||
use std::{
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex, Weak},
|
||||
};
|
||||
|
||||
/// One file: its path inside the folder without `.json` (`a/b`, as an id has
|
||||
/// it), the file, and its bytes or the error reading it gave.
|
||||
pub struct File {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
bytes: Result<Box<[u8]>, (io::ErrorKind, String)>,
|
||||
}
|
||||
impl File {
|
||||
pub fn bytes(&self) -> io::Result<&[u8]> {
|
||||
self.bytes
|
||||
.as_deref()
|
||||
.map_err(|(kind, message)| io::Error::new(*kind, message.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `.json` file under a folder, its subfolders' too, in name order.
|
||||
pub struct Folder {
|
||||
pub files: Vec<File>,
|
||||
}
|
||||
|
||||
static OPEN: Mutex<Vec<(PathBuf, Weak<Folder>)>> = Mutex::new(Vec::new());
|
||||
|
||||
fn find(open: &[(PathBuf, Weak<Folder>)], dir: &Path) -> Option<Arc<Folder>> {
|
||||
open.iter()
|
||||
.find(|(d, _)| d == dir)
|
||||
.and_then(|(_, f)| f.upgrade())
|
||||
}
|
||||
|
||||
/// `dir`'s files if another reader holds them, without reading anything.
|
||||
pub fn held(dir: &Path) -> Option<Arc<Folder>> {
|
||||
find(&OPEN.lock().unwrap_or_else(|e| e.into_inner()), dir)
|
||||
}
|
||||
|
||||
/// The `.json` files under `dir`: shared with any other reader holding them,
|
||||
/// else listed and read now. An error when `dir` itself can't be listed.
|
||||
pub fn read(dir: &Path) -> io::Result<Arc<Folder>> {
|
||||
if let Some(folder) = held(dir) {
|
||||
return Ok(folder);
|
||||
}
|
||||
let files = list(dir)?
|
||||
.into_iter()
|
||||
.map(|(name, path)| {
|
||||
let bytes = fs::read(&path)
|
||||
.map(Vec::into_boxed_slice)
|
||||
.map_err(|e| (e.kind(), e.to_string()));
|
||||
File { name, path, bytes }
|
||||
})
|
||||
.collect();
|
||||
let folder = Arc::new(Folder { files });
|
||||
let mut open = OPEN.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// Another reader may have read it meanwhile: share one copy.
|
||||
if let Some(theirs) = find(&open, dir) {
|
||||
return Ok(theirs);
|
||||
}
|
||||
open.retain(|(_, f)| f.strong_count() > 0);
|
||||
open.push((dir.to_owned(), Arc::downgrade(&folder)));
|
||||
Ok(folder)
|
||||
}
|
||||
|
||||
/// The `.json` files under `dir` and its subfolders, in name order: each
|
||||
/// one's path inside the folder without the extension (`a/b`) and its file.
|
||||
/// An error when `dir` itself can't be listed; a subfolder or an entry that
|
||||
/// can't be is warned and skipped, as vanilla's `PathPackResources` keeps
|
||||
/// what it found.
|
||||
pub fn list(dir: &Path) -> io::Result<Vec<(String, PathBuf)>> {
|
||||
let mut files = Vec::new();
|
||||
let mut stack = vec![fs::read_dir(dir)?];
|
||||
while let Some(entries) = stack.pop() {
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(e) => {
|
||||
log::warn!("Horizon: failed to list an entry of {}: {e}", dir.display());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
match fs::read_dir(&path) {
|
||||
Ok(list) => stack.push(list),
|
||||
Err(e) => log::warn!("Horizon: failed to list {}: {e}", path.display()),
|
||||
}
|
||||
} else if path.extension().is_some_and(|e| e == "json") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut named: Vec<(String, PathBuf)> = files
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let inside = path.strip_prefix(dir).unwrap_or(&path).with_extension("");
|
||||
let name: Vec<_> = inside.iter().map(|c| c.to_string_lossy()).collect();
|
||||
(name.join("/"), path)
|
||||
})
|
||||
.collect();
|
||||
// Vanilla lists resources by id, in a map keyed by it
|
||||
// (`FallbackResourceManager`): `crushing` before `crushing/iron`, where
|
||||
// a path's components put the subfolder's file first.
|
||||
named.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok(named)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn folder(tag: &str) -> PathBuf {
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("horizon-json-folder-{tag}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
fs::create_dir_all(dir.join("brewing")).unwrap();
|
||||
fs::write(dir.join("stick.json"), b"{\"a\":1}").unwrap();
|
||||
fs::write(dir.join("brewing/awkward.json"), b"{}").unwrap();
|
||||
fs::write(dir.join("notes.txt"), b"not json").unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readers_share_the_bytes_while_one_holds_them_and_read_again_after() {
|
||||
let dir = folder("share");
|
||||
let first = read(&dir).unwrap();
|
||||
let names: Vec<_> = first.files.iter().map(|f| f.name.as_str()).collect();
|
||||
assert_eq!(names, ["brewing/awkward", "stick"]);
|
||||
assert_eq!(first.files[1].bytes().unwrap(), b"{\"a\":1}");
|
||||
// Held: a change on the disk isn't seen, the copy read is shared.
|
||||
fs::write(dir.join("stick.json"), b"{\"a\":2}").unwrap();
|
||||
let second = read(&dir).unwrap();
|
||||
assert!(Arc::ptr_eq(&first, &second));
|
||||
drop((first, second));
|
||||
// Let go: the next reader reads the folder again.
|
||||
assert_eq!(read(&dir).unwrap().files[1].bytes().unwrap(), b"{\"a\":2}");
|
||||
fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_folder_that_cannot_be_listed_is_an_error() {
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("horizon-json-folder-none-{}", std::process::id()));
|
||||
assert!(read(&dir).is_err());
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ pub mod hud_overlays;
|
||||
pub mod inventory_effects;
|
||||
pub mod java_text;
|
||||
pub mod jigsaw_block;
|
||||
pub mod json_folder;
|
||||
pub mod level_loading;
|
||||
pub mod light;
|
||||
pub mod lod;
|
||||
|
||||
@@ -13,14 +13,14 @@ pub use horizon_client_render::{
|
||||
chunk_map, cit, cit_armor, cit_assets, cit_glint, clouds, collisions, command_block,
|
||||
containers, crash, datapack, dynamic_light, effects, environment, extras, fast_hash, fog, font,
|
||||
gfx, graphics, graphics_style, graphics_ui, hdr_calibration, hud, hud_layout, hud_overlays,
|
||||
inventory_effects, java_text, jigsaw_block, level_loading, light, lod, logging, menu_blur,
|
||||
mesher, minimap, mipmap, mod_list, model_library, models, multiline_edit, native_emissive,
|
||||
options_file, options_fixes, options_ui, overlay, pack_screen, packs, par, particles,
|
||||
poi_sections, post_chain, potent_sulfur, preedit, providers, screen_draw, screen_effects,
|
||||
sculk, section_mask, settings, shader_options, shader_pack_list, signs, skins, sound_engine,
|
||||
sound_events, sound_preview, spans, split_layout, split_map, split_profiles, sprites,
|
||||
streaming, structure_block, tab_list, telemetry, test_block, test_instance_block, testgen,
|
||||
title_edition, trims, ui, vault, weather, widgets, world, worldborder, zip,
|
||||
inventory_effects, java_text, jigsaw_block, json_folder, level_loading, light, lod, logging,
|
||||
menu_blur, mesher, minimap, mipmap, mod_list, model_library, models, multiline_edit,
|
||||
native_emissive, options_file, options_fixes, options_ui, overlay, pack_screen, packs, par,
|
||||
particles, poi_sections, post_chain, potent_sulfur, preedit, providers, screen_draw,
|
||||
screen_effects, sculk, section_mask, settings, shader_options, shader_pack_list, signs, skins,
|
||||
sound_engine, sound_events, sound_preview, spans, split_layout, split_map, split_profiles,
|
||||
sprites, streaming, structure_block, tab_list, telemetry, test_block, test_instance_block,
|
||||
testgen, title_edition, trims, ui, vault, weather, widgets, world, worldborder, zip,
|
||||
};
|
||||
pub mod attack_strength;
|
||||
pub mod attribute_updates;
|
||||
|
||||
@@ -14,8 +14,7 @@ use horizon_world::nbt::Tag;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -1208,48 +1207,6 @@ pub fn recipe_result(v: &Value) -> Option<ItemStack> {
|
||||
let template = Template::parse(v)?;
|
||||
template.item.is_some().then(|| template.create())
|
||||
}
|
||||
/// The `.json` files under a `recipe` folder, its subfolders' too, in id
|
||||
/// order: each one's path inside the folder without the extension (`a/b`, as a
|
||||
/// recipe id has it) and its file. An error when `dir` itself can't be read; a
|
||||
/// subfolder or an entry that can't be listed is warned and skipped, as
|
||||
/// vanilla's `PathPackResources` keeps what it found.
|
||||
pub(crate) fn recipe_files(dir: &Path) -> Result<Vec<(String, PathBuf)>> {
|
||||
let mut files = Vec::new();
|
||||
let mut stack = vec![fs::read_dir(dir)?];
|
||||
while let Some(entries) = stack.pop() {
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(e) => {
|
||||
log::warn!("Horizon: failed to list an entry of {}: {e}", dir.display());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
match fs::read_dir(&path) {
|
||||
Ok(list) => stack.push(list),
|
||||
Err(e) => log::warn!("Horizon: failed to list {}: {e}", path.display()),
|
||||
}
|
||||
} else if path.extension().is_some_and(|e| e == "json") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut named: Vec<(String, PathBuf)> = files
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let inside = path.strip_prefix(dir).unwrap_or(&path).with_extension("");
|
||||
let name: Vec<_> = inside.iter().map(|c| c.to_string_lossy()).collect();
|
||||
(name.join("/"), path)
|
||||
})
|
||||
.collect();
|
||||
// Vanilla lists resources by id, in a map keyed by it
|
||||
// (`FallbackResourceManager`): `crushing` before `crushing/iron`, where
|
||||
// a path's components put the subfolder's file first.
|
||||
named.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok(named)
|
||||
}
|
||||
/// The type of a recipe the game doesn't craft itself: one outside `minecraft`.
|
||||
/// A recipe's whole id from its name in the tables: vanilla's are kept bare
|
||||
/// (`oak_planks`), everyone else's whole (`mymod:crusher/iron`).
|
||||
@@ -1372,14 +1329,14 @@ impl Recipes {
|
||||
}
|
||||
mods
|
||||
};
|
||||
for (name, p) in recipe_files(&root.join("data/minecraft/recipe"))? {
|
||||
sources.push((name, fs::read(&p)?));
|
||||
for file in &crate::json_folder::read(&root.join("data/minecraft/recipe"))?.files {
|
||||
sources.push((file.name.clone(), file.bytes()?.to_vec()));
|
||||
}
|
||||
for namespace in crate::mod_content::namespaces(root) {
|
||||
let dir = root.join(format!("data/{namespace}/recipe"));
|
||||
if dir.is_dir() {
|
||||
for (name, p) in recipe_files(&dir)? {
|
||||
sources.push((format!("{namespace}:{name}"), fs::read(&p)?));
|
||||
for file in &crate::json_folder::read(&dir)?.files {
|
||||
sources.push((format!("{namespace}:{}", file.name), file.bytes()?.to_vec()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1887,6 +1844,7 @@ impl Recipes {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::banner::BannerLayer;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn result_count_is_bounded_like_vanilla() {
|
||||
|
||||
@@ -18996,6 +18996,9 @@ impl Game<'_> {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// The recipe tables, the stations and the recipe book read one folder:
|
||||
// hold its files so it is read once.
|
||||
let _recipe_files = horizon_client::json_folder::read(&root.join("data/minecraft/recipe"));
|
||||
let recipes = match Recipes::load(root, &items, &overlay) {
|
||||
Ok(recipes) => recipes,
|
||||
Err(e) => {
|
||||
@@ -19627,6 +19630,9 @@ fn load_world_rest(
|
||||
let items = startup::time("rest.items", || Items::load(root, registry, facts, overlay))?;
|
||||
log::info!("load items: {:.2}s", started.elapsed().as_secs_f32());
|
||||
progress.set(7);
|
||||
// The recipe tables, the stations and the recipe book read one folder:
|
||||
// hold its files so it is read once.
|
||||
let _recipe_files = horizon_client::json_folder::read(&root.join("data/minecraft/recipe"));
|
||||
let recipes = startup::time("rest.recipes", || Recipes::load(root, &items, overlay))?;
|
||||
let stations = startup::time("rest.stations", || {
|
||||
horizon_client::stations::StationData::load(root, &items, overlay)
|
||||
|
||||
@@ -717,14 +717,13 @@ impl RecipeIndex {
|
||||
// The packs' files replace first and the gate answers the merged
|
||||
// file, as the crafting loader's does: a false-conditioned pack
|
||||
// file leaves the lower one gone too.
|
||||
let mut sources: Vec<(String, serde_json::Value)> = crate::crafting::recipe_files(&dir)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|(name, path)| {
|
||||
let v = std::fs::read(&path)
|
||||
.ok()
|
||||
.and_then(|b| serde_json::from_slice::<serde_json::Value>(&b).ok())?;
|
||||
Some((format!("minecraft:{name}"), v))
|
||||
let folder = crate::json_folder::read(&dir).ok();
|
||||
let mut sources: Vec<(String, serde_json::Value)> = folder
|
||||
.iter()
|
||||
.flat_map(|folder| &folder.files)
|
||||
.filter_map(|file| {
|
||||
let v = serde_json::from_slice::<serde_json::Value>(file.bytes().ok()?).ok()?;
|
||||
Some((format!("minecraft:{}", file.name), v))
|
||||
})
|
||||
.collect();
|
||||
for (id, files) in overlay.kind("recipe") {
|
||||
|
||||
@@ -503,18 +503,16 @@ impl StationRecipes {
|
||||
};
|
||||
for namespace in namespaces {
|
||||
let dir = root.join(format!("data/{namespace}/recipe"));
|
||||
let more = crate::crafting::recipe_files(&dir).unwrap_or_default();
|
||||
files.extend(
|
||||
more.into_iter()
|
||||
.map(|(name, p)| (namespace.clone(), name, p)),
|
||||
);
|
||||
if let Ok(folder) = crate::json_folder::read(&dir) {
|
||||
files.push((namespace, folder));
|
||||
}
|
||||
}
|
||||
let mut sources: Vec<(String, Value)> = files
|
||||
.iter()
|
||||
.filter_map(|(namespace, name, p)| {
|
||||
let bytes = fs::read(p).ok()?;
|
||||
let v = serde_json::from_slice::<Value>(&bytes).ok()?;
|
||||
Some((format!("{namespace}:{name}"), v))
|
||||
.flat_map(|(namespace, folder)| folder.files.iter().map(move |f| (namespace, f)))
|
||||
.filter_map(|(namespace, file)| {
|
||||
let v = serde_json::from_slice::<Value>(file.bytes().ok()?).ok()?;
|
||||
Some((format!("{namespace}:{}", file.name), v))
|
||||
})
|
||||
.collect();
|
||||
// The world's packs' recipes over the game's own: the top pack's
|
||||
|
||||
Reference in new issue
Block a user