feat: add compute command

This commit is contained in:
Alexander Medvedev 2026-09-19 15:38:19 +02:00
parent 8813176fdd
commit 12f5687629
19 changed files with 3673 additions and 222 deletions

View File

@ -107,8 +107,35 @@ pub static MOB_EFFECT_ARGUMENT: ResourceArgument =
.map(|value| value as &'static (dyn Any + Send + Sync)) .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 = pub static DAMAGE_TYPE_ARGUMENT: ResourceArgument =
ResourceArgument(DAMAGE_TYPE_REGISTRY, &|id: Identifier| { 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()) let dt = pumpkin_data::damage::DamageType::from_name(id.path())
.or_else(|| pumpkin_data::damage::DamageType::from_name(&id.to_string()))?; .or_else(|| pumpkin_data::damage::DamageType::from_name(&id.to_string()))?;
let idx = dt.id as usize; let idx = dt.id as usize;
@ -207,7 +234,13 @@ impl<S: crate::source::CommandSource> ArgumentType<S> for ResourceArgument {
let types = DAMAGE_TYPES let types = DAMAGE_TYPES
.iter() .iter()
.map(|dt| format!("minecraft:{}", dt.message_id)); .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 { } else {
Suggestions::empty() Suggestions::empty()
} }

View File

@ -75,3 +75,144 @@ pub struct LootTable {
pub type ChestLootEntry = LootEntry; pub type ChestLootEntry = LootEntry;
pub type ChestLootPool = LootPool; pub type ChestLootPool = LootPool;
pub type ChestLootTable = LootTable; 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(),
}
}
}

View File

