feat: add compute command
This commit is contained in:
parent
8813176fdd
commit
12f5687629
@ -107,8 +107,35 @@ pub static MOB_EFFECT_ARGUMENT: ResourceArgument =
|
||||
.map(|value| value as &'static (dyn Any + Send + Sync))
|
||||
});
|
||||
|
||||
pub type DynamicDamageTypeResolver =
|
||||
Box<dyn Fn(&str) -> Option<&'static pumpkin_data::damage::DamageType> + Send + Sync>;
|
||||
|
||||
pub static DYNAMIC_DAMAGE_TYPE_RESOLVER: std::sync::RwLock<Option<DynamicDamageTypeResolver>> =
|
||||
std::sync::RwLock::new(None);
|
||||
pub static DYNAMIC_DAMAGE_TYPE_NAMES: std::sync::RwLock<Vec<String>> =
|
||||
std::sync::RwLock::new(Vec::new());
|
||||
|
||||
pub fn register_dynamic_damage_types(resolver: DynamicDamageTypeResolver, names: Vec<String>) {
|
||||
if let Ok(mut guard) = DYNAMIC_DAMAGE_TYPE_RESOLVER.write() {
|
||||
*guard = Some(resolver);
|
||||
}
|
||||
if let Ok(mut guard) = DYNAMIC_DAMAGE_TYPE_NAMES.write() {
|
||||
*guard = names;
|
||||
}
|
||||
}
|
||||
|
||||
pub static DAMAGE_TYPE_ARGUMENT: ResourceArgument =
|
||||
ResourceArgument(DAMAGE_TYPE_REGISTRY, &|id: Identifier| {
|
||||
if let Ok(guard) = DYNAMIC_DAMAGE_TYPE_RESOLVER.read()
|
||||
&& let Some(resolver) = guard.as_ref()
|
||||
{
|
||||
let id_str = id.to_string();
|
||||
let path_str = id.path();
|
||||
if let Some(dt) = resolver(&id_str).or_else(|| resolver(path_str)) {
|
||||
return Some(dt as &'static (dyn Any + Send + Sync));
|
||||
}
|
||||
}
|
||||
|
||||
let dt = pumpkin_data::damage::DamageType::from_name(id.path())
|
||||
.or_else(|| pumpkin_data::damage::DamageType::from_name(&id.to_string()))?;
|
||||
let idx = dt.id as usize;
|
||||
@ -207,7 +234,13 @@ impl<S: crate::source::CommandSource> ArgumentType<S> for ResourceArgument {
|
||||
let types = DAMAGE_TYPES
|
||||
.iter()
|
||||
.map(|dt| format!("minecraft:{}", dt.message_id));
|
||||
suggestions_builder.filter_and_suggest_iter(types).build()
|
||||
let extra = DYNAMIC_DAMAGE_TYPE_NAMES
|
||||
.read()
|
||||
.map(|g| g.clone())
|
||||
.unwrap_or_default();
|
||||
suggestions_builder
|
||||
.filter_and_suggest_iter(types.chain(extra))
|
||||
.build()
|
||||
} else {
|
||||
Suggestions::empty()
|
||||
}
|
||||
|
||||
@ -75,3 +75,144 @@ pub struct LootTable {
|
||||
pub type ChestLootEntry = LootEntry;
|
||||
pub type ChestLootPool = LootPool;
|
||||
pub type ChestLootTable = LootTable;
|
||||
|
||||
/// Conditions required for an entry or pool to be eligible for dynamic loot generation.
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub enum DynamicLootCondition {
|
||||
#[default]
|
||||
None,
|
||||
SilkTouch,
|
||||
NoSilkTouch,
|
||||
Shears,
|
||||
SilkTouchOrShears,
|
||||
NoSilkTouchOrShears,
|
||||
SurvivesExplosion,
|
||||
KilledByPlayer,
|
||||
RandomChance {
|
||||
chance: f32,
|
||||
},
|
||||
RandomChanceWithEnchantedBonus {
|
||||
unenchanted_chance: f32,
|
||||
enchanted_chance_base: f32,
|
||||
enchanted_chance_per_level_above_first: f32,
|
||||
},
|
||||
TableBonus {
|
||||
chances: Box<[f32]>,
|
||||
},
|
||||
AllOf(Vec<Self>),
|
||||
AnyOf(Vec<Self>),
|
||||
Inverted(Box<Self>),
|
||||
EntityOnFire,
|
||||
WeatherCheck {
|
||||
raining: Option<bool>,
|
||||
thundering: Option<bool>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<LootCondition> for DynamicLootCondition {
|
||||
fn from(cond: LootCondition) -> Self {
|
||||
match cond {
|
||||
LootCondition::None => Self::None,
|
||||
LootCondition::SilkTouch => Self::SilkTouch,
|
||||
LootCondition::NoSilkTouch => Self::NoSilkTouch,
|
||||
LootCondition::Shears => Self::Shears,
|
||||
LootCondition::SilkTouchOrShears => Self::SilkTouchOrShears,
|
||||
LootCondition::NoSilkTouchOrShears => Self::NoSilkTouchOrShears,
|
||||
LootCondition::SurvivesExplosion => Self::SurvivesExplosion,
|
||||
LootCondition::KilledByPlayer => Self::KilledByPlayer,
|
||||
LootCondition::RandomChance { chance } => Self::RandomChance { chance },
|
||||
LootCondition::RandomChanceWithEnchantedBonus {
|
||||
unenchanted_chance,
|
||||
enchanted_chance_base,
|
||||
enchanted_chance_per_level_above_first,
|
||||
} => Self::RandomChanceWithEnchantedBonus {
|
||||
unenchanted_chance,
|
||||
enchanted_chance_base,
|
||||
enchanted_chance_per_level_above_first,
|
||||
},
|
||||
LootCondition::TableBonus { chances } => Self::TableBonus {
|
||||
chances: chances.to_vec().into_boxed_slice(),
|
||||
},
|
||||
LootCondition::AllOf(conditions) => {
|
||||
Self::AllOf(conditions.iter().copied().map(Self::from).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single item entry inside a dynamic loot pool.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DynamicLootEntry {
|
||||
/// Registry name of the item (e.g. `"minecraft:diamond"`).
|
||||
pub item: String,
|
||||
/// Relative probability weight; higher values are more likely.
|
||||
pub weight: i32,
|
||||
/// Minimum stack size (inclusive).
|
||||
pub min_count: i32,
|
||||
/// Maximum stack size (inclusive).
|
||||
pub max_count: i32,
|
||||
/// Condition required for this entry to be eligible.
|
||||
pub condition: DynamicLootCondition,
|
||||
/// Bonus formula to apply with fortune / looting (if any).
|
||||
pub bonus_formula: Option<LootBonusFormula>,
|
||||
}
|
||||
|
||||
impl From<LootEntry> for DynamicLootEntry {
|
||||
fn from(e: LootEntry) -> Self {
|
||||
Self {
|
||||
item: e.item.to_string(),
|
||||
weight: e.weight,
|
||||
min_count: e.min_count,
|
||||
max_count: e.max_count,
|
||||
condition: DynamicLootCondition::from(e.condition),
|
||||
bonus_formula: e.bonus_formula,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One roll pool inside a dynamic loot table.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DynamicLootPool {
|
||||
/// Item entries eligible for selection each roll.
|
||||
pub entries: Vec<DynamicLootEntry>,
|
||||
/// Minimum number of roll attempts (inclusive).
|
||||
pub min_rolls: i32,
|
||||
/// Maximum number of roll attempts (inclusive).
|
||||
pub max_rolls: i32,
|
||||
/// Weight of the implicit "empty" (no item) outcome per roll.
|
||||
pub empty_weight: i32,
|
||||
/// Condition required for this entire pool to run.
|
||||
pub condition: DynamicLootCondition,
|
||||
}
|
||||
|
||||
impl From<LootPool> for DynamicLootPool {
|
||||
fn from(p: LootPool) -> Self {
|
||||
Self {
|
||||
entries: p
|
||||
.entries
|
||||
.iter()
|
||||
.copied()
|
||||
.map(DynamicLootEntry::from)
|
||||
.collect(),
|
||||
min_rolls: p.min_rolls,
|
||||
max_rolls: p.max_rolls,
|
||||
empty_weight: p.empty_weight,
|
||||
condition: DynamicLootCondition::from(p.condition),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete dynamic loot table consisting of one or more pools.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DynamicLootTable {
|
||||
/// All pools to roll when generating loot for this table.
|
||||
pub pools: Vec<DynamicLootPool>,
|
||||
}
|
||||
|
||||
impl From<&LootTable> for DynamicLootTable {
|
||||
fn from(t: &LootTable) -> Self {
|
||||
Self {
|
||||
pools: t.pools.iter().copied().map(DynamicLootPool::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1055,11 +1055,11 @@ impl TrialSpawner {
|
||||
}
|
||||
|
||||
if let Some(equip) = equipment_loot_table
|
||||
&& let Some(loot_table) = pumpkin_data::loot_table::get_loot_table(&equip)
|
||||
&& let Some(loot_table) = world.get_loot_table(&equip)
|
||||
&& let Some(living) = entity.get_living_entity()
|
||||
{
|
||||
let seed = rand::random::<i64>();
|
||||
let items = crate::world::loot::generate_loot(loot_table, seed);
|
||||
let items = loot_table.generate_loot(seed);
|
||||
let mut equipment = living
|
||||
.entity_equipment
|
||||
.lock()
|
||||
@ -1126,10 +1126,10 @@ impl TrialSpawner {
|
||||
) {
|
||||
let mut dropped_any = false;
|
||||
if let Some(key) = ejecting_loot_table
|
||||
&& let Some(loot_table) = pumpkin_data::loot_table::get_loot_table(key)
|
||||
&& let Some(loot_table) = world.get_loot_table(key)
|
||||
{
|
||||
let seed = rand::random::<i64>();
|
||||
let items = crate::world::loot::generate_loot(loot_table, seed);
|
||||
let items = loot_table.generate_loot(seed);
|
||||
for stack in items {
|
||||
world.drop_stack(&spawner_pos, stack);
|
||||
dropped_any = true;
|
||||
|
||||
@ -460,9 +460,9 @@ pub fn drop_loot(
|
||||
params: &LootContextParameters,
|
||||
) {
|
||||
let key = format!("minecraft:blocks/{}", block.name);
|
||||
if let Some(loot_table) = pumpkin_data::loot_table::get_loot_table(&key) {
|
||||
if let Some(loot_table) = world.get_loot_table(&key) {
|
||||
let seed: i64 = rand::random();
|
||||
let items = crate::world::loot::generate_loot_with_context(loot_table, seed, params);
|
||||
let items = crate::world::loot::generate_loot_from_handle(&loot_table, seed, params);
|
||||
if !items.is_empty() {
|
||||
let mut event = crate::plugin::block::block_drop_item::BlockDropItemEvent {
|
||||
block_pos: *pos,
|
||||
|
||||
1431
crates/pumpkin/src/command/commands/compute.rs
Normal file
1431
crates/pumpkin/src/command/commands/compute.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -27,6 +27,8 @@ use crate::command::errors::command_syntax_error::CommandSyntaxError;
|
||||
use crate::command::errors::error_types::CommandErrorType;
|
||||
use crate::command::node::dispatcher::CommandDispatcher;
|
||||
use crate::command::node::{CommandExecutor, CommandExecutorResult};
|
||||
use crate::command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult};
|
||||
use crate::command::suggestion::suggestions::SuggestionsBuilder;
|
||||
use crate::entity::EntityBase;
|
||||
use crate::world::loot::LootContextParameters;
|
||||
|
||||
@ -39,6 +41,23 @@ static ERROR_INVALID_LOOT_TABLE: CommandErrorType<1> = CommandErrorType::new(
|
||||
"Loot table '%s' not found",
|
||||
);
|
||||
|
||||
struct LootTableSuggestionProvider;
|
||||
|
||||
impl SuggestionProvider for LootTableSuggestionProvider {
|
||||
fn suggest(
|
||||
&self,
|
||||
context: &CommandContext,
|
||||
mut builder: SuggestionsBuilder,
|
||||
) -> SuggestionProviderResult {
|
||||
let server = context.server();
|
||||
let custom_names = server.datapack_manager.get_loot_table_names();
|
||||
for name in custom_names {
|
||||
builder = builder.suggest(name);
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
static ERROR_NO_HELD_ITEMS: CommandErrorType<1> = CommandErrorType::new(
|
||||
translation::java::COMMANDS_DROP_NO_HELD_ITEMS,
|
||||
translation::java::COMMANDS_DROP_NO_HELD_ITEMS,
|
||||
@ -345,8 +364,11 @@ impl CommandExecutor for LootExecutor {
|
||||
ToolSource::OffHand => get_hand_item(context, false)?,
|
||||
};
|
||||
|
||||
let loot_table =
|
||||
pumpkin_data::loot_table::get_loot_table(&key).ok_or_else(|| {
|
||||
let loot_table = context
|
||||
.server()
|
||||
.datapack_manager
|
||||
.get_loot_table(&key)
|
||||
.ok_or_else(|| {
|
||||
ERROR_INVALID_LOOT_TABLE
|
||||
.create_without_context(TextComponent::text(loot_table_str.to_string()))
|
||||
})?;
|
||||
@ -361,7 +383,7 @@ impl CommandExecutor for LootExecutor {
|
||||
..Default::default()
|
||||
};
|
||||
let seed: i64 = rand::random();
|
||||
drops = crate::world::loot::generate_loot_with_context(loot_table, seed, ¶ms);
|
||||
drops = crate::world::loot::generate_loot_from_handle(&loot_table, seed, ¶ms);
|
||||
}
|
||||
Source::Loot => {
|
||||
let loot_table_str = StringArgumentType::get(context, "loot_table")?;
|
||||
@ -371,8 +393,11 @@ impl CommandExecutor for LootExecutor {
|
||||
format!("minecraft:{loot_table_str}")
|
||||
};
|
||||
|
||||
let loot_table =
|
||||
pumpkin_data::loot_table::get_loot_table(&key).ok_or_else(|| {
|
||||
let loot_table = context
|
||||
.server()
|
||||
.datapack_manager
|
||||
.get_loot_table(&key)
|
||||
.ok_or_else(|| {
|
||||
ERROR_INVALID_LOOT_TABLE
|
||||
.create_without_context(TextComponent::text(loot_table_str.to_string()))
|
||||
})?;
|
||||
@ -382,7 +407,7 @@ impl CommandExecutor for LootExecutor {
|
||||
..Default::default()
|
||||
};
|
||||
let seed: i64 = rand::random();
|
||||
drops = crate::world::loot::generate_loot_with_context(loot_table, seed, ¶ms);
|
||||
drops = crate::world::loot::generate_loot_from_handle(&loot_table, seed, ¶ms);
|
||||
}
|
||||
Source::Kill => {
|
||||
let target_entities = EntityArgumentType::get_entities(context, "target_entity")?;
|
||||
@ -402,10 +427,13 @@ impl CommandExecutor for LootExecutor {
|
||||
for entity in &target_entities {
|
||||
let resource_name = entity.get_entity().entity_type.resource_name;
|
||||
let key = format!("minecraft:entities/{resource_name}");
|
||||
if let Some(loot_table) = pumpkin_data::loot_table::get_loot_table(&key) {
|
||||
if let Some(loot_table) = context.server().datapack_manager.get_loot_table(&key)
|
||||
{
|
||||
let seed: i64 = rand::random();
|
||||
drops.extend(crate::world::loot::generate_loot_with_context(
|
||||
loot_table, seed, ¶ms,
|
||||
drops.extend(crate::world::loot::generate_loot_from_handle(
|
||||
&loot_table,
|
||||
seed,
|
||||
¶ms,
|
||||
));
|
||||
last_key = Some(key);
|
||||
}
|
||||
@ -427,8 +455,11 @@ impl CommandExecutor for LootExecutor {
|
||||
let block = world.get_block(&pos);
|
||||
let key = format!("minecraft:blocks/{}", block.name);
|
||||
|
||||
let loot_table =
|
||||
pumpkin_data::loot_table::get_loot_table(&key).ok_or_else(|| {
|
||||
let loot_table = context
|
||||
.server()
|
||||
.datapack_manager
|
||||
.get_loot_table(&key)
|
||||
.ok_or_else(|| {
|
||||
ERROR_NO_BLOCK_LOOT_TABLE
|
||||
.create_without_context(TextComponent::text(block.name.to_string()))
|
||||
})?;
|
||||
@ -451,7 +482,7 @@ impl CommandExecutor for LootExecutor {
|
||||
..Default::default()
|
||||
};
|
||||
let seed: i64 = rand::random();
|
||||
drops = crate::world::loot::generate_loot_with_context(loot_table, seed, ¶ms);
|
||||
drops = crate::world::loot::generate_loot_from_handle(&loot_table, seed, ¶ms);
|
||||
table_id_for_callback = Some(key);
|
||||
}
|
||||
}
|
||||
@ -589,7 +620,9 @@ impl CommandExecutor for LootExecutor {
|
||||
fn add_sources(target: Target) -> Vec<LiteralArgumentBuilder> {
|
||||
vec![
|
||||
literal("fish").then(
|
||||
argument("loot_table", StringArgumentType::SingleWord).then(
|
||||
argument("loot_table", StringArgumentType::SingleWord)
|
||||
.suggests(LootTableSuggestionProvider)
|
||||
.then(
|
||||
argument("fish_pos", BlockPosArgumentType)
|
||||
.executes(LootExecutor {
|
||||
target,
|
||||
@ -620,7 +653,9 @@ fn add_sources(target: Target) -> Vec<LiteralArgumentBuilder> {
|
||||
),
|
||||
),
|
||||
literal("loot").then(
|
||||
argument("loot_table", StringArgumentType::SingleWord).executes(LootExecutor {
|
||||
argument("loot_table", StringArgumentType::SingleWord)
|
||||
.suggests(LootTableSuggestionProvider)
|
||||
.executes(LootExecutor {
|
||||
target,
|
||||
source: Source::Loot,
|
||||
}),
|
||||
|
||||
@ -14,6 +14,7 @@ mod banlist;
|
||||
mod bossbar;
|
||||
mod clear;
|
||||
mod clone;
|
||||
mod compute;
|
||||
mod damage;
|
||||
mod data;
|
||||
mod datapack;
|
||||
@ -174,6 +175,7 @@ pub fn default_dispatcher(
|
||||
effect::register(&mut dispatcher, registry);
|
||||
summon::register(&mut dispatcher, registry);
|
||||
swing::register(&mut dispatcher, registry);
|
||||
compute::register(&mut dispatcher, registry);
|
||||
damage::register(&mut dispatcher, registry);
|
||||
rotate::register(&mut dispatcher, registry);
|
||||
tellraw::register(&mut dispatcher, registry);
|
||||
|
||||
136
crates/pumpkin/src/data/datapack/context_provider_loader.rs
Normal file
136
crates/pumpkin/src/data/datapack/context_provider_loader.rs
Normal file
@ -0,0 +1,136 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_nbt::tag::NbtTag;
|
||||
use serde_json::Value;
|
||||
use tracing::warn;
|
||||
|
||||
pub type ContextProviderRegistry = HashMap<String, NbtTag>;
|
||||
|
||||
#[must_use]
|
||||
pub fn json_value_to_nbt(value: &Value) -> NbtTag {
|
||||
match value {
|
||||
Value::Null => NbtTag::String(String::new().into()),
|
||||
Value::Bool(val) => NbtTag::Byte(i8::from(*val)),
|
||||
Value::Number(val) => val.as_i64().map_or_else(
|
||||
|| {
|
||||
val.as_u64().map_or_else(
|
||||
|| NbtTag::Double(val.as_f64().unwrap_or_default()),
|
||||
|u_val| {
|
||||
i32::try_from(u_val).map_or_else(
|
||||
|_| {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
i64::try_from(u_val)
|
||||
.map_or_else(|_| NbtTag::Double(u_val as f64), NbtTag::Long)
|
||||
},
|
||||
NbtTag::Int,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
|i_val| i32::try_from(i_val).map_or(NbtTag::Long(i_val), NbtTag::Int),
|
||||
),
|
||||
Value::String(val) => NbtTag::String(val.clone().into()),
|
||||
Value::Array(values) => NbtTag::List(values.iter().map(json_value_to_nbt).collect()),
|
||||
Value::Object(values) => {
|
||||
let mut compound = NbtCompound::new();
|
||||
for (name, val) in values {
|
||||
compound.put(name, json_value_to_nbt(val));
|
||||
}
|
||||
NbtTag::Compound(compound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_context_providers_from_dir(
|
||||
namespace: &str,
|
||||
provider_dir: &Path,
|
||||
registry: &mut ContextProviderRegistry,
|
||||
) -> usize {
|
||||
if !provider_dir.is_dir() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let before = registry.len();
|
||||
load_context_providers_recursive(namespace, provider_dir, provider_dir, registry);
|
||||
registry.len() - before
|
||||
}
|
||||
|
||||
fn load_context_providers_recursive(
|
||||
namespace: &str,
|
||||
base_dir: &Path,
|
||||
current_dir: &Path,
|
||||
registry: &mut ContextProviderRegistry,
|
||||
) {
|
||||
let Ok(entries) = fs::read_dir(current_dir) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
load_context_providers_recursive(namespace, base_dir, &path, registry);
|
||||
} else if path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
|
||||
&& let Ok(rel_path) = path.strip_prefix(base_dir)
|
||||
{
|
||||
let mut stem_path = rel_path.to_string_lossy().to_string();
|
||||
|
||||
if let Some(stem) = stem_path.strip_suffix(".json") {
|
||||
stem_path = stem.to_string();
|
||||
}
|
||||
|
||||
let stem_path = stem_path.replace('\\', "/");
|
||||
let provider_id = format!("{namespace}:{stem_path}");
|
||||
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(content) => match serde_json::from_str::<Value>(&content) {
|
||||
Ok(val) => {
|
||||
let tag = json_value_to_nbt(&val);
|
||||
registry.insert(provider_id, tag);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to parse context provider '{provider_id}' from '{}': {e}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to read context provider file '{}': {e}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn json_value_to_nbt_works() {
|
||||
let json: Value = serde_json::json!({
|
||||
"type": "minecraft:add",
|
||||
"left": 10,
|
||||
"right": 20.5,
|
||||
"items": ["a", "b"]
|
||||
});
|
||||
|
||||
let tag = json_value_to_nbt(&json);
|
||||
if let NbtTag::Compound(compound) = tag {
|
||||
assert_eq!(compound.get_string("type"), Some("minecraft:add"));
|
||||
assert_eq!(compound.get_int("left"), Some(10));
|
||||
assert_eq!(compound.get_double("right"), Some(20.5));
|
||||
} else {
|
||||
panic!("Expected compound");
|
||||
}
|
||||
}
|
||||
}
|
||||
417
crates/pumpkin/src/data/datapack/damage_type_loader.rs
Normal file
417
crates/pumpkin/src/data/datapack/damage_type_loader.rs
Normal file
@ -0,0 +1,417 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use pumpkin_data::damage::{DamageEffects, DamageScaling, DamageType, DeathMessageType};
|
||||
use pumpkin_data::registry::RegistryEntryData;
|
||||
use pumpkin_nbt::{Nbt, NbtCompound};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DamageScalingWrapper {
|
||||
Never,
|
||||
WhenCausedByLivingNonPlayer,
|
||||
Always,
|
||||
}
|
||||
|
||||
impl From<DamageScalingWrapper> for DamageScaling {
|
||||
fn from(wrapper: DamageScalingWrapper) -> Self {
|
||||
match wrapper {
|
||||
DamageScalingWrapper::Never => Self::Never,
|
||||
DamageScalingWrapper::WhenCausedByLivingNonPlayer => Self::WhenCausedByLivingNonPlayer,
|
||||
DamageScalingWrapper::Always => Self::Always,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DamageScalingWrapper {
|
||||
#[must_use]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Never => "never",
|
||||
Self::WhenCausedByLivingNonPlayer => "when_caused_by_living_non_player",
|
||||
Self::Always => "always",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DamageEffectsWrapper {
|
||||
Hurt,
|
||||
Thorns,
|
||||
Drowning,
|
||||
Burning,
|
||||
Poking,
|
||||
Freezing,
|
||||
}
|
||||
|
||||
impl From<DamageEffectsWrapper> for DamageEffects {
|
||||
fn from(wrapper: DamageEffectsWrapper) -> Self {
|
||||
match wrapper {
|
||||
DamageEffectsWrapper::Hurt => Self::Hurt,
|
||||
DamageEffectsWrapper::Thorns => Self::Thorns,
|
||||
DamageEffectsWrapper::Drowning => Self::Drowning,
|
||||
DamageEffectsWrapper::Burning => Self::Burning,
|
||||
DamageEffectsWrapper::Poking => Self::Poking,
|
||||
DamageEffectsWrapper::Freezing => Self::Freezing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DamageEffectsWrapper {
|
||||
#[must_use]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Hurt => "hurt",
|
||||
Self::Thorns => "thorns",
|
||||
Self::Drowning => "drowning",
|
||||
Self::Burning => "burning",
|
||||
Self::Poking => "poking",
|
||||
Self::Freezing => "freezing",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DeathMessageTypeWrapper {
|
||||
Default,
|
||||
FallVariants,
|
||||
IntentionalGameDesign,
|
||||
}
|
||||
|
||||
impl From<DeathMessageTypeWrapper> for DeathMessageType {
|
||||
fn from(wrapper: DeathMessageTypeWrapper) -> Self {
|
||||
match wrapper {
|
||||
DeathMessageTypeWrapper::Default => Self::Default,
|
||||
DeathMessageTypeWrapper::FallVariants => Self::FallVariants,
|
||||
DeathMessageTypeWrapper::IntentionalGameDesign => Self::IntentionalGameDesign,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DeathMessageTypeWrapper {
|
||||
#[must_use]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Default => "default",
|
||||
Self::FallVariants => "fall_variants",
|
||||
Self::IntentionalGameDesign => "intentional_game_design",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A parsed damage type definition from a datapack JSON file.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DamageTypeDefinition {
|
||||
pub message_id: String,
|
||||
pub scaling: DamageScalingWrapper,
|
||||
#[serde(default)]
|
||||
pub exhaustion: f32,
|
||||
#[serde(default)]
|
||||
pub effects: Option<DamageEffectsWrapper>,
|
||||
#[serde(default)]
|
||||
pub death_message_type: Option<DeathMessageTypeWrapper>,
|
||||
}
|
||||
|
||||
/// A registered damage type in the runtime registry.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DamageTypeEntry {
|
||||
pub id: String,
|
||||
pub numeric_id: u8,
|
||||
pub definition: DamageTypeDefinition,
|
||||
pub damage_type: DamageType,
|
||||
pub nbt_data: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl DamageTypeEntry {
|
||||
#[must_use]
|
||||
pub fn to_registry_entry(&self) -> RegistryEntryData {
|
||||
RegistryEntryData {
|
||||
entry_id: self.id.clone(),
|
||||
data: Some(self.nbt_data.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type DamageTypeRegistry = HashMap<String, DamageTypeEntry>;
|
||||
|
||||
/// Encodes a `DamageTypeDefinition` into a `pumpkin_data::damage::DamageType`.
|
||||
#[must_use]
|
||||
pub fn to_damage_type(numeric_id: u8, def: &DamageTypeDefinition) -> DamageType {
|
||||
DamageType {
|
||||
death_message_type: def
|
||||
.death_message_type
|
||||
.map_or(DeathMessageType::Default, Into::into),
|
||||
exhaustion: def.exhaustion,
|
||||
effects: def.effects.map(Into::into),
|
||||
message_id: Box::leak(def.message_id.clone().into_boxed_str()),
|
||||
scaling: def.scaling.into(),
|
||||
id: numeric_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes the damage type definition into binary NBT payload.
|
||||
#[must_use]
|
||||
pub fn to_registry_nbt_bytes(def: &DamageTypeDefinition) -> Box<[u8]> {
|
||||
let mut nbt = NbtCompound::new();
|
||||
nbt.put_string("message_id", def.message_id.clone());
|
||||
nbt.put_string("scaling", def.scaling.as_str().to_string());
|
||||
nbt.put_double("exhaustion", f64::from(def.exhaustion));
|
||||
|
||||
if let Some(effects) = def.effects {
|
||||
nbt.put_string("effects", effects.as_str().to_string());
|
||||
}
|
||||
|
||||
if let Some(dmt) = def.death_message_type
|
||||
&& dmt != DeathMessageTypeWrapper::Default
|
||||
{
|
||||
nbt.put_string("death_message_type", dmt.as_str().to_string());
|
||||
}
|
||||
|
||||
Nbt::from(nbt).write_unnamed().to_vec().into_boxed_slice()
|
||||
}
|
||||
|
||||
/// Encodes the damage type definition into a `RegistryEntryData` with binary NBT payload.
|
||||
#[must_use]
|
||||
pub fn to_registry_entry(entry_id: String, def: &DamageTypeDefinition) -> RegistryEntryData {
|
||||
RegistryEntryData {
|
||||
entry_id,
|
||||
data: Some(to_registry_nbt_bytes(def)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads all damage type JSON definitions from a directory into the definition registry.
|
||||
pub fn load_damage_types_from_dir<S: std::hash::BuildHasher>(
|
||||
namespace: &str,
|
||||
dir: &Path,
|
||||
registry: &mut HashMap<String, DamageTypeDefinition, S>,
|
||||
) -> usize {
|
||||
if !dir.is_dir() {
|
||||
return 0;
|
||||
}
|
||||
let before = registry.len();
|
||||
load_damage_types_recursive(namespace, dir, dir, registry);
|
||||
registry.len() - before
|
||||
}
|
||||
|
||||
fn load_damage_types_recursive<S: std::hash::BuildHasher>(
|
||||
namespace: &str,
|
||||
base_dir: &Path,
|
||||
current_dir: &Path,
|
||||
registry: &mut HashMap<String, DamageTypeDefinition, S>,
|
||||
) {
|
||||
let Ok(entries) = fs::read_dir(current_dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
load_damage_types_recursive(namespace, base_dir, &path, registry);
|
||||
} else if path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
|
||||
&& let Ok(rel_path) = path.strip_prefix(base_dir)
|
||||
{
|
||||
let mut stem_path = rel_path.to_string_lossy().to_string();
|
||||
if let Some(stem) = stem_path.strip_suffix(".json") {
|
||||
stem_path = stem.to_string();
|
||||
}
|
||||
let stem_path = stem_path.replace('\\', "/");
|
||||
let damage_type_id = format!("{namespace}:{stem_path}");
|
||||
|
||||
if let Ok(content) = fs::read_to_string(&path)
|
||||
&& let Ok(definition) = serde_json::from_str::<DamageTypeDefinition>(&content)
|
||||
{
|
||||
registry.insert(damage_type_id, definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_full_damage_type_json() {
|
||||
let json = r#"{
|
||||
"message_id": "bleed",
|
||||
"scaling": "when_caused_by_living_non_player",
|
||||
"exhaustion": 0.25,
|
||||
"effects": "burning",
|
||||
"death_message_type": "intentional_game_design"
|
||||
}"#;
|
||||
|
||||
let def: DamageTypeDefinition = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(def.message_id, "bleed");
|
||||
assert_eq!(
|
||||
def.scaling,
|
||||
DamageScalingWrapper::WhenCausedByLivingNonPlayer
|
||||
);
|
||||
assert!((def.exhaustion - 0.25).abs() < 1e-6);
|
||||
assert_eq!(def.effects, Some(DamageEffectsWrapper::Burning));
|
||||
assert_eq!(
|
||||
def.death_message_type,
|
||||
Some(DeathMessageTypeWrapper::IntentionalGameDesign)
|
||||
);
|
||||
|
||||
let dt = to_damage_type(52, &def);
|
||||
assert_eq!(dt.id, 52);
|
||||
assert_eq!(dt.message_id, "bleed");
|
||||
assert_eq!(dt.scaling, DamageScaling::WhenCausedByLivingNonPlayer);
|
||||
assert_eq!(dt.effects, Some(DamageEffects::Burning));
|
||||
assert_eq!(
|
||||
dt.death_message_type,
|
||||
DeathMessageType::IntentionalGameDesign
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_minimal_damage_type_json() {
|
||||
let json = r#"{
|
||||
"message_id": "simple",
|
||||
"scaling": "never"
|
||||
}"#;
|
||||
|
||||
let def: DamageTypeDefinition = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(def.message_id, "simple");
|
||||
assert_eq!(def.scaling, DamageScalingWrapper::Never);
|
||||
assert_eq!(def.exhaustion, 0.0);
|
||||
assert_eq!(def.effects, None);
|
||||
assert_eq!(def.death_message_type, None);
|
||||
|
||||
let dt = to_damage_type(51, &def);
|
||||
assert_eq!(dt.id, 51);
|
||||
assert_eq!(dt.message_id, "simple");
|
||||
assert_eq!(dt.scaling, DamageScaling::Never);
|
||||
assert_eq!(dt.effects, None);
|
||||
assert_eq!(dt.death_message_type, DeathMessageType::Default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_registry_entry_encodes_valid_nbt() {
|
||||
let def = DamageTypeDefinition {
|
||||
message_id: "frost".to_string(),
|
||||
scaling: DamageScalingWrapper::Always,
|
||||
exhaustion: 0.1,
|
||||
effects: Some(DamageEffectsWrapper::Freezing),
|
||||
death_message_type: Some(DeathMessageTypeWrapper::FallVariants),
|
||||
};
|
||||
|
||||
let entry = to_registry_entry("custom:frost".to_string(), &def);
|
||||
assert_eq!(entry.entry_id, "custom:frost");
|
||||
assert!(entry.data.is_some());
|
||||
|
||||
let data = entry.data.unwrap();
|
||||
let mut cursor = std::io::Cursor::new(&data[..]);
|
||||
let mut reader = pumpkin_nbt::deserializer::NbtReadHelperJava::new(
|
||||
pumpkin_nbt::deserializer::NbtStreamReader(&mut cursor),
|
||||
);
|
||||
let nbt = Nbt::read_unnamed(&mut reader).unwrap().root_tag;
|
||||
assert_eq!(nbt.get_string("message_id"), Some("frost"));
|
||||
assert_eq!(nbt.get_string("scaling"), Some("always"));
|
||||
assert_eq!(nbt.get_string("effects"), Some("freezing"));
|
||||
assert_eq!(nbt.get_string("death_message_type"), Some("fall_variants"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_damage_types_from_directory_recursively() {
|
||||
let temp_dir =
|
||||
std::env::temp_dir().join(format!("pumpkin_test_dt_{}", uuid::Uuid::new_v4()));
|
||||
let sub_dir = temp_dir.join("sub");
|
||||
fs::create_dir_all(&sub_dir).unwrap();
|
||||
|
||||
let dt1_json = r#"{
|
||||
"message_id": "bleeding",
|
||||
"scaling": "never",
|
||||
"exhaustion": 0.5
|
||||
}"#;
|
||||
fs::write(temp_dir.join("bleed.json"), dt1_json).unwrap();
|
||||
|
||||
let dt2_json = r#"{
|
||||
"message_id": "radiation",
|
||||
"scaling": "always",
|
||||
"exhaustion": 1.0,
|
||||
"effects": "poking"
|
||||
}"#;
|
||||
fs::write(sub_dir.join("rad.json"), dt2_json).unwrap();
|
||||
|
||||
let mut registry = HashMap::new();
|
||||
let count = load_damage_types_from_dir("custom", &temp_dir, &mut registry);
|
||||
assert_eq!(count, 2);
|
||||
assert!(registry.contains_key("custom:bleed"));
|
||||
assert!(registry.contains_key("custom:sub/rad"));
|
||||
|
||||
let rad_def = ®istry["custom:sub/rad"];
|
||||
assert_eq!(rad_def.message_id, "radiation");
|
||||
assert_eq!(rad_def.scaling, DamageScalingWrapper::Always);
|
||||
assert_eq!(rad_def.effects, Some(DamageEffectsWrapper::Poking));
|
||||
|
||||
let _ = fs::remove_dir_all(temp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_damage_type_entries_preserves_vanilla_and_appends_custom() {
|
||||
use crate::data::datapack::merge_damage_type_entries;
|
||||
|
||||
let vanilla = vec![
|
||||
RegistryEntryData {
|
||||
entry_id: "arrow".to_string(),
|
||||
data: Some(vec![1, 2, 3].into_boxed_slice()),
|
||||
},
|
||||
RegistryEntryData {
|
||||
entry_id: "cactus".to_string(),
|
||||
data: Some(vec![4, 5, 6].into_boxed_slice()),
|
||||
},
|
||||
];
|
||||
|
||||
let mut custom = HashMap::new();
|
||||
let bleed_def = DamageTypeDefinition {
|
||||
message_id: "bleed".to_string(),
|
||||
scaling: DamageScalingWrapper::Never,
|
||||
exhaustion: 0.1,
|
||||
effects: None,
|
||||
death_message_type: None,
|
||||
};
|
||||
let bleed_entry = DamageTypeEntry {
|
||||
id: "custom:bleed".to_string(),
|
||||
numeric_id: 51,
|
||||
definition: bleed_def.clone(),
|
||||
damage_type: to_damage_type(51, &bleed_def),
|
||||
nbt_data: vec![7, 8, 9].into_boxed_slice(),
|
||||
};
|
||||
custom.insert("custom:bleed".to_string(), bleed_entry);
|
||||
|
||||
// Overriding vanilla "arrow"
|
||||
let arrow_def = DamageTypeDefinition {
|
||||
message_id: "custom_arrow".to_string(),
|
||||
scaling: DamageScalingWrapper::Always,
|
||||
exhaustion: 0.5,
|
||||
effects: None,
|
||||
death_message_type: None,
|
||||
};
|
||||
let arrow_override = DamageTypeEntry {
|
||||
id: "minecraft:arrow".to_string(),
|
||||
numeric_id: 0,
|
||||
definition: arrow_def.clone(),
|
||||
damage_type: to_damage_type(0, &arrow_def),
|
||||
nbt_data: vec![99, 99].into_boxed_slice(),
|
||||
};
|
||||
custom.insert("minecraft:arrow".to_string(), arrow_override);
|
||||
|
||||
let merged = merge_damage_type_entries(&vanilla, &custom);
|
||||
assert_eq!(merged.len(), 3);
|
||||
// Arrow replaced in place at index 0
|
||||
assert_eq!(merged[0].entry_id, "arrow");
|
||||
assert_eq!(merged[0].data.as_deref(), Some(&[99, 99][..]));
|
||||
// Cactus preserved at index 1
|
||||
assert_eq!(merged[1].entry_id, "cactus");
|
||||
assert_eq!(merged[1].data.as_deref(), Some(&[4, 5, 6][..]));
|
||||
// Custom bleed appended at index 2
|
||||
assert_eq!(merged[2].entry_id, "custom:bleed");
|
||||
assert_eq!(merged[2].data.as_deref(), Some(&[7, 8, 9][..]));
|
||||
}
|
||||
}
|
||||
503
crates/pumpkin/src/data/datapack/loot_table_loader.rs
Normal file
503
crates/pumpkin/src/data/datapack/loot_table_loader.rs
Normal file
@ -0,0 +1,503 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use pumpkin_util::loot_table::{
|
||||
DynamicLootCondition, DynamicLootEntry, DynamicLootPool, DynamicLootTable, LootBonusFormula,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Parses an entire loot table JSON string into a [`DynamicLootTable`].
|
||||
#[must_use]
|
||||
pub fn parse_loot_table(json_content: &str) -> Option<DynamicLootTable> {
|
||||
let root: Value = serde_json::from_str(json_content).ok()?;
|
||||
let pools_val = root.get("pools").and_then(Value::as_array)?;
|
||||
|
||||
let pools = pools_val.iter().map(parse_pool).collect();
|
||||
|
||||
Some(DynamicLootTable { pools })
|
||||
}
|
||||
|
||||
fn parse_pool(val: &Value) -> DynamicLootPool {
|
||||
let (min_rolls, max_rolls) = val.get("rolls").map_or((1, 1), parse_rolls);
|
||||
|
||||
let mut entries = Vec::new();
|
||||
let mut empty_weight = 0;
|
||||
|
||||
if let Some(entries_val) = val.get("entries").and_then(Value::as_array) {
|
||||
for entry_val in entries_val {
|
||||
parse_entry(entry_val, &mut entries, &mut empty_weight);
|
||||
}
|
||||
}
|
||||
|
||||
let pool_conditions = val.get("conditions").map_or(Vec::new(), parse_conditions);
|
||||
let condition = combine_conditions(pool_conditions);
|
||||
|
||||
DynamicLootPool {
|
||||
entries,
|
||||
min_rolls,
|
||||
max_rolls,
|
||||
empty_weight,
|
||||
condition,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_rolls(val: &Value) -> (i32, i32) {
|
||||
if let Some(n) = val.as_i64() {
|
||||
let r = n as i32;
|
||||
return (r, r);
|
||||
}
|
||||
if let Some(f) = val.as_f64() {
|
||||
let r = f as i32;
|
||||
return (r, r);
|
||||
}
|
||||
if let Some(obj) = val.as_object() {
|
||||
if let (Some(min), Some(max)) = (obj.get("min"), obj.get("max")) {
|
||||
let min_r = min
|
||||
.as_i64()
|
||||
.or_else(|| min.as_f64().map(|f| f as i64))
|
||||
.unwrap_or(1) as i32;
|
||||
let max_r = max
|
||||
.as_i64()
|
||||
.or_else(|| max.as_f64().map(|f| f as i64))
|
||||
.unwrap_or(min_r as i64) as i32;
|
||||
return (min_r, max_r);
|
||||
}
|
||||
if let Some(value) = obj.get("value") {
|
||||
let r = value
|
||||
.as_i64()
|
||||
.or_else(|| value.as_f64().map(|f| f as i64))
|
||||
.unwrap_or(1) as i32;
|
||||
return (r, r);
|
||||
}
|
||||
if let Some(n) = obj.get("n").and_then(Value::as_i64) {
|
||||
return (0, n as i32);
|
||||
}
|
||||
}
|
||||
(1, 1)
|
||||
}
|
||||
|
||||
fn parse_entry(val: &Value, pool_entries: &mut Vec<DynamicLootEntry>, empty_weight: &mut i32) {
|
||||
let entry_type = val
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("minecraft:item");
|
||||
let entry_type = entry_type.strip_prefix("minecraft:").unwrap_or(entry_type);
|
||||
let weight = val.get("weight").and_then(Value::as_i64).unwrap_or(1) as i32;
|
||||
|
||||
match entry_type {
|
||||
"empty" => {
|
||||
*empty_weight += weight;
|
||||
}
|
||||
"item" | "tag" => {
|
||||
if let Some(name) = val.get("name").and_then(Value::as_str) {
|
||||
let (min_count, max_count, bonus_formula) =
|
||||
val.get("functions").map_or((1, 1, None), parse_functions);
|
||||
let conditions = val.get("conditions").map_or(Vec::new(), parse_conditions);
|
||||
let condition = combine_conditions(conditions);
|
||||
|
||||
pool_entries.push(DynamicLootEntry {
|
||||
item: name.to_string(),
|
||||
weight,
|
||||
min_count,
|
||||
max_count,
|
||||
condition,
|
||||
bonus_formula,
|
||||
});
|
||||
}
|
||||
}
|
||||
"alternatives" | "group" | "sequence" => {
|
||||
if let Some(children) = val.get("children").and_then(Value::as_array) {
|
||||
for child in children {
|
||||
parse_entry(child, pool_entries, empty_weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_functions(val: &Value) -> (i32, i32, Option<LootBonusFormula>) {
|
||||
let mut min_count = 1;
|
||||
let mut max_count = 1;
|
||||
let mut bonus_formula = None;
|
||||
|
||||
let Some(functions) = val.as_array() else {
|
||||
return (min_count, max_count, bonus_formula);
|
||||
};
|
||||
|
||||
for func in functions {
|
||||
let func_type = func
|
||||
.get("function")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let func_type = func_type.strip_prefix("minecraft:").unwrap_or(func_type);
|
||||
|
||||
match func_type {
|
||||
"set_count" => {
|
||||
if let Some(count) = func.get("count") {
|
||||
let (min_c, max_c) = parse_rolls(count);
|
||||
min_count = min_c;
|
||||
max_count = max_c;
|
||||
}
|
||||
}
|
||||
"apply_bonus" => {
|
||||
if let Some(formula) = func.get("formula").and_then(Value::as_str) {
|
||||
let formula_clean = formula.strip_prefix("minecraft:").unwrap_or(formula);
|
||||
match formula_clean {
|
||||
"ore_drops" => {
|
||||
bonus_formula = Some(LootBonusFormula::OreDrops);
|
||||
}
|
||||
"uniform_bonus_count" => {
|
||||
let multiplier = func
|
||||
.get("parameters")
|
||||
.and_then(|p| p.get("bonusMultiplier"))
|
||||
.and_then(Value::as_i64)
|
||||
.unwrap_or(1) as i32;
|
||||
bonus_formula = Some(LootBonusFormula::UniformBonusCount(multiplier));
|
||||
}
|
||||
"binomial_with_bonus_count" => {
|
||||
let extra = func
|
||||
.get("parameters")
|
||||
.and_then(|p| p.get("extra"))
|
||||
.and_then(Value::as_i64)
|
||||
.unwrap_or(0) as i32;
|
||||
let probability =
|
||||
func.get("parameters")
|
||||
.and_then(|p| p.get("probability"))
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(0.0) as f32;
|
||||
bonus_formula = Some(LootBonusFormula::BinomialWithBonusCount {
|
||||
extra,
|
||||
probability,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"looting_enchant" => {
|
||||
let max_bonus = func.get("count").map_or(1, |c| {
|
||||
c.get("max")
|
||||
.and_then(Value::as_i64)
|
||||
.or_else(|| c.as_i64())
|
||||
.unwrap_or(1) as i32
|
||||
});
|
||||
bonus_formula = Some(LootBonusFormula::UniformBonusCount(max_bonus.max(1)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(min_count, max_count, bonus_formula)
|
||||
}
|
||||
|
||||
fn parse_conditions(val: &Value) -> Vec<DynamicLootCondition> {
|
||||
let Some(arr) = val.as_array() else {
|
||||
return Vec::new();
|
||||
};
|
||||
arr.iter().map(parse_condition).collect()
|
||||
}
|
||||
|
||||
fn parse_condition(val: &Value) -> DynamicLootCondition {
|
||||
let cond_type = val
|
||||
.get("condition")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let cond_type = cond_type.strip_prefix("minecraft:").unwrap_or(cond_type);
|
||||
|
||||
match cond_type {
|
||||
"survives_explosion" => DynamicLootCondition::SurvivesExplosion,
|
||||
"killed_by_player" => DynamicLootCondition::KilledByPlayer,
|
||||
"random_chance" => {
|
||||
let chance = val.get("chance").and_then(Value::as_f64).unwrap_or(1.0) as f32;
|
||||
DynamicLootCondition::RandomChance { chance }
|
||||
}
|
||||
"random_chance_with_enchanted_bonus" => {
|
||||
let unenchanted = val
|
||||
.get("unenchanted_chance")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(0.0) as f32;
|
||||
let enchanted_base = val
|
||||
.get("enchanted_chance")
|
||||
.or_else(|| val.get("enchanted_chance_base"))
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(f64::from(unenchanted)) as f32;
|
||||
let per_level = val
|
||||
.get("enchanted_chance_per_level_above_first")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(0.0) as f32;
|
||||
DynamicLootCondition::RandomChanceWithEnchantedBonus {
|
||||
unenchanted_chance: unenchanted,
|
||||
enchanted_chance_base: enchanted_base,
|
||||
enchanted_chance_per_level_above_first: per_level,
|
||||
}
|
||||
}
|
||||
"table_bonus" => {
|
||||
let chances: Vec<f32> = val
|
||||
.get("chances")
|
||||
.and_then(Value::as_array)
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|x| x.as_f64().map(|f| f as f32))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
DynamicLootCondition::TableBonus {
|
||||
chances: chances.into_boxed_slice(),
|
||||
}
|
||||
}
|
||||
"inverted" => val.get("term").map_or(DynamicLootCondition::None, |term| {
|
||||
DynamicLootCondition::Inverted(Box::new(parse_condition(term)))
|
||||
}),
|
||||
"any_of" => val
|
||||
.get("terms")
|
||||
.and_then(Value::as_array)
|
||||
.map_or(DynamicLootCondition::None, |terms| {
|
||||
DynamicLootCondition::AnyOf(terms.iter().map(parse_condition).collect())
|
||||
}),
|
||||
"all_of" => val
|
||||
.get("terms")
|
||||
.and_then(Value::as_array)
|
||||
.map_or(DynamicLootCondition::None, |terms| {
|
||||
DynamicLootCondition::AllOf(terms.iter().map(parse_condition).collect())
|
||||
}),
|
||||
"match_tool" => {
|
||||
let pred_str = val
|
||||
.get("predicate")
|
||||
.map_or_else(String::new, Value::to_string);
|
||||
let has_silk = pred_str.contains("silk_touch");
|
||||
let has_shears = pred_str.contains("shears");
|
||||
if has_silk && has_shears {
|
||||
DynamicLootCondition::SilkTouchOrShears
|
||||
} else if has_silk {
|
||||
DynamicLootCondition::SilkTouch
|
||||
} else if has_shears {
|
||||
DynamicLootCondition::Shears
|
||||
} else {
|
||||
DynamicLootCondition::None
|
||||
}
|
||||
}
|
||||
"entity_properties" => {
|
||||
let on_fire = val
|
||||
.get("predicate")
|
||||
.and_then(|p| p.get("flags"))
|
||||
.and_then(|f| f.get("is_on_fire"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
if on_fire {
|
||||
DynamicLootCondition::EntityOnFire
|
||||
} else {
|
||||
DynamicLootCondition::None
|
||||
}
|
||||
}
|
||||
"weather_check" => {
|
||||
let raining = val.get("raining").and_then(Value::as_bool);
|
||||
let thundering = val.get("thundering").and_then(Value::as_bool);
|
||||
DynamicLootCondition::WeatherCheck {
|
||||
raining,
|
||||
thundering,
|
||||
}
|
||||
}
|
||||
_ => DynamicLootCondition::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn combine_conditions(conditions: Vec<DynamicLootCondition>) -> DynamicLootCondition {
|
||||
if conditions.is_empty() {
|
||||
DynamicLootCondition::None
|
||||
} else if conditions.len() == 1 {
|
||||
conditions.into_iter().next().unwrap_or_default()
|
||||
} else {
|
||||
DynamicLootCondition::AllOf(conditions)
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads all loot table JSON files from a directory recursively into `registry`.
|
||||
pub fn load_loot_tables_from_dir<S: std::hash::BuildHasher>(
|
||||
namespace: &str,
|
||||
dir: &Path,
|
||||
registry: &mut HashMap<String, Arc<DynamicLootTable>, S>,
|
||||
) -> usize {
|
||||
if !dir.is_dir() {
|
||||
return 0;
|
||||
}
|
||||
let before = registry.len();
|
||||
load_loot_tables_recursive(namespace, dir, dir, registry);
|
||||
registry.len() - before
|
||||
}
|
||||
|
||||
fn load_loot_tables_recursive<S: std::hash::BuildHasher>(
|
||||
namespace: &str,
|
||||
base_dir: &Path,
|
||||
current_dir: &Path,
|
||||
registry: &mut HashMap<String, Arc<DynamicLootTable>, S>,
|
||||
) {
|
||||
let Ok(entries) = fs::read_dir(current_dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
load_loot_tables_recursive(namespace, base_dir, &path, registry);
|
||||
} else if path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
|
||||
&& let Ok(rel_path) = path.strip_prefix(base_dir)
|
||||
{
|
||||
let mut stem_path = rel_path.to_string_lossy().to_string();
|
||||
if let Some(stem) = stem_path.strip_suffix(".json") {
|
||||
stem_path = stem.to_string();
|
||||
}
|
||||
let stem_path = stem_path.replace('\\', "/");
|
||||
let loot_table_id = format!("{namespace}:{stem_path}");
|
||||
|
||||
if let Ok(content) = fs::read_to_string(&path)
|
||||
&& let Some(table) = parse_loot_table(&content)
|
||||
{
|
||||
registry.insert(loot_table_id, Arc::new(table));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_simple_loot_table() {
|
||||
let json = r#"{
|
||||
"type": "minecraft:chest",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 2,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:diamond",
|
||||
"weight": 5,
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:set_count",
|
||||
"count": {"min": 1, "max": 3}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:empty",
|
||||
"weight": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let table = parse_loot_table(json).expect("valid loot table");
|
||||
assert_eq!(table.pools.len(), 1);
|
||||
let pool = &table.pools[0];
|
||||
assert_eq!(pool.min_rolls, 2);
|
||||
assert_eq!(pool.max_rolls, 2);
|
||||
assert_eq!(pool.empty_weight, 10);
|
||||
assert_eq!(pool.entries.len(), 1);
|
||||
assert_eq!(pool.entries[0].item, "minecraft:diamond");
|
||||
assert_eq!(pool.entries[0].weight, 5);
|
||||
assert_eq!(pool.entries[0].min_count, 1);
|
||||
assert_eq!(pool.entries[0].max_count, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_block_loot_with_silk_touch_and_fortune() {
|
||||
let json = r#"{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:alternatives",
|
||||
"children": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:coal_ore",
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "minecraft:match_tool",
|
||||
"predicate": {
|
||||
"predicates": {
|
||||
"minecraft:enchantments": [
|
||||
{"enchantments": "minecraft:silk_touch"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:coal",
|
||||
"functions": [
|
||||
{
|
||||
"function": "minecraft:apply_bonus",
|
||||
"formula": "minecraft:ore_drops"
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "minecraft:survives_explosion"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let table = parse_loot_table(json).expect("valid block loot table");
|
||||
assert_eq!(table.pools.len(), 1);
|
||||
let pool = &table.pools[0];
|
||||
assert_eq!(pool.entries.len(), 2);
|
||||
assert_eq!(pool.entries[0].item, "minecraft:coal_ore");
|
||||
assert_eq!(pool.entries[0].condition, DynamicLootCondition::SilkTouch);
|
||||
assert_eq!(pool.entries[1].item, "minecraft:coal");
|
||||
assert_eq!(
|
||||
pool.entries[1].bonus_formula,
|
||||
Some(LootBonusFormula::OreDrops)
|
||||
);
|
||||
assert_eq!(
|
||||
pool.entries[1].condition,
|
||||
DynamicLootCondition::SurvivesExplosion
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_loading() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let root = temp.path();
|
||||
let sub = root.join("blocks");
|
||||
fs::create_dir_all(&sub).expect("create dir");
|
||||
|
||||
let table_json = r#"{
|
||||
"pools": [
|
||||
{
|
||||
"rolls": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "minecraft:dirt"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
fs::write(sub.join("dirt.json"), table_json).expect("write json");
|
||||
|
||||
let mut registry = HashMap::new();
|
||||
let count = load_loot_tables_from_dir("custom", root, &mut registry);
|
||||
assert_eq!(count, 1);
|
||||
assert!(registry.contains_key("custom:blocks/dirt"));
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,7 @@
|
||||
pub mod context_provider_loader;
|
||||
pub mod damage_type_loader;
|
||||
pub mod function_loader;
|
||||
pub mod loot_table_loader;
|
||||
pub mod recipe_loader;
|
||||
pub mod test_loader;
|
||||
|
||||
@ -8,14 +11,20 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use pumpkin_data::damage::DamageType;
|
||||
use pumpkin_data::registry::RegistryEntryData;
|
||||
use pumpkin_nbt::tag::NbtTag;
|
||||
use pumpkin_nbt::{NbtCompound, nbt_compress::read_gzip_compound_tag};
|
||||
use pumpkin_protocol::codec::recipe::DynamicRecipe;
|
||||
use pumpkin_util::loot_table::DynamicLootTable;
|
||||
|
||||
use crate::command::context::command_source::CommandSource;
|
||||
use crate::server::Server;
|
||||
use crate::server::recipe::RecipeManager;
|
||||
use crate::world::loot::LootTableHandle;
|
||||
|
||||
use self::context_provider_loader::ContextProviderRegistry;
|
||||
use self::damage_type_loader::{DamageTypeDefinition, DamageTypeEntry, DamageTypeRegistry};
|
||||
use self::test_loader::{
|
||||
TestInstance, TestInstanceRegistry, load_test_instances_from_dir, to_registry_entry,
|
||||
};
|
||||
@ -63,6 +72,10 @@ pub struct DatapackManager {
|
||||
functions: RwLock<HashMap<String, Arc<[String]>>>,
|
||||
function_tags: RwLock<HashMap<String, Vec<String>>>,
|
||||
test_instances: RwLock<TestInstanceRegistry>,
|
||||
context_int_providers: RwLock<ContextProviderRegistry>,
|
||||
context_float_providers: RwLock<ContextProviderRegistry>,
|
||||
damage_types: RwLock<DamageTypeRegistry>,
|
||||
loot_tables: RwLock<HashMap<String, Arc<DynamicLootTable>>>,
|
||||
}
|
||||
|
||||
fn share_function_bodies(
|
||||
@ -88,6 +101,10 @@ impl DatapackManager {
|
||||
functions: RwLock::new(HashMap::new()),
|
||||
function_tags: RwLock::new(HashMap::new()),
|
||||
test_instances: RwLock::new(HashMap::new()),
|
||||
context_int_providers: RwLock::new(HashMap::new()),
|
||||
context_float_providers: RwLock::new(HashMap::new()),
|
||||
damage_types: RwLock::new(HashMap::new()),
|
||||
loot_tables: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,6 +120,10 @@ impl DatapackManager {
|
||||
let mut all_functions: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let mut all_function_tags: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let mut all_test_instances: TestInstanceRegistry = HashMap::new();
|
||||
let mut all_context_int_providers: ContextProviderRegistry = HashMap::new();
|
||||
let mut all_context_float_providers: ContextProviderRegistry = HashMap::new();
|
||||
let mut all_damage_type_defs: HashMap<String, DamageTypeDefinition> = HashMap::new();
|
||||
let mut all_loot_tables: HashMap<String, Arc<DynamicLootTable>> = HashMap::new();
|
||||
|
||||
// Embedded test instances are compile-time constants and always load,
|
||||
// so on-disk packs can override them by id.
|
||||
@ -110,60 +131,25 @@ impl DatapackManager {
|
||||
if embedded_count > 0 {
|
||||
info!("Loaded {embedded_count} embedded test instance(s)");
|
||||
}
|
||||
if datapacks_dir.is_dir() {
|
||||
match fs::read_dir(&datapacks_dir) {
|
||||
Ok(entries) => {
|
||||
for entry in entries.flatten() {
|
||||
let pack_path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
let mut acc = PackContentAccumulators {
|
||||
recipes: &mut all_recipes,
|
||||
functions: &mut all_functions,
|
||||
function_tags: &mut all_function_tags,
|
||||
test_instances: &mut all_test_instances,
|
||||
damage_types: &mut all_damage_type_defs,
|
||||
context_int_providers: &mut all_context_int_providers,
|
||||
context_float_providers: &mut all_context_float_providers,
|
||||
loot_tables: &mut all_loot_tables,
|
||||
};
|
||||
|
||||
if file_name.starts_with('.') || !pack_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pack_id = format!("file/{file_name}");
|
||||
let is_enabled = enabled_packs
|
||||
.iter()
|
||||
.any(|p| p == &pack_id || p == &file_name);
|
||||
if !is_enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (description, pack_format, known_packs) = read_pack_mcmeta(&pack_path);
|
||||
|
||||
let (pack_recipe_count, pack_function_count, pack_test_instance_count) =
|
||||
load_pack_contents(
|
||||
&pack_path,
|
||||
&mut all_recipes,
|
||||
&mut all_functions,
|
||||
&mut all_function_tags,
|
||||
&mut all_test_instances,
|
||||
scan_datapacks_dir(
|
||||
&datapacks_dir,
|
||||
enabled_packs,
|
||||
&mut acc,
|
||||
&mut loaded_packs_vec,
|
||||
);
|
||||
|
||||
info!(
|
||||
"Loaded datapack '{file_name}': {pack_recipe_count} recipe(s), {pack_function_count} function(s), {pack_test_instance_count} test instance(s)"
|
||||
);
|
||||
|
||||
loaded_packs_vec.push(LoadedDatapack {
|
||||
id: pack_id,
|
||||
name: file_name,
|
||||
description,
|
||||
pack_format,
|
||||
root_path: pack_path,
|
||||
recipe_count: pack_recipe_count,
|
||||
function_count: pack_function_count,
|
||||
known_packs,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(
|
||||
"Failed to read datapacks directory '{}': {error}",
|
||||
datapacks_dir.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let damage_type_registry = build_damage_type_registry(all_damage_type_defs);
|
||||
|
||||
recipe_manager.set_recipes(all_recipes);
|
||||
*self
|
||||
@ -183,6 +169,22 @@ impl DatapackManager {
|
||||
.test_instances
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = all_test_instances;
|
||||
*self
|
||||
.context_int_providers
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = all_context_int_providers;
|
||||
*self
|
||||
.context_float_providers
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = all_context_float_providers;
|
||||
*self
|
||||
.damage_types
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = damage_type_registry;
|
||||
*self
|
||||
.loot_tables
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = all_loot_tables;
|
||||
}
|
||||
|
||||
pub fn get_loaded_packs(&self) -> Vec<LoadedDatapack> {
|
||||
@ -219,6 +221,162 @@ impl DatapackManager {
|
||||
names
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_context_int_provider(&self, id: &str) -> Option<NbtTag> {
|
||||
self.context_int_providers
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_context_float_provider(&self, id: &str) -> Option<NbtTag> {
|
||||
self.context_float_providers
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_context_int_provider_names(&self) -> Vec<String> {
|
||||
let providers = self
|
||||
.context_int_providers
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut names: Vec<_> = providers.keys().cloned().collect();
|
||||
names.sort_unstable();
|
||||
names
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_context_float_provider_names(&self) -> Vec<String> {
|
||||
let providers = self
|
||||
.context_float_providers
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut names: Vec<_> = providers.keys().cloned().collect();
|
||||
names.sort_unstable();
|
||||
names
|
||||
}
|
||||
|
||||
pub fn insert_context_int_provider(&self, id: String, tag: NbtTag) {
|
||||
self.context_int_providers
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(id, tag);
|
||||
}
|
||||
|
||||
pub fn insert_context_float_provider(&self, id: String, tag: NbtTag) {
|
||||
self.context_float_providers
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(id, tag);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_loot_table(&self, key: &str) -> Option<LootTableHandle> {
|
||||
let full_key = if key.contains(':') {
|
||||
key.to_string()
|
||||
} else {
|
||||
format!("minecraft:{key}")
|
||||
};
|
||||
|
||||
let guard = self
|
||||
.loot_tables
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
|
||||
if let Some(table) = guard.get(&full_key).or_else(|| guard.get(key)) {
|
||||
return Some(LootTableHandle::Dynamic(table.clone()));
|
||||
}
|
||||
|
||||
pumpkin_data::loot_table::get_loot_table(key)
|
||||
.or_else(|| pumpkin_data::loot_table::get_loot_table(&full_key))
|
||||
.map(LootTableHandle::Static)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_loot_table_names(&self) -> Vec<String> {
|
||||
let guard = self
|
||||
.loot_tables
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut names: Vec<_> = guard.keys().cloned().collect();
|
||||
names.sort_unstable();
|
||||
names
|
||||
}
|
||||
|
||||
pub fn insert_loot_table(&self, key: String, table: Arc<DynamicLootTable>) {
|
||||
self.loot_tables
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.insert(key, table);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_damage_type(&self, id: &str) -> Option<DamageType> {
|
||||
let guard = self
|
||||
.damage_types
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let full_id = if id.contains(':') {
|
||||
id.to_string()
|
||||
} else {
|
||||
format!("minecraft:{id}")
|
||||
};
|
||||
guard
|
||||
.get(&full_id)
|
||||
.or_else(|| guard.get(id))
|
||||
.map(|e| e.damage_type)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_damage_type_names(&self) -> Vec<String> {
|
||||
let guard = self
|
||||
.damage_types
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut names: Vec<_> = guard.keys().cloned().collect();
|
||||
names.sort_unstable();
|
||||
names
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_damage_type_registry_map(&self) -> HashMap<String, DamageTypeEntry> {
|
||||
self.damage_types
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_all_damage_type_registry_entries(&self) -> Vec<RegistryEntryData> {
|
||||
let guard = self
|
||||
.damage_types
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let mut entries: Vec<RegistryEntryData> = guard
|
||||
.values()
|
||||
.map(DamageTypeEntry::to_registry_entry)
|
||||
.collect();
|
||||
entries.sort_by(|a, b| a.entry_id.cmp(&b.entry_id));
|
||||
entries
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn merge_damage_type_entries(
|
||||
&self,
|
||||
vanilla_entries: &[RegistryEntryData],
|
||||
) -> Vec<RegistryEntryData> {
|
||||
let guard = self
|
||||
.damage_types
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
merge_damage_type_entries(vanilla_entries, &guard)
|
||||
}
|
||||
|
||||
/// Returns datapack test instances in the protocol's synced-registry entry format.
|
||||
/// The vanilla Test Instance Block renderer resolves required/padding/base rotation
|
||||
/// through this registry using the controller's `data.test` resource key.
|
||||
@ -868,19 +1026,153 @@ fn parse_structure_resource_location(resource_location: &str) -> Result<(&str, &
|
||||
Ok((namespace, path))
|
||||
}
|
||||
|
||||
/// Loads recipes, functions, function tags, and test instances from a single
|
||||
/// datapack directory, returning per-pack counts as `(recipes, functions, test_instances)`.
|
||||
fn build_damage_type_registry(
|
||||
mut all_damage_type_defs: HashMap<String, DamageTypeDefinition>,
|
||||
) -> DamageTypeRegistry {
|
||||
let mut damage_type_registry: DamageTypeRegistry = HashMap::new();
|
||||
let mut next_custom_id = 51u8;
|
||||
let mut static_map: HashMap<String, &'static DamageType> = HashMap::new();
|
||||
let mut names = Vec::new();
|
||||
|
||||
let mut sorted_ids: Vec<String> = all_damage_type_defs.keys().cloned().collect();
|
||||
sorted_ids.sort();
|
||||
|
||||
for id in sorted_ids {
|
||||
let Some(def) = all_damage_type_defs.remove(&id) else {
|
||||
continue;
|
||||
};
|
||||
let path = id.split_once(':').map_or(id.as_str(), |(_, p)| p);
|
||||
let numeric_id = pumpkin_data::damage::DamageType::from_name(path)
|
||||
.or_else(|| pumpkin_data::damage::DamageType::from_name(&id))
|
||||
.map_or_else(
|
||||
|| {
|
||||
let nid = next_custom_id;
|
||||
next_custom_id = next_custom_id.saturating_add(1);
|
||||
nid
|
||||
},
|
||||
|vanilla_dt| vanilla_dt.id,
|
||||
);
|
||||
|
||||
let dt = damage_type_loader::to_damage_type(numeric_id, &def);
|
||||
let nbt_data = damage_type_loader::to_registry_nbt_bytes(&def);
|
||||
let leaked_dt = Box::leak(Box::new(dt));
|
||||
static_map.insert(id.clone(), leaked_dt);
|
||||
static_map.insert(path.to_string(), leaked_dt);
|
||||
names.push(id.clone());
|
||||
|
||||
damage_type_registry.insert(
|
||||
id.clone(),
|
||||
DamageTypeEntry {
|
||||
id,
|
||||
numeric_id,
|
||||
definition: def,
|
||||
damage_type: dt,
|
||||
nbt_data,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pumpkin_command::argument_types::resource::register_dynamic_damage_types(
|
||||
Box::new(move |req_id: &str| static_map.get(req_id).copied()),
|
||||
names,
|
||||
);
|
||||
|
||||
damage_type_registry
|
||||
}
|
||||
|
||||
struct PackContentAccumulators<'a> {
|
||||
recipes: &'a mut Vec<DynamicRecipe>,
|
||||
functions: &'a mut HashMap<String, Vec<String>>,
|
||||
function_tags: &'a mut HashMap<String, Vec<String>>,
|
||||
test_instances: &'a mut TestInstanceRegistry,
|
||||
damage_types: &'a mut HashMap<String, DamageTypeDefinition>,
|
||||
context_int_providers: &'a mut ContextProviderRegistry,
|
||||
context_float_providers: &'a mut ContextProviderRegistry,
|
||||
loot_tables: &'a mut HashMap<String, Arc<DynamicLootTable>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct PackContentCounts {
|
||||
recipes: usize,
|
||||
functions: usize,
|
||||
test_instances: usize,
|
||||
damage_types: usize,
|
||||
int_providers: usize,
|
||||
float_providers: usize,
|
||||
loot_tables: usize,
|
||||
}
|
||||
|
||||
fn scan_datapacks_dir(
|
||||
datapacks_dir: &Path,
|
||||
enabled_packs: &[String],
|
||||
acc: &mut PackContentAccumulators<'_>,
|
||||
loaded_packs_vec: &mut Vec<LoadedDatapack>,
|
||||
) {
|
||||
if !datapacks_dir.is_dir() {
|
||||
return;
|
||||
}
|
||||
let entries = match fs::read_dir(datapacks_dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
"Failed to read datapacks directory '{}': {error}",
|
||||
datapacks_dir.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let pack_path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
if file_name.starts_with('.') || !pack_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pack_id = format!("file/{file_name}");
|
||||
let is_enabled = enabled_packs
|
||||
.iter()
|
||||
.any(|p| p == &pack_id || p == &file_name);
|
||||
if !is_enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (description, pack_format, known_packs) = read_pack_mcmeta(&pack_path);
|
||||
let counts = load_pack_contents(&pack_path, acc);
|
||||
|
||||
info!(
|
||||
"Loaded datapack '{file_name}': {} recipe(s), {} function(s), {} test instance(s), {} damage type(s), {} int provider(s), {} float provider(s), {} loot table(s)",
|
||||
counts.recipes,
|
||||
counts.functions,
|
||||
counts.test_instances,
|
||||
counts.damage_types,
|
||||
counts.int_providers,
|
||||
counts.float_providers,
|
||||
counts.loot_tables,
|
||||
);
|
||||
|
||||
loaded_packs_vec.push(LoadedDatapack {
|
||||
id: pack_id,
|
||||
name: file_name,
|
||||
description,
|
||||
pack_format,
|
||||
root_path: pack_path,
|
||||
recipe_count: counts.recipes,
|
||||
function_count: counts.functions,
|
||||
known_packs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads recipes, functions, function tags, test instances, damage types, context number providers,
|
||||
/// and loot tables from a single datapack directory, returning per-pack counts as [`PackContentCounts`].
|
||||
fn load_pack_contents(
|
||||
pack_path: &Path,
|
||||
all_recipes: &mut Vec<DynamicRecipe>,
|
||||
all_functions: &mut HashMap<String, Vec<String>>,
|
||||
all_function_tags: &mut HashMap<String, Vec<String>>,
|
||||
all_test_instances: &mut TestInstanceRegistry,
|
||||
) -> (usize, usize, usize) {
|
||||
acc: &mut PackContentAccumulators<'_>,
|
||||
) -> PackContentCounts {
|
||||
let data_dir = pack_path.join("data");
|
||||
let mut pack_recipe_count = 0;
|
||||
let mut pack_function_count = 0;
|
||||
let mut pack_test_instance_count = 0;
|
||||
let mut counts = PackContentCounts::default();
|
||||
|
||||
if data_dir.is_dir()
|
||||
&& let Ok(ns_entries) = fs::read_dir(&data_dir)
|
||||
@ -899,8 +1191,8 @@ fn load_pack_contents(
|
||||
load_recipes_from_dir(
|
||||
&namespace,
|
||||
&recipe_dir,
|
||||
all_recipes,
|
||||
&mut pack_recipe_count,
|
||||
acc.recipes,
|
||||
&mut counts.recipes,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -909,9 +1201,9 @@ fn load_pack_contents(
|
||||
for fn_sub in ["function", "functions"] {
|
||||
let fn_dir = ns_path.join(fn_sub);
|
||||
if fn_dir.is_dir() {
|
||||
let before = all_functions.len();
|
||||
function_loader::load_functions_from_dir(&namespace, &fn_dir, all_functions);
|
||||
pack_function_count += all_functions.len() - before;
|
||||
let before = acc.functions.len();
|
||||
function_loader::load_functions_from_dir(&namespace, &fn_dir, acc.functions);
|
||||
counts.functions += acc.functions.len() - before;
|
||||
}
|
||||
}
|
||||
|
||||
@ -921,26 +1213,126 @@ fn load_pack_contents(
|
||||
function_loader::load_function_tags_from_dir(
|
||||
&namespace,
|
||||
&tags_dir,
|
||||
all_function_tags,
|
||||
acc.function_tags,
|
||||
);
|
||||
}
|
||||
// Load game test instances
|
||||
let test_instance_dir = ns_path.join("test_instance");
|
||||
if test_instance_dir.is_dir() {
|
||||
pack_test_instance_count += load_test_instances_from_dir(
|
||||
counts.test_instances += load_test_instances_from_dir(
|
||||
&namespace,
|
||||
&test_instance_dir,
|
||||
all_test_instances,
|
||||
acc.test_instances,
|
||||
);
|
||||
}
|
||||
|
||||
// Load damage types
|
||||
for dt_sub in ["damage_type", "damage_types"] {
|
||||
let dt_dir = ns_path.join(dt_sub);
|
||||
if dt_dir.is_dir() {
|
||||
counts.damage_types += damage_type_loader::load_damage_types_from_dir(
|
||||
&namespace,
|
||||
&dt_dir,
|
||||
acc.damage_types,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load context int providers
|
||||
for int_sub in ["context_int_provider", "context_int_providers"] {
|
||||
let provider_dir = ns_path.join(int_sub);
|
||||
if provider_dir.is_dir() {
|
||||
counts.int_providers +=
|
||||
context_provider_loader::load_context_providers_from_dir(
|
||||
&namespace,
|
||||
&provider_dir,
|
||||
acc.context_int_providers,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load context float providers
|
||||
for float_sub in ["context_float_provider", "context_float_providers"] {
|
||||
let provider_dir = ns_path.join(float_sub);
|
||||
if provider_dir.is_dir() {
|
||||
counts.float_providers +=
|
||||
context_provider_loader::load_context_providers_from_dir(
|
||||
&namespace,
|
||||
&provider_dir,
|
||||
acc.context_float_providers,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load loot tables
|
||||
for lt_sub in ["loot_table", "loot_tables"] {
|
||||
let lt_dir = ns_path.join(lt_sub);
|
||||
if lt_dir.is_dir() {
|
||||
counts.loot_tables += loot_table_loader::load_loot_tables_from_dir(
|
||||
&namespace,
|
||||
<_dir,
|
||||
acc.loot_tables,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
pack_recipe_count,
|
||||
pack_function_count,
|
||||
pack_test_instance_count,
|
||||
)
|
||||
counts
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn clone_registry_entry(entry: &RegistryEntryData) -> RegistryEntryData {
|
||||
RegistryEntryData {
|
||||
entry_id: entry.entry_id.clone(),
|
||||
data: entry.data.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn merge_damage_type_entries<S: std::hash::BuildHasher>(
|
||||
vanilla_entries: &[RegistryEntryData],
|
||||
custom_entries: &HashMap<String, DamageTypeEntry, S>,
|
||||
) -> Vec<RegistryEntryData> {
|
||||
if custom_entries.is_empty() {
|
||||
return vanilla_entries.iter().map(clone_registry_entry).collect();
|
||||
}
|
||||
|
||||
let mut merged = Vec::with_capacity(vanilla_entries.len() + custom_entries.len());
|
||||
let mut overridden = std::collections::HashSet::new();
|
||||
|
||||
for entry in vanilla_entries {
|
||||
let full_id = if entry.entry_id.contains(':') {
|
||||
entry.entry_id.clone()
|
||||
} else {
|
||||
format!("minecraft:{}", entry.entry_id)
|
||||
};
|
||||
|
||||
if let Some(custom) = custom_entries
|
||||
.get(&full_id)
|
||||
.or_else(|| custom_entries.get(&entry.entry_id))
|
||||
{
|
||||
let mut reg_entry = custom.to_registry_entry();
|
||||
reg_entry.entry_id.clone_from(&entry.entry_id);
|
||||
merged.push(reg_entry);
|
||||
overridden.insert(full_id);
|
||||
overridden.insert(entry.entry_id.clone());
|
||||
} else {
|
||||
merged.push(clone_registry_entry(entry));
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_custom: Vec<&DamageTypeEntry> = custom_entries
|
||||
.values()
|
||||
.filter(|e| !overridden.contains(&e.id))
|
||||
.collect();
|
||||
sorted_custom.sort_by_key(|e| e.numeric_id);
|
||||
|
||||
for custom in sorted_custom {
|
||||
merged.push(custom.to_registry_entry());
|
||||
}
|
||||
|
||||
merged
|
||||
}
|
||||
|
||||
fn read_pack_mcmeta(pack_path: &Path) -> (String, u32, Vec<KnownPackData>) {
|
||||
|
||||
@ -2273,11 +2273,12 @@ impl LivingEntity {
|
||||
fn drop_loot(&self, params: &LootContextParameters) {
|
||||
let resource_name = self.get_entity().entity_type.resource_name;
|
||||
let key = format!("minecraft:entities/{resource_name}");
|
||||
if let Some(loot_table) = pumpkin_data::loot_table::get_loot_table(&key) {
|
||||
let world = self.entity.world.load();
|
||||
if let Some(loot_table) = world.get_loot_table(&key) {
|
||||
let seed: i64 = rand::random();
|
||||
let pos = self.entity.block_pos.load();
|
||||
for stack in crate::world::loot::generate_loot_with_context(loot_table, seed, params) {
|
||||
self.entity.world.load().drop_stack(&pos, stack);
|
||||
for stack in crate::world::loot::generate_loot_from_handle(&loot_table, seed, params) {
|
||||
world.drop_stack(&pos, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,8 +15,6 @@ use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_util::text::TextComponent;
|
||||
|
||||
use crate::entity::{Entity, player::Player};
|
||||
use crate::world::loot::fill_chest_inventory;
|
||||
use pumpkin_data::loot_table::get_loot_table;
|
||||
|
||||
pub(super) struct MinecartInventory {
|
||||
items: RwLock<Vec<ItemStack>>,
|
||||
@ -98,7 +96,7 @@ impl MinecartInventory {
|
||||
let Some((loot_table, seed)) = loot_table else {
|
||||
return;
|
||||
};
|
||||
let Some(table) = get_loot_table(&loot_table) else {
|
||||
let Some(table) = crate::world::loot::get_loot_table(&loot_table) else {
|
||||
*self
|
||||
.loot_table
|
||||
.lock()
|
||||
@ -107,7 +105,7 @@ impl MinecartInventory {
|
||||
};
|
||||
|
||||
let inventory: Arc<dyn Inventory> = self.clone();
|
||||
fill_chest_inventory(&inventory, table, seed);
|
||||
crate::world::loot::fill_chest_inventory_handle(&inventory, &table, seed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -43,8 +43,8 @@ fn get_archaeology_loot(is_sand: bool, location: BlockPos, world: &World) -> Ite
|
||||
"minecraft:archaeology/trail_ruins_common"
|
||||
};
|
||||
|
||||
if let Some(table) = pumpkin_data::loot_table::get_loot_table(loot_key) {
|
||||
let items = crate::world::loot::generate_loot(table, rand::random());
|
||||
if let Some(table) = world.get_loot_table(loot_key) {
|
||||
let items = table.generate_loot(rand::random());
|
||||
if let Some(first) = items.into_iter().next() {
|
||||
return first;
|
||||
}
|
||||
|
||||
@ -50,10 +50,10 @@ impl ItemBehaviour for HoeItem {
|
||||
world.set_block_state(&location, result.new_state_id, BlockFlags::NOTIFY_ALL);
|
||||
|
||||
if let Some(loot_key) = result.entry.loot
|
||||
&& let Some(loot_table) = pumpkin_data::loot_table::get_loot_table(loot_key)
|
||||
&& let Some(loot_table) = world.get_loot_table(loot_key)
|
||||
{
|
||||
let seed = rand::random::<i64>();
|
||||
let drops = crate::world::loot::generate_loot(loot_table, seed);
|
||||
let drops = loot_table.generate_loot(seed);
|
||||
for drop_stack in drops {
|
||||
if result.entry.drop_strategy == Some(DropStrategy::ClickedFace) {
|
||||
world.drop_stack_from_face(&location, face, drop_stack);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use super::*;
|
||||
use bytes::Bytes;
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl JavaClient {
|
||||
pub async fn handle_known_packs(&self, server: &Server) -> Option<PacketHandlerResult> {
|
||||
@ -15,12 +17,48 @@ impl JavaClient {
|
||||
|
||||
let test_instance_entries =
|
||||
server.datapack_manager.get_test_instance_registry_entries();
|
||||
let custom_damage_type_map = server.datapack_manager.get_damage_type_registry_map();
|
||||
|
||||
let packets = tokio::task::spawn_blocking(move || {
|
||||
Self::build_synced_registry_packets(
|
||||
version,
|
||||
&test_instance_entries,
|
||||
&custom_damage_type_map,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
for packet_data in packets {
|
||||
self.send_packet_now(packet_data).await;
|
||||
}
|
||||
}
|
||||
|
||||
// We are done with configuring
|
||||
self.send_packet(&CFinishConfig).await;
|
||||
|
||||
if !version.supports_configuration_state() {
|
||||
return Some(self.handle_config_acknowledged(server).await);
|
||||
}
|
||||
|
||||
debug!("Finished config");
|
||||
None
|
||||
}
|
||||
|
||||
fn build_synced_registry_packets<S: std::hash::BuildHasher>(
|
||||
version: JavaMinecraftVersion,
|
||||
test_instance_entries: &[pumpkin_data::registry::RegistryEntryData],
|
||||
custom_damage_type_map: &HashMap<
|
||||
String,
|
||||
crate::data::datapack::damage_type_loader::DamageTypeEntry,
|
||||
S,
|
||||
>,
|
||||
) -> Vec<Bytes> {
|
||||
let registry = Registry::get_synced(version);
|
||||
let mut packets = Vec::new();
|
||||
let mut sent_dimension_type = false;
|
||||
let mut sent_test_instance = false;
|
||||
let mut sent_damage_type = false;
|
||||
|
||||
for reg in ®istry {
|
||||
if reg.registry_id == "minecraft:dimension_type" {
|
||||
@ -30,7 +68,24 @@ impl JavaClient {
|
||||
if reg.registry_id == "minecraft:test_instance" {
|
||||
sent_test_instance = true;
|
||||
|
||||
let packet = CRegistryData::new(®.registry_id, &test_instance_entries);
|
||||
let packet = CRegistryData::new(®.registry_id, test_instance_entries);
|
||||
|
||||
if let Ok(data) = Self::serialize_packet_for_version(&packet, version) {
|
||||
packets.push(data);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if reg.registry_id == "minecraft:damage_type" {
|
||||
sent_damage_type = true;
|
||||
|
||||
let merged_damage_types = crate::data::datapack::merge_damage_type_entries(
|
||||
®.registry_entries,
|
||||
custom_damage_type_map,
|
||||
);
|
||||
|
||||
let packet = CRegistryData::new(®.registry_id, &merged_damage_types);
|
||||
|
||||
if let Ok(data) = Self::serialize_packet_for_version(&packet, version) {
|
||||
packets.push(data);
|
||||
@ -51,7 +106,18 @@ impl JavaClient {
|
||||
// client, so always provide it even when the static table omitted it.
|
||||
if !sent_test_instance {
|
||||
let test_instance = "minecraft:test_instance".to_string();
|
||||
let packet = CRegistryData::new(&test_instance, &test_instance_entries);
|
||||
let packet = CRegistryData::new(&test_instance, test_instance_entries);
|
||||
|
||||
if let Ok(data) = Self::serialize_packet_for_version(&packet, version) {
|
||||
packets.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
if !sent_damage_type && !custom_damage_type_map.is_empty() {
|
||||
let damage_type = "minecraft:damage_type".to_string();
|
||||
let merged_damage_types =
|
||||
crate::data::datapack::merge_damage_type_entries(&[], custom_damage_type_map);
|
||||
let packet = CRegistryData::new(&damage_type, &merged_damage_types);
|
||||
|
||||
if let Ok(data) = Self::serialize_packet_for_version(&packet, version) {
|
||||
packets.push(data);
|
||||
@ -59,21 +125,7 @@ impl JavaClient {
|
||||
}
|
||||
|
||||
if !sent_dimension_type {
|
||||
let dims = [
|
||||
&pumpkin_data::dimension::Dimension::OVERWORLD,
|
||||
&pumpkin_data::dimension::Dimension::OVERWORLD_CAVES,
|
||||
&pumpkin_data::dimension::Dimension::THE_END,
|
||||
&pumpkin_data::dimension::Dimension::THE_NETHER,
|
||||
];
|
||||
|
||||
let dim_entries: Vec<pumpkin_data::registry::RegistryEntryData> = dims
|
||||
.iter()
|
||||
.map(|dim| pumpkin_data::registry::RegistryEntryData {
|
||||
entry_id: dim.minecraft_name.to_string(),
|
||||
data: Some(build_dimension_nbt(dim).into_boxed_slice()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let dim_entries = get_fallback_dimension_type_entries();
|
||||
let dim_type = "minecraft:dimension_type".to_string();
|
||||
let packet = CRegistryData::new(&dim_type, &dim_entries);
|
||||
|
||||
@ -82,16 +134,7 @@ impl JavaClient {
|
||||
}
|
||||
}
|
||||
|
||||
let mut tags = Vec::new();
|
||||
|
||||
for &key in pumpkin_data::tag::RegistryKey::NETWORK_KEYS {
|
||||
if pumpkin_data::tag::get_registry_key_tags(version, key)
|
||||
.is_some_and(|map| !map.is_empty())
|
||||
{
|
||||
tags.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
let tags = get_network_tag_keys(version);
|
||||
let packet = CUpdateTags::new(&tags);
|
||||
|
||||
if let Ok(data) = Self::serialize_packet_for_version(&packet, version) {
|
||||
@ -99,23 +142,34 @@ impl JavaClient {
|
||||
}
|
||||
|
||||
packets
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
for packet_data in packets {
|
||||
self.send_packet_now(packet_data).await;
|
||||
}
|
||||
}
|
||||
|
||||
// We are done with configuring
|
||||
self.send_packet(&CFinishConfig).await;
|
||||
|
||||
if !version.supports_configuration_state() {
|
||||
return Some(self.handle_config_acknowledged(server).await);
|
||||
}
|
||||
|
||||
debug!("Finished config");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_fallback_dimension_type_entries() -> Vec<pumpkin_data::registry::RegistryEntryData> {
|
||||
let dims = [
|
||||
&pumpkin_data::dimension::Dimension::OVERWORLD,
|
||||
&pumpkin_data::dimension::Dimension::OVERWORLD_CAVES,
|
||||
&pumpkin_data::dimension::Dimension::THE_END,
|
||||
&pumpkin_data::dimension::Dimension::THE_NETHER,
|
||||
];
|
||||
|
||||
dims.iter()
|
||||
.map(|dim| pumpkin_data::registry::RegistryEntryData {
|
||||
entry_id: dim.minecraft_name.to_string(),
|
||||
data: Some(build_dimension_nbt(dim).into_boxed_slice()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_network_tag_keys(version: JavaMinecraftVersion) -> Vec<pumpkin_data::tag::RegistryKey> {
|
||||
let mut tags = Vec::new();
|
||||
|
||||
for &key in pumpkin_data::tag::RegistryKey::NETWORK_KEYS {
|
||||
if pumpkin_data::tag::get_registry_key_tags(version, key).is_some_and(|map| !map.is_empty())
|
||||
{
|
||||
tags.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
tags
|
||||
}
|
||||
|
||||
@ -11,6 +11,14 @@ impl PendingConnection {
|
||||
}
|
||||
let registry = pumpkin_data::registry::Registry::get_synced(version);
|
||||
for reg in ®istry {
|
||||
if reg.registry_id == "minecraft:damage_type" {
|
||||
let merged = server
|
||||
.datapack_manager
|
||||
.merge_damage_type_entries(®.registry_entries);
|
||||
self.send_packet_now(&CRegistryData::new(®.registry_id, &merged))
|
||||
.await;
|
||||
continue;
|
||||
}
|
||||
self.send_packet_now(&CRegistryData::new(®.registry_id, ®.registry_entries))
|
||||
.await;
|
||||
}
|
||||
|
||||
@ -3,7 +3,10 @@ use pumpkin_data::damage::DamageType;
|
||||
use pumpkin_data::entity::EntityType;
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_util::loot_table::{LootBonusFormula, LootCondition, LootEntry, LootTable};
|
||||
pub use pumpkin_util::loot_table::{
|
||||
DynamicLootCondition, DynamicLootEntry, DynamicLootPool, DynamicLootTable, LootBonusFormula,
|
||||
LootCondition, LootEntry, LootPool, LootTable,
|
||||
};
|
||||
use pumpkin_util::random::{RandomImpl, xoroshiro128::Xoroshiro};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
@ -26,6 +29,74 @@ pub struct LootContextParameters {
|
||||
pub is_on_fire: Option<bool>,
|
||||
}
|
||||
|
||||
fn check_dynamic_condition(
|
||||
cond: &DynamicLootCondition,
|
||||
has_silk_touch: bool,
|
||||
has_shears: bool,
|
||||
fortune_level: i32,
|
||||
params: &LootContextParameters,
|
||||
rng: &mut Xoroshiro,
|
||||
) -> bool {
|
||||
match cond {
|
||||
DynamicLootCondition::None => true,
|
||||
DynamicLootCondition::SilkTouch => has_silk_touch,
|
||||
DynamicLootCondition::NoSilkTouch => !has_silk_touch,
|
||||
DynamicLootCondition::Shears => has_shears,
|
||||
DynamicLootCondition::SilkTouchOrShears => has_silk_touch || has_shears,
|
||||
DynamicLootCondition::NoSilkTouchOrShears => !has_silk_touch && !has_shears,
|
||||
DynamicLootCondition::KilledByPlayer => params.killed_by_player.unwrap_or(false),
|
||||
DynamicLootCondition::SurvivesExplosion => params
|
||||
.explosion_radius
|
||||
.is_none_or(|radius| rng.next_f32() <= 1.0 / radius),
|
||||
DynamicLootCondition::RandomChance { chance } => rng.next_f32() < *chance,
|
||||
DynamicLootCondition::RandomChanceWithEnchantedBonus {
|
||||
unenchanted_chance,
|
||||
enchanted_chance_base,
|
||||
enchanted_chance_per_level_above_first,
|
||||
} => {
|
||||
let chance = if fortune_level > 0 {
|
||||
enchanted_chance_base
|
||||
+ enchanted_chance_per_level_above_first * (fortune_level - 1) as f32
|
||||
} else {
|
||||
*unenchanted_chance
|
||||
};
|
||||
rng.next_f32() < chance
|
||||
}
|
||||
DynamicLootCondition::TableBonus { chances } => {
|
||||
let index = (fortune_level.max(0) as usize).min(chances.len().saturating_sub(1));
|
||||
chances
|
||||
.get(index)
|
||||
.is_some_and(|chance| rng.next_f32() < *chance)
|
||||
}
|
||||
DynamicLootCondition::AllOf(conditions) => conditions.iter().all(|c| {
|
||||
check_dynamic_condition(c, has_silk_touch, has_shears, fortune_level, params, rng)
|
||||
}),
|
||||
DynamicLootCondition::AnyOf(conditions) => conditions.iter().any(|c| {
|
||||
check_dynamic_condition(c, has_silk_touch, has_shears, fortune_level, params, rng)
|
||||
}),
|
||||
DynamicLootCondition::Inverted(cond) => {
|
||||
!check_dynamic_condition(cond, has_silk_touch, has_shears, fortune_level, params, rng)
|
||||
}
|
||||
DynamicLootCondition::EntityOnFire => params.is_on_fire.unwrap_or(false),
|
||||
DynamicLootCondition::WeatherCheck {
|
||||
raining,
|
||||
thundering,
|
||||
} => {
|
||||
if let Some(r) = raining
|
||||
&& params.is_raining != Some(*r)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(t) = thundering
|
||||
&& params.is_thundering != Some(*t)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_condition(
|
||||
cond: LootCondition,
|
||||
has_silk_touch: bool,
|
||||
@ -232,13 +303,207 @@ pub fn generate_loot_with_context(
|
||||
|
||||
pub use generate_loot as generate_chest_loot;
|
||||
|
||||
pub fn fill_chest_inventory(
|
||||
#[must_use]
|
||||
pub fn generate_dynamic_loot(table: &DynamicLootTable, seed: i64) -> Vec<ItemStack> {
|
||||
generate_dynamic_loot_with_context(table, seed, &LootContextParameters::default())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn generate_dynamic_loot_with_context(
|
||||
table: &DynamicLootTable,
|
||||
seed: i64,
|
||||
params: &LootContextParameters,
|
||||
) -> Vec<ItemStack> {
|
||||
let mut rng = Xoroshiro::from_seed(seed as u64);
|
||||
let mut items_to_place: Vec<ItemStack> = Vec::new();
|
||||
|
||||
let has_silk_touch = params.tool.as_ref().is_some_and(|tool| {
|
||||
pumpkin_data::Enchantment::from_name("silk_touch")
|
||||
.is_some_and(|e| tool.get_enchantment_level(e) > 0)
|
||||
});
|
||||
|
||||
let has_shears = params.tool.as_ref().is_some_and(|tool| {
|
||||
let name = tool
|
||||
.item
|
||||
.registry_key
|
||||
.strip_prefix("minecraft:")
|
||||
.unwrap_or(tool.item.registry_key);
|
||||
name == "shears"
|
||||
});
|
||||
|
||||
let fortune_level = params.tool.as_ref().map_or(0, |tool| {
|
||||
let fortune = pumpkin_data::Enchantment::from_name("fortune")
|
||||
.map_or(0, |e| tool.get_enchantment_level(e));
|
||||
let looting = pumpkin_data::Enchantment::from_name("looting")
|
||||
.map_or(0, |e| tool.get_enchantment_level(e));
|
||||
fortune.max(looting)
|
||||
});
|
||||
|
||||
for pool in &table.pools {
|
||||
if !check_dynamic_condition(
|
||||
&pool.condition,
|
||||
has_silk_touch,
|
||||
has_shears,
|
||||
fortune_level,
|
||||
params,
|
||||
&mut rng,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let eligible_entries: Vec<&DynamicLootEntry> = pool
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
check_dynamic_condition(
|
||||
&e.condition,
|
||||
has_silk_touch,
|
||||
has_shears,
|
||||
fortune_level,
|
||||
params,
|
||||
&mut rng,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if eligible_entries.is_empty() && pool.empty_weight == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let range = pool.max_rolls - pool.min_rolls;
|
||||
let rolls = pool.min_rolls
|
||||
+ if range > 0 {
|
||||
rng.next_bounded_i32(range + 1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
roll_dynamic_entries(
|
||||
rolls,
|
||||
pool.empty_weight,
|
||||
&eligible_entries,
|
||||
fortune_level,
|
||||
&mut rng,
|
||||
&mut items_to_place,
|
||||
);
|
||||
}
|
||||
|
||||
items_to_place
|
||||
}
|
||||
|
||||
fn roll_dynamic_entries(
|
||||
rolls: i32,
|
||||
empty_weight: i32,
|
||||
eligible_entries: &[&DynamicLootEntry],
|
||||
fortune_level: i32,
|
||||
rng: &mut Xoroshiro,
|
||||
items_to_place: &mut Vec<ItemStack>,
|
||||
) {
|
||||
for _ in 0..rolls {
|
||||
let entry_weight: i32 = eligible_entries.iter().map(|e| e.weight).sum();
|
||||
let total_weight = entry_weight + empty_weight;
|
||||
if total_weight == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut pick = rng.next_bounded_i32(total_weight);
|
||||
pick -= empty_weight;
|
||||
if pick < 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
for entry in eligible_entries {
|
||||
pick -= entry.weight;
|
||||
if pick < 0 {
|
||||
let count_range = entry.max_count - entry.min_count;
|
||||
let base_count = entry.min_count
|
||||
+ if count_range > 0 {
|
||||
rng.next_bounded_i32(count_range + 1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let mut final_count = base_count;
|
||||
if let Some(bonus) = entry.bonus_formula {
|
||||
final_count = apply_bonus_formula(final_count, bonus, fortune_level, rng);
|
||||
}
|
||||
|
||||
if final_count > 0 {
|
||||
let item_key = entry.item.strip_prefix("minecraft:").unwrap_or(&entry.item);
|
||||
if let Some(item) = Item::from_registry_key(item_key) {
|
||||
items_to_place.push(ItemStack::new(final_count as u8, item));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to either a compile-time static loot table or a datapack dynamic loot table.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum LootTableHandle {
|
||||
Static(&'static LootTable),
|
||||
Dynamic(std::sync::Arc<DynamicLootTable>),
|
||||
}
|
||||
|
||||
impl From<&'static LootTable> for LootTableHandle {
|
||||
fn from(t: &'static LootTable) -> Self {
|
||||
Self::Static(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::sync::Arc<DynamicLootTable>> for LootTableHandle {
|
||||
fn from(t: std::sync::Arc<DynamicLootTable>) -> Self {
|
||||
Self::Dynamic(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl LootTableHandle {
|
||||
#[must_use]
|
||||
pub fn generate_loot(&self, seed: i64) -> Vec<ItemStack> {
|
||||
self.generate_loot_with_context(seed, &LootContextParameters::default())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn generate_loot_with_context(
|
||||
&self,
|
||||
seed: i64,
|
||||
params: &LootContextParameters,
|
||||
) -> Vec<ItemStack> {
|
||||
generate_loot_from_handle(self, seed, params)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_loot_table(key: &str) -> Option<LootTableHandle> {
|
||||
let full_key = if key.contains(':') {
|
||||
key.to_string()
|
||||
} else {
|
||||
format!("minecraft:{key}")
|
||||
};
|
||||
pumpkin_data::loot_table::get_loot_table(key)
|
||||
.or_else(|| pumpkin_data::loot_table::get_loot_table(&full_key))
|
||||
.map(LootTableHandle::Static)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn generate_loot_from_handle(
|
||||
handle: &LootTableHandle,
|
||||
seed: i64,
|
||||
params: &LootContextParameters,
|
||||
) -> Vec<ItemStack> {
|
||||
match handle {
|
||||
LootTableHandle::Static(table) => generate_loot_with_context(table, seed, params),
|
||||
LootTableHandle::Dynamic(table) => generate_dynamic_loot_with_context(table, seed, params),
|
||||
}
|
||||
}
|
||||
|
||||
fn place_items_in_chest(
|
||||
inventory: &std::sync::Arc<dyn pumpkin_inventory::Inventory>,
|
||||
table: &LootTable,
|
||||
mut items_to_place: Vec<ItemStack>,
|
||||
seed: i64,
|
||||
) {
|
||||
let mut items_to_place = generate_loot(table, seed);
|
||||
|
||||
if items_to_place.is_empty() {
|
||||
return;
|
||||
}
|
||||
@ -266,6 +531,24 @@ pub fn fill_chest_inventory(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_chest_inventory_handle(
|
||||
inventory: &std::sync::Arc<dyn pumpkin_inventory::Inventory>,
|
||||
handle: &LootTableHandle,
|
||||
seed: i64,
|
||||
) {
|
||||
let items_to_place = generate_loot_from_handle(handle, seed, &LootContextParameters::default());
|
||||
place_items_in_chest(inventory, items_to_place, seed);
|
||||
}
|
||||
|
||||
pub fn fill_chest_inventory(
|
||||
inventory: &std::sync::Arc<dyn pumpkin_inventory::Inventory>,
|
||||
table: &LootTable,
|
||||
seed: i64,
|
||||
) {
|
||||
let items_to_place = generate_loot(table, seed);
|
||||
place_items_in_chest(inventory, items_to_place, seed);
|
||||
}
|
||||
|
||||
fn shuffle_and_split_items(
|
||||
result: &mut Vec<ItemStack>,
|
||||
available_slots: usize,
|
||||
|
||||
@ -7279,6 +7279,23 @@ impl World {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_loot_table(&self, key: &str) -> Option<crate::world::loot::LootTableHandle> {
|
||||
self.server.upgrade().map_or_else(
|
||||
|| {
|
||||
let full_key = if key.contains(':') {
|
||||
key.to_string()
|
||||
} else {
|
||||
format!("minecraft:{key}")
|
||||
};
|
||||
pumpkin_data::loot_table::get_loot_table(key)
|
||||
.or_else(|| pumpkin_data::loot_table::get_loot_table(&full_key))
|
||||
.map(crate::world::loot::LootTableHandle::Static)
|
||||
},
|
||||
|server| server.datapack_manager.get_loot_table(key),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn generate_loot(&self, loot_table: String) {
|
||||
let mut loot_event =
|
||||
crate::plugin::api::events::world::loot_generate::LootGenerateEvent::new(loot_table);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user