Pumpkin/crates/pumpkin-world/build.rs

489 lines
14 KiB
Rust

#![allow(clippy::unwrap_used, clippy::expect_used, clippy::too_many_lines)]
use std::collections::BTreeMap;
use std::env;
use std::fmt::Write;
use std::fs;
use std::path::{Path, PathBuf};
const ATTRS: &str = "\
#[allow(clippy::too_many_lines)]
#[allow(clippy::match_same_arms)]
#[allow(clippy::missing_const_for_fn)]
#[allow(clippy::match_single_binding)]
#[must_use]
";
/// Base packs, relative to `CARGO_MANIFEST_DIR`.
///
/// These are applied first. Each pack must contain `data/<namespace>/`.
///
/// The first field is the logical datapack id exposed at runtime.
/// The second field is the path relative to `CARGO_MANIFEST_DIR`.
const DATAPACK_PACKS: &[(&str, &str)] = &[("vanilla", "../../assets/datapack")];
/// Container dirs, relative to `CARGO_MANIFEST_DIR`.
///
/// Every immediate subdirectory is treated as a separate embedded datapack.
/// Container packs are applied after [`DATAPACK_PACKS`] in sorted order, so
/// later packs override earlier resources with the same resource id.
const DATAPACK_CONTAINERS: &[&str] = &["../../assets/tests/datapacks"];
/// Vanilla's implicit namespace: a bare `foo` is exactly `minecraft:foo`.
const DEFAULT_NAMESPACE: &str = "minecraft";
/// One registry's match arms, keyed by fully-qualified resource id.
///
/// Insertion overwrites, so a later datapack replaces an earlier datapack's
/// resource with the same id, matching vanilla datapack override semantics.
type Registry = BTreeMap<String, String>;
#[derive(Debug)]
struct EmbeddedPack {
/// Logical datapack id exposed to the runtime.
id: String,
/// Physical source directory used at compile time.
path: PathBuf,
}
#[derive(Default)]
struct Arms {
templates: Registry,
pools: Registry,
template_pool_json: Registry,
processor_list_json: Registry,
test_instance_json: Registry,
}
fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_dir = Path::new(&manifest_dir);
// Load order:
//
// 1. Base packs.
// 2. Container packs, sorted by directory name.
//
// Resources inserted later replace resources inserted earlier.
let mut packs = Vec::<EmbeddedPack>::new();
for &(id, relative_path) in DATAPACK_PACKS {
println!("cargo:rerun-if-changed={relative_path}");
packs.push(EmbeddedPack {
id: id.to_string(),
path: manifest_dir.join(relative_path),
});
}
for container in DATAPACK_CONTAINERS {
println!("cargo:rerun-if-changed={container}");
let container_dir = manifest_dir.join(container);
if !container_dir.is_dir() {
println!(
"cargo:warning=missing datapack container: {}",
container_dir.display()
);
continue;
}
let mut found = fs::read_dir(&container_dir)
.unwrap()
.map(Result::unwrap)
.filter(|entry| entry.path().is_dir())
.collect::<Vec<_>>();
// Deterministic datapack load order.
found.sort_by_key(std::fs::DirEntry::file_name);
for entry in found {
let id = entry.file_name().into_string().unwrap();
let path = entry.path();
packs.push(EmbeddedPack { id, path });
}
}
let mut arms = Arms::default();
let mut embedded_pack_names = Vec::<String>::new();
for pack in &packs {
if embed_pack(&pack.path, &mut arms) {
// A duplicate pack id can technically occur if multiple configured
// locations contain a pack with the same name. Resources still use
// normal load-order semantics, but the public pack list should only
// contain the id once.
if !embedded_pack_names.iter().any(|id| id == &pack.id) {
embedded_pack_names.push(pack.id.clone());
}
}
}
let mut template_out = String::new();
// ---------------------------------------------------------------------
// Structure/template resource lookup functions
// ---------------------------------------------------------------------
template_out.push_str(&wrap_fn(
"get_template_bytes",
"path",
"Option<&'static [u8]>",
&arms.templates,
));
template_out.push_str(&wrap_fn(
"get_pool_elements",
"pool_id",
"Option<&'static [&'static str]>",
&arms.pools,
));
template_out.push_str(&wrap_fn(
"get_template_pool_json",
"path",
"Option<&'static str>",
&arms.template_pool_json,
));
template_out.push_str(&wrap_fn(
"get_processor_list_json",
"path",
"Option<&'static str>",
&arms.processor_list_json,
));
// ---------------------------------------------------------------------
// Structure/template resource-name lists
// ---------------------------------------------------------------------
template_out.push_str(&name_list_fn(
"_generated_all_template_names",
&arms.templates,
));
template_out.push_str(&name_list_fn("_generated_all_pool_names", &arms.pools));
// ---------------------------------------------------------------------
// Datapack identity
// ---------------------------------------------------------------------
template_out.push_str(&string_list_fn(
"_generated_all_embedded_datapack_names",
&embedded_pack_names,
));
let out_dir = env::var_os("OUT_DIR").unwrap();
let out_dir = Path::new(&out_dir);
fs::write(out_dir.join("template_embeddings.rs"), template_out).unwrap();
// Test instances are a datapack registry of their own. Keep their generated
// resource table separate from the structure-template cache so consumers do
// not need to reach through `generation::structure::template`.
let mut test_instance_out = String::new();
test_instance_out.push_str(&wrap_fn(
"get_test_instance_json",
"path",
"Option<&'static str>",
&arms.test_instance_json,
));
test_instance_out.push_str(&name_list_fn(
"_generated_all_test_instance_names",
&arms.test_instance_json,
));
fs::write(
out_dir.join("test_instance_embeddings.rs"),
test_instance_out,
)
.unwrap();
}
/// Scans every namespace under `<pack>/data/`.
///
/// Returns `true` when the directory represents a valid datapack data root.
/// A valid but otherwise empty `data/` directory still counts as an embedded
/// datapack so it can be exposed through `/datapack list`.
fn embed_pack(pack_dir: &Path, arms: &mut Arms) -> bool {
let data_dir = pack_dir.join("data");
if !data_dir.is_dir() {
println!(
"cargo:warning=no data/ directory in pack: {}",
pack_dir.display()
);
return false;
}
let mut namespaces = fs::read_dir(&data_dir)
.unwrap()
.map(Result::unwrap)
.filter(|entry| entry.path().is_dir())
.map(|entry| (entry.file_name().into_string().unwrap(), entry.path()))
.collect::<Vec<_>>();
namespaces.sort_by(|left, right| left.0.cmp(&right.0));
for (namespace, namespace_dir) in namespaces {
embed_namespace(&namespace, &namespace_dir, arms);
}
true
}
/// Emits embedded resources for all supported registries within a single
/// `data/<namespace>/` directory.
fn embed_namespace(namespace: &str, namespace_dir: &Path, arms: &mut Arms) {
let structures_dir = namespace_dir.join("structure");
if structures_dir.is_dir() {
let mut pools: BTreeMap<String, Vec<String>> = BTreeMap::new();
process_structure_dir(
&structures_dir,
"",
&mut arms.templates,
&mut pools,
namespace,
);
for (pool_id, elements) in pools {
let mut arm = format!("{} => Some(&[\n", patterns(namespace, &pool_id));
for element in elements {
let _ = writeln!(arm, " \"{element}\",");
}
arm.push_str(" ]),");
arms.pools.insert(qualify(namespace, &pool_id), arm);
}
}
let worldgen_dir = namespace_dir.join("worldgen");
process_json_dir(
&worldgen_dir.join("template_pool"),
"",
&mut arms.template_pool_json,
namespace,
);
process_json_dir(
&worldgen_dir.join("processor_list"),
"",
&mut arms.processor_list_json,
namespace,
);
process_json_dir(
&namespace_dir.join("test_instance"),
"",
&mut arms.test_instance_json,
namespace,
);
}
/// Recursively emits `include_bytes!` match arms for every `.nbt` structure.
///
/// It also groups each directory's direct NBT children into the existing
/// structure-pool helper used elsewhere by Pumpkin.
fn process_structure_dir(
dir: &Path,
prefix: &str,
registry: &mut Registry,
pools: &mut BTreeMap<String, Vec<String>>,
namespace: &str,
) {
let mut entries = fs::read_dir(dir)
.unwrap()
.map(Result::unwrap)
.collect::<Vec<_>>();
entries.sort_by_key(std::fs::DirEntry::file_name);
for entry in entries {
let path = entry.path();
let name = entry.file_name().into_string().unwrap();
if path.is_dir() {
let new_prefix = if prefix.is_empty() {
name
} else {
format!("{prefix}/{name}")
};
process_structure_dir(&path, &new_prefix, registry, pools, namespace);
continue;
}
if !path
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("nbt"))
{
continue;
}
let stem = path.file_stem().unwrap().to_string_lossy();
let template_name = if prefix.is_empty() {
stem.to_string()
} else {
format!("{prefix}/{stem}")
};
let resource_id = qualify(namespace, &template_name);
let absolute_path = path.canonicalize().unwrap();
registry.insert(
resource_id.clone(),
format!(
"{patterns} => Some(include_bytes!(r#\"{path}\"#)),",
patterns = patterns(namespace, &template_name),
path = absolute_path.display(),
),
);
if !prefix.is_empty() {
pools
.entry(prefix.to_string())
.or_default()
.push(resource_id);
}
}
}
/// Recursively emits `include_str!` match arms for every `.json` resource.
fn process_json_dir(dir: &Path, prefix: &str, registry: &mut Registry, namespace: &str) {
if !dir.is_dir() {
return;
}
let mut entries = fs::read_dir(dir)
.unwrap()
.map(Result::unwrap)
.collect::<Vec<_>>();
entries.sort_by_key(std::fs::DirEntry::file_name);
for entry in entries {
let path = entry.path();
let name = entry.file_name().into_string().unwrap();
if path.is_dir() {
let new_prefix = if prefix.is_empty() {
name
} else {
format!("{prefix}/{name}")
};
process_json_dir(&path, &new_prefix, registry, namespace);
continue;
}
let Some(stem) = name.strip_suffix(".json") else {
continue;
};
let id = if prefix.is_empty() {
stem.to_string()
} else {
format!("{prefix}/{stem}")
};
let absolute_path = path.canonicalize().unwrap();
registry.insert(
qualify(namespace, &id),
format!(
"{patterns} => Some(include_str!(r#\"{path}\"#)),",
patterns = patterns(namespace, &id),
path = absolute_path.display(),
),
);
}
}
/// Returns a fully-qualified Minecraft resource id.
fn qualify(namespace: &str, id: &str) -> String {
format!("{namespace}:{id}")
}
/// Produces match patterns for an embedded resource.
///
/// Resources in the default namespace additionally accept their bare id,
/// because vanilla treats `foo` and `minecraft:foo` as equivalent.
fn patterns(namespace: &str, id: &str) -> String {
if namespace == DEFAULT_NAMESPACE {
format!(" \"{namespace}:{id}\" | \"{id}\"")
} else {
format!(" \"{namespace}:{id}\"")
}
}
/// Generates one resource lookup function.
fn wrap_fn(name: &str, argument: &str, return_type: &str, registry: &Registry) -> String {
let mut arms = String::new();
for arm in registry.values() {
let _ = writeln!(arms, "{arm}");
}
format!(
"{ATTRS}\
pub fn {name}({argument}: &str) -> {return_type} {{\n\
\x20 match {argument} {{\n\
{arms}\
\x20 _ => None,\n\
\x20 }}\n\
}}\n\n"
)
}
/// Generates a list containing every resource id in a registry.
fn name_list_fn(name: &str, registry: &Registry) -> String {
let mut output = format!(
"#[must_use]\n\
#[allow(clippy::too_many_lines, clippy::large_stack_arrays)]\n\
pub const fn {name}() -> &'static [&'static str] {{\n\
\x20 &[\n"
);
for id in registry.keys() {
let _ = writeln!(output, " \"{id}\",");
}
output.push_str(" ]\n}\n\n");
output
}
/// Generates a static list of strings.
///
/// Unlike [`name_list_fn`], this is used for values that are not resource ids,
/// such as logical embedded datapack names.
fn string_list_fn(name: &str, values: &[String]) -> String {
let mut output = format!(
"#[must_use]\n\
#[allow(clippy::too_many_lines, clippy::large_stack_arrays)]\n\
pub const fn {name}() -> &'static [&'static str] {{\n\
\x20 &[\n"
);
for value in values {
// Debug formatting produces a properly escaped Rust string literal.
let _ = writeln!(output, " {value:?},");
}
output.push_str(" ]\n}\n\n");
output
}