@ -1055,11 +1055,11 @@ impl TrialSpawner {
} }
if let Some(equip) = equipment_loot_table 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 Some(living) = entity.get_living_entity()
{ {
let seed = rand::random::<i64>(); 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 let mut equipment = living
.entity_equipment .entity_equipment
.lock() .lock()
@ -1126,10 +1126,10 @@ impl TrialSpawner {
) { ) {
let mut dropped_any = false; let mut dropped_any = false;
if let Some(key) = ejecting_loot_table 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 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 { for stack in items {
world.drop_stack(&spawner_pos, stack); world.drop_stack(&spawner_pos, stack);
dropped_any = true; dropped_any = true;

View File

@ -460,9 +460,9 @@ pub fn drop_loot(
params: &LootContextParameters, params: &LootContextParameters,
) { ) {
let key = format!("minecraft:blocks/{}", block.name); 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 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() { if !items.is_empty() {
let mut event = crate::plugin::block::block_drop_item::BlockDropItemEvent { let mut event = crate::plugin::block::block_drop_item::BlockDropItemEvent {
block_pos: *pos, block_pos: *pos,

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,8 @@ use crate::command::errors::command_syntax_error::CommandSyntaxError;
use crate::command::errors::error_types::CommandErrorType; use crate::command::errors::error_types::CommandErrorType;
use crate::command::node::dispatcher::CommandDispatcher; use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult}; 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::entity::EntityBase;
use crate::world::loot::LootContextParameters; use crate::world::loot::LootContextParameters;
@ -39,6 +41,23 @@ static ERROR_INVALID_LOOT_TABLE: CommandErrorType<1> = CommandErrorType::new(
"Loot table '%s' not found", "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( static ERROR_NO_HELD_ITEMS: CommandErrorType<1> = CommandErrorType::new(
translation::java::COMMANDS_DROP_NO_HELD_ITEMS, translation::java::COMMANDS_DROP_NO_HELD_ITEMS,
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)?, ToolSource::OffHand => get_hand_item(context, false)?,
}; };
let loot_table = let loot_table = context
pumpkin_data::loot_table::get_loot_table(&key).ok_or_else(|| { .server()
.datapack_manager
.get_loot_table(&key)
.ok_or_else(|| {
ERROR_INVALID_LOOT_TABLE ERROR_INVALID_LOOT_TABLE
.create_without_context(TextComponent::text(loot_table_str.to_string())) .create_without_context(TextComponent::text(loot_table_str.to_string()))
})?; })?;
@ -361,7 +383,7 @@ impl CommandExecutor for LootExecutor {
..Default::default() ..Default::default()
}; };
let seed: i64 = rand::random(); let seed: i64 = rand::random();
drops = crate::world::loot::generate_loot_with_context(loot_table, seed, &params); drops = crate::world::loot::generate_loot_from_handle(&loot_table, seed, &params);
} }
Source::Loot => { Source::Loot => {
let loot_table_str = StringArgumentType::get(context, "loot_table")?; let loot_table_str = StringArgumentType::get(context, "loot_table")?;
@ -371,8 +393,11 @@ impl CommandExecutor for LootExecutor {
format!("minecraft:{loot_table_str}") format!("minecraft:{loot_table_str}")
}; };
let loot_table = let loot_table = context
pumpkin_data::loot_table::get_loot_table(&key).ok_or_else(|| { .server()
.datapack_manager
.get_loot_table(&key)
.ok_or_else(|| {
ERROR_INVALID_LOOT_TABLE ERROR_INVALID_LOOT_TABLE
.create_without_context(TextComponent::text(loot_table_str.to_string())) .create_without_context(TextComponent::text(loot_table_str.to_string()))
})?; })?;
@ -382,7 +407,7 @@ impl CommandExecutor for LootExecutor {
..Default::default() ..Default::default()
}; };
let seed: i64 = rand::random(); let seed: i64 = rand::random();
drops = crate::world::loot::generate_loot_with_context(loot_table, seed, &params); drops = crate::world::loot::generate_loot_from_handle(&loot_table, seed, &params);
} }
Source::Kill => { Source::Kill => {
let target_entities = EntityArgumentType::get_entities(context, "target_entity")?; let target_entities = EntityArgumentType::get_entities(context, "target_entity")?;
@ -402,10 +427,13 @@ impl CommandExecutor for LootExecutor {
for entity in &target_entities { for entity in &target_entities {
let resource_name = entity.get_entity().entity_type.resource_name; let resource_name = entity.get_entity().entity_type.resource_name;
let key = format!("minecraft:entities/{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(); let seed: i64 = rand::random();
drops.extend(crate::world::loot::generate_loot_with_context( drops.extend(crate::world::loot::generate_loot_from_handle(
loot_table, seed, &params, &loot_table,
seed,
&params,
)); ));
last_key = Some(key); last_key = Some(key);
} }
@ -427,8 +455,11 @@ impl CommandExecutor for LootExecutor {
let block = world.get_block(&pos); let block = world.get_block(&pos);
let key = format!("minecraft:blocks/{}", block.name); let key = format!("minecraft:blocks/{}", block.name);
let loot_table = let loot_table = context
pumpkin_data::loot_table::get_loot_table(&key).ok_or_else(|| { .server()
.datapack_manager
.get_loot_table(&key)
.ok_or_else(|| {
ERROR_NO_BLOCK_LOOT_TABLE ERROR_NO_BLOCK_LOOT_TABLE
.create_without_context(TextComponent::text(block.name.to_string())) .create_without_context(TextComponent::text(block.name.to_string()))
})?; })?;
@ -451,7 +482,7 @@ impl CommandExecutor for LootExecutor {
..Default::default() ..Default::default()
}; };
let seed: i64 = rand::random(); let seed: i64 = rand::random();
drops = crate::world::loot::generate_loot_with_context(loot_table, seed, &params); drops = crate::world::loot::generate_loot_from_handle(&loot_table, seed, &params);
table_id_for_callback = Some(key); table_id_for_callback = Some(key);
} }
} }
@ -589,7 +620,9 @@ impl CommandExecutor for LootExecutor {
fn add_sources(target: Target) -> Vec<LiteralArgumentBuilder> { fn add_sources(target: Target) -> Vec<LiteralArgumentBuilder> {
vec![ vec![
literal("fish").then( literal("fish").then(
argument("loot_table", StringArgumentType::SingleWord).then( argument("loot_table", StringArgumentType::SingleWord)
.suggests(LootTableSuggestionProvider)
.then(
argument("fish_pos", BlockPosArgumentType) argument("fish_pos", BlockPosArgumentType)
.executes(LootExecutor { .executes(LootExecutor {
target, target,
@ -620,7 +653,9 @@ fn add_sources(target: Target) -> Vec<LiteralArgumentBuilder> {
), ),
), ),
literal("loot").then( literal("loot").then(
argument("loot_table", StringArgumentType::SingleWord).executes(LootExecutor { argument("loot_table", StringArgumentType::SingleWord)
.suggests(LootTableSuggestionProvider)
.executes(LootExecutor {
target, target,
source: Source::Loot, source: Source::Loot,
}), }),

View File

@ -14,6 +14,7 @@ mod banlist;
mod bossbar; mod bossbar;
mod clear; mod clear;
mod clone; mod clone;
mod compute;
mod damage; mod damage;
mod data; mod data;
mod datapack; mod datapack;
@ -174,6 +175,7 @@ pub fn default_dispatcher(
effect::register(&mut dispatcher, registry); effect::register(&mut dispatcher, registry);
summon::register(&mut dispatcher, registry); summon::register(&mut dispatcher, registry);
swing::register(&mut dispatcher, registry); swing::register(&mut dispatcher, registry);
compute::register(&mut dispatcher, registry);
damage::register(&mut dispatcher, registry); damage::register(&mut dispatcher, registry);
rotate::register(&mut dispatcher, registry); rotate::register(&mut dispatcher, registry);
tellraw::register(&mut dispatcher, registry); tellraw::register(&mut dispatcher, registry);

View 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");
}
}
}

View 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 = &registry["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][..]));
}
}

View 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"));
}
}

View File

@ -1,4 +1,7 @@
pub mod context_provider_loader;
pub mod damage_type_loader;
pub mod function_loader; pub mod function_loader;
pub mod loot_table_loader;
pub mod recipe_loader; pub mod recipe_loader;
pub mod test_loader; pub mod test_loader;
@ -8,14 +11,20 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use tracing::{info, warn}; use tracing::{info, warn};
use pumpkin_data::damage::DamageType;
use pumpkin_data::registry::RegistryEntryData; use pumpkin_data::registry::RegistryEntryData;
use pumpkin_nbt::tag::NbtTag;
use pumpkin_nbt::{NbtCompound, nbt_compress::read_gzip_compound_tag}; use pumpkin_nbt::{NbtCompound, nbt_compress::read_gzip_compound_tag};
use pumpkin_protocol::codec::recipe::DynamicRecipe; use pumpkin_protocol::codec::recipe::DynamicRecipe;
use pumpkin_util::loot_table::DynamicLootTable;
use crate::command::context::command_source::CommandSource; use crate::command::context::command_source::CommandSource;
use crate::server::Server; use crate::server::Server;
use crate::server::recipe::RecipeManager; 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::{ use self::test_loader::{
TestInstance, TestInstanceRegistry, load_test_instances_from_dir, to_registry_entry, TestInstance, TestInstanceRegistry, load_test_instances_from_dir, to_registry_entry,
}; };
@ -63,6 +72,10 @@ pub struct DatapackManager {
functions: RwLock<HashMap<String, Arc<[String]>>>, functions: RwLock<HashMap<String, Arc<[String]>>>,
function_tags: RwLock<HashMap<String, Vec<String>>>, function_tags: RwLock<HashMap<String, Vec<String>>>,
test_instances: RwLock<TestInstanceRegistry>, 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( fn share_function_bodies(
@ -88,6 +101,10 @@ impl DatapackManager {
functions: RwLock::new(HashMap::new()), functions: RwLock::new(HashMap::new()),
function_tags: RwLock::new(HashMap::new()), function_tags: RwLock::new(HashMap::new()),
test_instances: 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_functions: HashMap<String, Vec<String>> = HashMap::new();
let mut all_function_tags: 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_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, // Embedded test instances are compile-time constants and always load,
// so on-disk packs can override them by id. // so on-disk packs can override them by id.
@ -110,60 +131,25 @@ impl DatapackManager {
if embedded_count > 0 { if embedded_count > 0 {
info!("Loaded {embedded_count} embedded test instance(s)"); info!("Loaded {embedded_count} embedded test instance(s)");
} }
if datapacks_dir.is_dir() { let mut acc = PackContentAccumulators {
match fs::read_dir(&datapacks_dir) { recipes: &mut all_recipes,
Ok(entries) => { functions: &mut all_functions,
for entry in entries.flatten() { function_tags: &mut all_function_tags,
let pack_path = entry.path(); test_instances: &mut all_test_instances,
let file_name = entry.file_name().to_string_lossy().to_string(); 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() { scan_datapacks_dir(
continue; &datapacks_dir,
} enabled_packs,
&mut acc,
let pack_id = format!("file/{file_name}"); &mut loaded_packs_vec,
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,
); );
info!( let damage_type_registry = build_damage_type_registry(all_damage_type_defs);
"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()
);
}
}
}
recipe_manager.set_recipes(all_recipes); recipe_manager.set_recipes(all_recipes);
*self *self
@ -183,6 +169,22 @@ impl DatapackManager {
.test_instances .test_instances
.write() .write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = all_test_instances; .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> { pub fn get_loaded_packs(&self) -> Vec<LoadedDatapack> {
@ -219,6 +221,162 @@ impl DatapackManager {
names 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. /// Returns datapack test instances in the protocol's synced-registry entry format.
/// The vanilla Test Instance Block renderer resolves required/padding/base rotation /// The vanilla Test Instance Block renderer resolves required/padding/base rotation
/// through this registry using the controller's `data.test` resource key. /// 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)) Ok((namespace, path))
} }
/// Loads recipes, functions, function tags, and test instances from a single fn build_damage_type_registry(
/// datapack directory, returning per-pack counts as `(recipes, functions, test_instances)`. 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( fn load_pack_contents(
pack_path: &Path, pack_path: &Path,
all_recipes: &mut Vec<DynamicRecipe>, acc: &mut PackContentAccumulators<'_>,
all_functions: &mut HashMap<String, Vec<String>>, ) -> PackContentCounts {
all_function_tags: &mut HashMap<String, Vec<String>>,
all_test_instances: &mut TestInstanceRegistry,
) -> (usize, usize, usize) {
let data_dir = pack_path.join("data"); let data_dir = pack_path.join("data");
let mut pack_recipe_count = 0; let mut counts = PackContentCounts::default();
let mut pack_function_count = 0;
let mut pack_test_instance_count = 0;
if data_dir.is_dir() if data_dir.is_dir()
&& let Ok(ns_entries) = fs::read_dir(&data_dir) && let Ok(ns_entries) = fs::read_dir(&data_dir)
@ -899,8 +1191,8 @@ fn load_pack_contents(
load_recipes_from_dir( load_recipes_from_dir(
&namespace, &namespace,
&recipe_dir, &recipe_dir,
all_recipes, acc.recipes,
&mut pack_recipe_count, &mut counts.recipes,
); );
} }
} }
@ -909,9 +1201,9 @@ fn load_pack_contents(
for fn_sub in ["function", "functions"] { for fn_sub in ["function", "functions"] {
let fn_dir = ns_path.join(fn_sub); let fn_dir = ns_path.join(fn_sub);
if fn_dir.is_dir() { if fn_dir.is_dir() {
let before = all_functions.len(); let before = acc.functions.len();
function_loader::load_functions_from_dir(&namespace, &fn_dir, all_functions); function_loader::load_functions_from_dir(&namespace, &fn_dir, acc.functions);
pack_function_count += all_functions.len() - before; counts.functions += acc.functions.len() - before;
} }
} }
@ -921,26 +1213,126 @@ fn load_pack_contents(
function_loader::load_function_tags_from_dir( function_loader::load_function_tags_from_dir(
&namespace, &namespace,
&tags_dir, &tags_dir,
all_function_tags, acc.function_tags,
); );
} }
// Load game test instances // Load game test instances
let test_instance_dir = ns_path.join("test_instance"); let test_instance_dir = ns_path.join("test_instance");
if test_instance_dir.is_dir() { if test_instance_dir.is_dir() {
pack_test_instance_count += load_test_instances_from_dir( counts.test_instances += load_test_instances_from_dir(
&namespace, &namespace,
&test_instance_dir, &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,
&lt_dir,
acc.loot_tables,
); );
} }
} }
} }
}
( counts
pack_recipe_count, }
pack_function_count,
pack_test_instance_count, #[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>) { fn read_pack_mcmeta(pack_path: &Path) -> (String, u32, Vec<KnownPackData>) {

View File

@ -2273,11 +2273,12 @@ impl LivingEntity {
fn drop_loot(&self, params: &LootContextParameters) { fn drop_loot(&self, params: &LootContextParameters) {
let resource_name = self.get_entity().entity_type.resource_name; let resource_name = self.get_entity().entity_type.resource_name;
let key = format!("minecraft:entities/{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 seed: i64 = rand::random();
let pos = self.entity.block_pos.load(); let pos = self.entity.block_pos.load();
for stack in crate::world::loot::generate_loot_with_context(loot_table, seed, params) { for stack in crate::world::loot::generate_loot_from_handle(&loot_table, seed, params) {
self.entity.world.load().drop_stack(&pos, stack); world.drop_stack(&pos, stack);
} }
} }
} }

View File

@ -15,8 +15,6 @@ use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use crate::entity::{Entity, player::Player}; 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 { pub(super) struct MinecartInventory {
items: RwLock<Vec<ItemStack>>, items: RwLock<Vec<ItemStack>>,
@ -98,7 +96,7 @@ impl MinecartInventory {
let Some((loot_table, seed)) = loot_table else { let Some((loot_table, seed)) = loot_table else {
return; return;
}; };
let Some(table) = get_loot_table(&loot_table) else { let Some(table) = crate::world::loot::get_loot_table(&loot_table) else {
*self *self
.loot_table .loot_table
.lock() .lock()
@ -107,7 +105,7 @@ impl MinecartInventory {
}; };
let inventory: Arc<dyn Inventory> = self.clone(); let inventory: Arc<dyn Inventory> = self.clone();
fill_chest_inventory(&inventory, table, seed); crate::world::loot::fill_chest_inventory_handle(&inventory, &table, seed);
} }
} }

View File

@ -43,8 +43,8 @@ fn get_archaeology_loot(is_sand: bool, location: BlockPos, world: &World) -> Ite
"minecraft:archaeology/trail_ruins_common" "minecraft:archaeology/trail_ruins_common"
}; };
if let Some(table) = pumpkin_data::loot_table::get_loot_table(loot_key) { if let Some(table) = world.get_loot_table(loot_key) {
let items = crate::world::loot::generate_loot(table, rand::random()); let items = table.generate_loot(rand::random());
if let Some(first) = items.into_iter().next() { if let Some(first) = items.into_iter().next() {
return first; return first;
} }

View File

@ -50,10 +50,10 @@ impl ItemBehaviour for HoeItem {
world.set_block_state(&location, result.new_state_id, BlockFlags::NOTIFY_ALL); world.set_block_state(&location, result.new_state_id, BlockFlags::NOTIFY_ALL);
if let Some(loot_key) = result.entry.loot 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 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 { for drop_stack in drops {
if result.entry.drop_strategy == Some(DropStrategy::ClickedFace) { if result.entry.drop_strategy == Some(DropStrategy::ClickedFace) {
world.drop_stack_from_face(&location, face, drop_stack); world.drop_stack_from_face(&location, face, drop_stack);

View File

@ -1,5 +1,7 @@
#[allow(clippy::wildcard_imports)] #[allow(clippy::wildcard_imports)]
use super::*; use super::*;
use bytes::Bytes;
use std::collections::HashMap;
impl JavaClient { impl JavaClient {
pub async fn handle_known_packs(&self, server: &Server) -> Option<PacketHandlerResult> { pub async fn handle_known_packs(&self, server: &Server) -> Option<PacketHandlerResult> {
@ -15,12 +17,48 @@ impl JavaClient {
let test_instance_entries = let test_instance_entries =
server.datapack_manager.get_test_instance_registry_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 || { 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 registry = Registry::get_synced(version);
let mut packets = Vec::new(); let mut packets = Vec::new();
let mut sent_dimension_type = false; let mut sent_dimension_type = false;
let mut sent_test_instance = false; let mut sent_test_instance = false;
let mut sent_damage_type = false;
for reg in &registry { for reg in &registry {
if reg.registry_id == "minecraft:dimension_type" { if reg.registry_id == "minecraft:dimension_type" {
@ -30,7 +68,24 @@ impl JavaClient {
if reg.registry_id == "minecraft:test_instance" { if reg.registry_id == "minecraft:test_instance" {
sent_test_instance = true; sent_test_instance = true;
let packet = CRegistryData::new(&reg.registry_id, &test_instance_entries); let packet = CRegistryData::new(&reg.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(
&reg.registry_entries,
custom_damage_type_map,
);
let packet = CRegistryData::new(&reg.registry_id, &merged_damage_types);
if let Ok(data) = Self::serialize_packet_for_version(&packet, version) { if let Ok(data) = Self::serialize_packet_for_version(&packet, version) {
packets.push(data); packets.push(data);
@ -51,7 +106,18 @@ impl JavaClient {
// client, so always provide it even when the static table omitted it. // client, so always provide it even when the static table omitted it.
if !sent_test_instance { if !sent_test_instance {
let test_instance = "minecraft:test_instance".to_string(); 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) { if let Ok(data) = Self::serialize_packet_for_version(&packet, version) {
packets.push(data); packets.push(data);
@ -59,21 +125,7 @@ impl JavaClient {
} }
if !sent_dimension_type { if !sent_dimension_type {
let dims = [ let dim_entries = get_fallback_dimension_type_entries();
&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_type = "minecraft:dimension_type".to_string(); let dim_type = "minecraft:dimension_type".to_string();
let packet = CRegistryData::new(&dim_type, &dim_entries); let packet = CRegistryData::new(&dim_type, &dim_entries);
@ -82,16 +134,7 @@ impl JavaClient {
} }
} }
let mut tags = Vec::new(); let tags = get_network_tag_keys(version);
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 packet = CUpdateTags::new(&tags); let packet = CUpdateTags::new(&tags);
if let Ok(data) = Self::serialize_packet_for_version(&packet, version) { if let Ok(data) = Self::serialize_packet_for_version(&packet, version) {
@ -99,23 +142,34 @@ impl JavaClient {
} }
packets 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
}

View File

@ -11,6 +11,14 @@ impl PendingConnection {
} }
let registry = pumpkin_data::registry::Registry::get_synced(version); let registry = pumpkin_data::registry::Registry::get_synced(version);
for reg in &registry { for reg in &registry {
if reg.registry_id == "minecraft:damage_type" {
let merged = server
.datapack_manager
.merge_damage_type_entries(&reg.registry_entries);
self.send_packet_now(&CRegistryData::new(&reg.registry_id, &merged))
.await;
continue;
}
self.send_packet_now(&CRegistryData::new(&reg.registry_id, &reg.registry_entries)) self.send_packet_now(&CRegistryData::new(&reg.registry_id, &reg.registry_entries))
.await; .await;
} }

View File

@ -3,7 +3,10 @@ use pumpkin_data::damage::DamageType;
use pumpkin_data::entity::EntityType; use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item; use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack; 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}; use pumpkin_util::random::{RandomImpl, xoroshiro128::Xoroshiro};
#[derive(Default, Clone)] #[derive(Default, Clone)]
@ -26,6 +29,74 @@ pub struct LootContextParameters {
pub is_on_fire: Option<bool>, 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( fn check_condition(
cond: LootCondition, cond: LootCondition,
has_silk_touch: bool, has_silk_touch: bool,
@ -232,13 +303,207 @@ pub fn generate_loot_with_context(
pub use generate_loot as generate_chest_loot; 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>, inventory: &std::sync::Arc<dyn pumpkin_inventory::Inventory>,
table: &LootTable, mut items_to_place: Vec<ItemStack>,
seed: i64, seed: i64,
) { ) {
let mut items_to_place = generate_loot(table, seed);
if items_to_place.is_empty() { if items_to_place.is_empty() {
return; 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( fn shuffle_and_split_items(
result: &mut Vec<ItemStack>, result: &mut Vec<ItemStack>,
available_slots: usize, available_slots: usize,

View File

@ -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) { pub fn generate_loot(&self, loot_table: String) {
let mut loot_event = let mut loot_event =
crate::plugin::api::events::world::loot_generate::LootGenerateEvent::new(loot_table); crate::plugin::api::events::world::loot_generate::LootGenerateEvent::new(loot_table);