282 lines
8.5 KiB
Rust
282 lines
8.5 KiB
Rust
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<CountStruct>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
/// Weight (default 1 if absent).
|
|
#[serde(default = "default_weight")]
|
|
weight: i32,
|
|
/// Optional list of functions.
|
|
#[serde(default)]
|
|
functions: Vec<EntryFunctionStruct>,
|
|
}
|
|
|
|
fn default_weight() -> i32 {
|
|
1
|
|
}
|
|
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct PoolStruct {
|
|
entries: Vec<PoolEntryStruct>,
|
|
rolls: RollsStruct,
|
|
}
|
|
|
|
/// Top-level chest loot table JSON.
|
|
#[derive(Deserialize, Clone, Debug)]
|
|
struct ChestLootTableJson {
|
|
pools: Vec<PoolStruct>,
|
|
}
|
|
|
|
/// 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<TokenStream> {
|
|
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::<Vec<_>>()
|
|
.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::<ChestLootTableJson>(&raw) {
|
|
Ok(table) => result.push((relative, table)),
|
|
Err(e) => eprintln!("Warning: could not parse {}: {e}", path.display()),
|
|
}
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|