use std::{fs, path::Path}; use heck::ToShoutySnakeCase; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; use serde::Deserialize; use syn::LitStr; /// `rolls` can be a bare float or an object with `type/min/max`. #[derive(Deserialize, Clone, Debug)] #[serde(untagged)] enum RollsStruct { Constant(f32), Provider { #[allow(dead_code)] #[serde(rename = "type")] provider_type: String, #[allow(dead_code)] #[serde(default)] min: f32, #[serde(default)] max: f32, }, } impl RollsStruct { fn min(&self) -> i32 { match self { Self::Constant(v) => v.round() as i32, Self::Provider { min, .. } => min.round() as i32, } } fn max(&self) -> i32 { match self { Self::Constant(v) => v.round() as i32, Self::Provider { max, .. } => max.round() as i32, } } } /// A `set_count` count provider (uniform or constant). #[derive(Deserialize, Clone, Debug)] #[serde(untagged)] enum CountStruct { Constant(f32), Provider { #[serde(rename = "type")] #[allow(dead_code)] provider_type: String, #[serde(default)] min: f32, #[serde(default)] max: f32, }, } impl CountStruct { fn min(&self) -> i32 { match self { Self::Constant(v) => v.round() as i32, Self::Provider { min, .. } => min.round() as i32, } } fn max(&self) -> i32 { match self { Self::Constant(v) => v.round() as i32, Self::Provider { max, .. } => max.round() as i32, } } } #[derive(Deserialize, Clone, Debug)] struct EntryFunctionStruct { function: String, count: Option, } /// A single entry inside a pool. #[derive(Deserialize, Clone, Debug)] struct PoolEntryStruct { #[serde(rename = "type")] entry_type: String, /// Item name (only for `minecraft:item`). name: Option, /// Weight (default 1 if absent). #[serde(default = "default_weight")] weight: i32, /// Optional list of functions. #[serde(default)] functions: Vec, } fn default_weight() -> i32 { 1 } #[derive(Deserialize, Clone, Debug)] struct PoolStruct { entries: Vec, rolls: RollsStruct, } /// Top-level chest loot table JSON. #[derive(Deserialize, Clone, Debug)] struct ChestLootTableJson { pools: Vec, } /// Convert a relative path (e.g. `"trial_chambers/entrance"`) to a Minecraft /// namespaced key (e.g. `"minecraft:chests/trial_chambers/entrance"`). fn path_to_key(relative: &str) -> String { format!("minecraft:chests/{relative}") } /// Convert a file stem path to a valid Rust SCREAMING_SNAKE_CASE identifier prefix. /// e.g. `"trial_chambers/entrance"` -> `"TRIAL_CHAMBERS_ENTRANCE"` fn path_to_ident(relative: &str) -> String { relative.replace('/', "_").to_shouty_snake_case() } /// Read every chest loot JSON from `../assets/loot_table/chests/` (recursively) /// and emit a `pumpkin-data/src/generated/chest_loot.rs` with static constants /// and a `get_chest_loot_table(key) -> Option<&'static ChestLootTable>` function. pub fn build() -> TokenStream { let base = Path::new("../assets/loot_table/chests"); // Collect all JSON files recursively, sorted for deterministic output. let mut files: Vec<(String, ChestLootTableJson)> = collect_json_files(base, base); files.sort_by(|a, b| a.0.cmp(&b.0)); let mut all_tokens = TokenStream::new(); // Emit one set of statics per file let mut table_idents = Vec::new(); let mut table_keys = Vec::new(); for (relative_path, table) in &files { let prefix = path_to_ident(relative_path); let key = path_to_key(relative_path); let table_ident = format_ident!("{}", prefix); let pool_tokens = emit_table(&prefix, table, &mut all_tokens); let pools_ident = format_ident!("{}_POOLS", prefix); all_tokens.extend(quote! { static #pools_ident: &[ChestLootPool] = &[#(#pool_tokens),*]; pub static #table_ident: ChestLootTable = ChestLootTable { pools: #pools_ident }; }); table_idents.push(table_ident); table_keys.push(LitStr::new(&key, Span::call_site())); } // Emit get_chest_loot_table all_tokens.extend(quote! { #[must_use] pub fn get_chest_loot_table(key: &str) -> Option<&'static ChestLootTable> { match key { #(#table_keys => Some(&#table_idents),)* _ => None, } } }); quote! { pub use pumpkin_util::chest_loot_table::*; #all_tokens } } /// Emit static entry arrays and pool literals for one table. /// Returns the list of `ChestLootPool` literals (one per pool). fn emit_table( prefix: &str, table: &ChestLootTableJson, tokens: &mut TokenStream, ) -> Vec { let mut pool_literals = Vec::new(); for (pool_idx, pool) in table.pools.iter().enumerate() { let min_rolls = pool.rolls.min(); let max_rolls = pool.rolls.max(); let mut entry_literals = Vec::new(); let mut empty_weight: i32 = 0; for entry in &pool.entries { match entry.entry_type.as_str() { "minecraft:empty" => { empty_weight += entry.weight; } "minecraft:item" => { let name = match &entry.name { Some(n) => n.clone(), None => continue, // malformed, skip }; let weight = entry.weight; // Extract min/max count from the first `set_count` function. let (min_count, max_count) = entry .functions .iter() .find(|f| f.function == "minecraft:set_count") .and_then(|f| f.count.as_ref()) .map(|c| (c.min(), c.max())) .unwrap_or((1, 1)); let name_lit = LitStr::new(&name, Span::call_site()); entry_literals.push(quote! { ChestLootEntry { item: #name_lit, weight: #weight, min_count: #min_count, max_count: #max_count, } }); } _ => {} // skip loot_table refs, dynamic, etc. } } // Emit the entries static array. let entries_ident = format_ident!("{}_POOL{}_ENTRIES", prefix, pool_idx); tokens.extend(quote! { static #entries_ident: &[ChestLootEntry] = &[#(#entry_literals),*]; }); pool_literals.push(quote! { ChestLootPool { entries: #entries_ident, min_rolls: #min_rolls, max_rolls: #max_rolls, empty_weight: #empty_weight, } }); } pool_literals } /// Recursively collect all `*.json` files under `dir`, returning a vec of /// `(relative_stem_path, parsed_table)`. fn collect_json_files(base: &Path, dir: &Path) -> Vec<(String, ChestLootTableJson)> { let mut result = Vec::new(); let entries = match fs::read_dir(dir) { Ok(e) => e, Err(_) => return result, }; for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { result.extend(collect_json_files(base, &path)); } else if path.extension().and_then(|s| s.to_str()) == Some("json") { // Build relative stem: e.g. "trial_chambers/entrance" let rel = path.strip_prefix(base).unwrap(); let stem = rel.with_extension(""); // Use forward slashes on all platforms. let relative = stem .components() .map(|c| c.as_os_str().to_string_lossy().into_owned()) .collect::>() .join("/"); let raw = match fs::read_to_string(&path) { Ok(s) => s, Err(e) => { eprintln!("Warning: could not read {}: {e}", path.display()); continue; } }; match serde_json::from_str::(&raw) { Ok(table) => result.push((relative, table)), Err(e) => eprintln!("Warning: could not parse {}: {e}", path.display()), } } } result }