Alexander Medvedev 2026-04-08 11:01:29 +02:00
parent 161b90ed28
commit 878116ceff
5 changed files with 207 additions and 50 deletions

View File

@ -131,6 +131,90 @@ impl ToTokens for AlternativeEntryStruct {
} }
} }
#[derive(Deserialize, Clone, Debug)]
pub struct SequenceEntryStruct {
/// Child entries evaluated in order until the first condition failure.
children: Vec<LootPoolEntryStruct>,
}
impl ToTokens for SequenceEntryStruct {
/// Emits a `SequenceEntry { … }` struct literal token stream for code generation.
fn to_tokens(&self, tokens: &mut TokenStream) {
let children = self.children.iter().map(ToTokens::to_token_stream);
tokens.extend(quote! {
SequenceEntry {
children: &[#(#children),*],
}
});
}
}
/// Deserialized group loot entry that evaluates all children regardless of individual success.
#[derive(Deserialize, Clone, Debug)]
pub struct GroupEntryStruct {
/// Child entries all evaluated unconditionally.
children: Vec<LootPoolEntryStruct>,
}
impl ToTokens for GroupEntryStruct {
/// Emits a `GroupEntry { … }` struct literal token stream for code generation.
fn to_tokens(&self, tokens: &mut TokenStream) {
let children = self.children.iter().map(ToTokens::to_token_stream);
tokens.extend(quote! {
GroupEntry {
children: &[#(#children),*],
}
});
}
}
/// Deserialized dynamic loot entry holding the dynamic drop's namespaced key.
#[derive(Deserialize, Clone, Debug)]
pub struct DynamicEntryStruct {
/// Namespaced key identifying the dynamic drop type (e.g., `"minecraft:contents"`).
name: String,
}
impl ToTokens for DynamicEntryStruct {
/// Emits a `DynamicEntry { … }` struct literal token stream for code generation.
fn to_tokens(&self, tokens: &mut TokenStream) {
let name = LitStr::new(&self.name, Span::call_site());
tokens.extend(quote! {
DynamicEntry {
name: #name,
}
});
}
}
/// Deserialized tag loot entry holding the item tag's namespaced key.
#[derive(Deserialize, Clone, Debug)]
pub struct TagEntryStruct {
/// Namespaced item tag key (e.g., `"minecraft:planks"`).
name: String,
/// If `true`, yields one random item from the tag instead of all items.
#[serde(default)]
expand: bool,
}
impl ToTokens for TagEntryStruct {
/// Emits a `TagEntry { … }` struct literal token stream for code generation.
fn to_tokens(&self, tokens: &mut TokenStream) {
let name = LitStr::new(&self.name, Span::call_site());
let expand = self.expand;
tokens.extend(quote! {
TagEntry {
name: #name,
expand: #expand,
}
});
}
}
/// Deserialized variant of a loot pool entry, tagged by the `"type"` field. /// Deserialized variant of a loot pool entry, tagged by the `"type"` field.
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -141,28 +225,27 @@ pub enum LootPoolEntryTypesStruct {
/// Yields a specific item. /// Yields a specific item.
#[serde(rename = "minecraft:item")] #[serde(rename = "minecraft:item")]
Item(ItemEntryStruct), Item(ItemEntryStruct),
/// References another loot table. /// References another loot table by namespaced key.
#[serde(rename = "minecraft:loot_table")] #[serde(rename = "minecraft:loot_table")]
LootTable, LootTable,
/// Yields dynamically determined drops (e.g., shulker box contents). /// Yields dynamically determined drops (e.g., shulker box contents).
#[serde(rename = "minecraft:dynamic")] #[serde(rename = "minecraft:dynamic")]
Dynamic, Dynamic(DynamicEntryStruct),
/// Yields all items in a tag. /// Yields all items (or one random item) in an item tag.
#[serde(rename = "minecraft:tag")] #[serde(rename = "minecraft:tag")]
Tag, Tag(TagEntryStruct),
/// Tries each child entry until one succeeds. /// Tries each child entry until one succeeds.
#[serde(rename = "minecraft:alternatives")] #[serde(rename = "minecraft:alternatives")]
Alternatives(AlternativeEntryStruct), Alternatives(AlternativeEntryStruct),
/// Evaluates all children in order, stopping on the first failure. /// Evaluates all children in order, stopping on the first failure.
#[serde(rename = "minecraft:sequence")] #[serde(rename = "minecraft:sequence")]
Sequence, Sequence(SequenceEntryStruct),
/// Evaluates all children regardless of individual success. /// Evaluates all children regardless of individual success.
#[serde(rename = "minecraft:group")] #[serde(rename = "minecraft:group")]
Group, Group(GroupEntryStruct),
} }
impl ToTokens for LootPoolEntryTypesStruct { impl ToTokens for LootPoolEntryTypesStruct {
/// Emits the matching `LootPoolEntryTypes::*` token stream for code generation.
fn to_tokens(&self, tokens: &mut TokenStream) { fn to_tokens(&self, tokens: &mut TokenStream) {
match self { match self {
Self::Empty => { Self::Empty => {
@ -174,20 +257,21 @@ impl ToTokens for LootPoolEntryTypesStruct {
Self::LootTable => { Self::LootTable => {
tokens.extend(quote! { LootPoolEntryTypes::LootTable }); tokens.extend(quote! { LootPoolEntryTypes::LootTable });
} }
Self::Dynamic => { Self::Dynamic(entry) => {
// TODO
tokens.extend(quote! { LootPoolEntryTypes::Dynamic }); tokens.extend(quote! { LootPoolEntryTypes::Dynamic });
} }
Self::Tag => { Self::Tag(entry) => {
tokens.extend(quote! { LootPoolEntryTypes::Tag }); tokens.extend(quote! { LootPoolEntryTypes::Tag(#entry) });
} }
Self::Alternatives(alt) => { Self::Alternatives(alt) => {
tokens.extend(quote! { LootPoolEntryTypes::Alternatives(#alt) }); tokens.extend(quote! { LootPoolEntryTypes::Alternatives(#alt) });
} }
Self::Sequence => { Self::Sequence(seq) => {
tokens.extend(quote! { LootPoolEntryTypes::Sequence }); tokens.extend(quote! { LootPoolEntryTypes::Sequence(#seq) });
} }
Self::Group => { Self::Group(grp) => {
tokens.extend(quote! { LootPoolEntryTypes::Group }); tokens.extend(quote! { LootPoolEntryTypes::Group(#grp) });
} }
} }
} }
@ -208,7 +292,9 @@ pub enum LootConditionStruct {
AllOf, AllOf,
/// Passes with the given probability. /// Passes with the given probability.
#[serde(rename = "minecraft:random_chance")] #[serde(rename = "minecraft:random_chance")]
RandomChance, RandomChance {
chance: f32,
},
/// Passes with probability scaled by an enchantment level. /// Passes with probability scaled by an enchantment level.
#[serde(rename = "minecraft:random_chance_with_enchanted_bonus")] #[serde(rename = "minecraft:random_chance_with_enchanted_bonus")]
RandomChanceWithEnchantedBonus, RandomChanceWithEnchantedBonus,
@ -268,7 +354,9 @@ impl ToTokens for LootConditionStruct {
Self::Inverted => quote! { LootCondition::Inverted }, Self::Inverted => quote! { LootCondition::Inverted },
Self::AnyOf => quote! { LootCondition::AnyOf }, Self::AnyOf => quote! { LootCondition::AnyOf },
Self::AllOf => quote! { LootCondition::AllOf }, Self::AllOf => quote! { LootCondition::AllOf },
Self::RandomChance => quote! { LootCondition::RandomChance }, Self::RandomChance { chance } => {
quote! { LootCondition::RandomChance { chance: #chance } }
}
Self::RandomChanceWithEnchantedBonus => { Self::RandomChanceWithEnchantedBonus => {
quote! { LootCondition::RandomChanceWithEnchantedBonus } quote! { LootCondition::RandomChanceWithEnchantedBonus }
} }

View File

@ -41378,7 +41378,9 @@ impl Block {
content: LootPoolEntryTypes::Item(ItemEntry { content: LootPoolEntryTypes::Item(ItemEntry {
name: "minecraft:wheat_seeds", name: "minecraft:wheat_seeds",
}), }),
conditions: Some(&[LootCondition::RandomChance]), conditions: Some(&[LootCondition::RandomChance {
chance: 0.125f32,
}]),
functions: Some(&[ functions: Some(&[
LootFunction { LootFunction {
content: LootFunctionTypes::ApplyBonus { content: LootFunctionTypes::ApplyBonus {
@ -41468,7 +41470,9 @@ impl Block {
content: LootPoolEntryTypes::Item(ItemEntry { content: LootPoolEntryTypes::Item(ItemEntry {
name: "minecraft:wheat_seeds", name: "minecraft:wheat_seeds",
}), }),
conditions: Some(&[LootCondition::RandomChance]), conditions: Some(&[LootCondition::RandomChance {
chance: 0.125f32,
}]),
functions: Some(&[ functions: Some(&[
LootFunction { LootFunction {
content: LootFunctionTypes::ApplyBonus { content: LootFunctionTypes::ApplyBonus {
@ -166451,7 +166455,7 @@ impl Block {
content: LootPoolEntryTypes::Item(ItemEntry { content: LootPoolEntryTypes::Item(ItemEntry {
name: "minecraft:poisonous_potato", name: "minecraft:poisonous_potato",
}), }),
conditions: Some(&[LootCondition::RandomChance]), conditions: Some(&[LootCondition::RandomChance { chance: 0.02f32 }]),
functions: None, functions: None,
}], }],
rolls: LootNumberProviderTypes::Constant(1f32), rolls: LootNumberProviderTypes::Constant(1f32),
@ -201193,7 +201197,7 @@ impl Block {
}), }),
conditions: Some(&[ conditions: Some(&[
LootCondition::SurvivesExplosion, LootCondition::SurvivesExplosion,
LootCondition::RandomChance, LootCondition::RandomChance { chance: 0.125f32 },
]), ]),
functions: None, functions: None,
}, },
@ -201238,7 +201242,7 @@ impl Block {
}), }),
conditions: Some(&[ conditions: Some(&[
LootCondition::SurvivesExplosion, LootCondition::SurvivesExplosion,
LootCondition::RandomChance, LootCondition::RandomChance { chance: 0.125f32 },
]), ]),
functions: None, functions: None,
}, },
@ -201345,7 +201349,7 @@ impl Block {
}), }),
conditions: Some(&[ conditions: Some(&[
LootCondition::SurvivesExplosion, LootCondition::SurvivesExplosion,
LootCondition::RandomChance, LootCondition::RandomChance { chance: 0.125f32 },
]), ]),
functions: None, functions: None,
}, },
@ -201390,7 +201394,7 @@ impl Block {
}), }),
conditions: Some(&[ conditions: Some(&[
LootCondition::SurvivesExplosion, LootCondition::SurvivesExplosion,
LootCondition::RandomChance, LootCondition::RandomChance { chance: 0.125f32 },
]), ]),
functions: None, functions: None,
}, },

View File

@ -1441,7 +1441,7 @@ impl EntityType {
}], }],
rolls: LootNumberProviderTypes::Constant(1f32), rolls: LootNumberProviderTypes::Constant(1f32),
bonus_rolls: 0f32, bonus_rolls: 0f32,
conditions: Some(&[LootCondition::RandomChance]), conditions: Some(&[LootCondition::RandomChance { chance: 0.05f32 }]),
functions: None, functions: None,
}, },
]), ]),
@ -1774,7 +1774,10 @@ impl EntityType {
}, },
LootPool { LootPool {
entries: &[LootPoolEntry { entries: &[LootPoolEntry {
content: LootPoolEntryTypes::Tag, content: LootPoolEntryTypes::Tag(TagEntry {
name: "minecraft:creeper_drop_music_discs",
expand: true,
}),
conditions: None, conditions: None,
functions: None, functions: None,
}], }],
@ -5590,7 +5593,7 @@ impl EntityType {
}], }],
rolls: LootNumberProviderTypes::Constant(1f32), rolls: LootNumberProviderTypes::Constant(1f32),
bonus_rolls: 0f32, bonus_rolls: 0f32,
conditions: Some(&[LootCondition::RandomChance]), conditions: Some(&[LootCondition::RandomChance { chance: 0.05f32 }]),
functions: None, functions: None,
}, },
]), ]),
@ -5856,7 +5859,7 @@ impl EntityType {
}], }],
rolls: LootNumberProviderTypes::Constant(1f32), rolls: LootNumberProviderTypes::Constant(1f32),
bonus_rolls: 0f32, bonus_rolls: 0f32,
conditions: Some(&[LootCondition::RandomChance]), conditions: Some(&[LootCondition::RandomChance { chance: 0.05f32 }]),
functions: None, functions: None,
}, },
]), ]),
@ -7358,7 +7361,7 @@ impl EntityType {
}], }],
rolls: LootNumberProviderTypes::Constant(1f32), rolls: LootNumberProviderTypes::Constant(1f32),
bonus_rolls: 0f32, bonus_rolls: 0f32,
conditions: Some(&[LootCondition::RandomChance]), conditions: Some(&[LootCondition::RandomChance { chance: 0.05f32 }]),
functions: None, functions: None,
}, },
]), ]),

View File

@ -5,7 +5,7 @@ use serde::Deserialize;
use crate::random::RandomImpl; use crate::random::RandomImpl;
/// Represents a loot table used to generate items or rewards. /// Represents a loot table used to generate items or rewards.
#[derive(Clone, Copy, PartialEq, Debug)] #[derive(Clone, Copy, Debug)]
pub struct LootTable { pub struct LootTable {
/// The type of loot table (e.g., `Entity`, `Block`, `Chest`). /// The type of loot table (e.g., `Entity`, `Block`, `Chest`).
pub r#type: LootTableType, pub r#type: LootTableType,
@ -16,7 +16,7 @@ pub struct LootTable {
} }
/// Defines a loot pool containing entries, rolls, and conditions. /// Defines a loot pool containing entries, rolls, and conditions.
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, Debug)]
pub struct LootPool { pub struct LootPool {
/// Entries contained in this loot pool. /// Entries contained in this loot pool.
pub entries: &'static [LootPoolEntry], pub entries: &'static [LootPoolEntry],
@ -38,35 +38,42 @@ pub struct ItemEntry {
} }
/// Represents an alternative loot entry, which chooses one of its children. /// Represents an alternative loot entry, which chooses one of its children.
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, Debug)]
pub struct AlternativeEntry { pub struct AlternativeEntry {
/// Child entries for this alternative. /// Child entries for this alternative.
pub children: &'static [LootPoolEntry], pub children: &'static [LootPoolEntry],
} }
/// Types of entries that can exist in a loot pool. #[derive(Clone, Debug)]
#[derive(Clone, PartialEq, Debug)] pub struct TagEntry {
pub name: &'static str,
pub expand: bool,
}
#[derive(Clone, Debug)]
pub struct SequenceEntry {
pub children: &'static [LootPoolEntry],
}
#[derive(Clone, Debug)]
pub struct GroupEntry {
pub children: &'static [LootPoolEntry],
}
#[derive(Clone, Debug)]
pub enum LootPoolEntryTypes { pub enum LootPoolEntryTypes {
/// Empty entry; no item is generated.
Empty, Empty,
/// Entry that generates a specific item.
Item(ItemEntry), Item(ItemEntry),
/// Entry that references another loot table.
LootTable, LootTable,
/// Dynamic entry, resolved at runtime.
Dynamic, Dynamic,
/// Entry that references a tag. Tag(TagEntry),
Tag,
/// Entry that provides alternatives.
Alternatives(AlternativeEntry), Alternatives(AlternativeEntry),
/// Entry that executes sequentially. Sequence(SequenceEntry),
Sequence, Group(GroupEntry),
/// Entry that groups multiple entries.
Group,
} }
/// Conditions that can modify whether loot entries are applied. /// Conditions that can modify whether loot entries are applied.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub enum LootCondition { pub enum LootCondition {
/// Inverts the result of another condition. /// Inverts the result of another condition.
Inverted, Inverted,
@ -75,7 +82,7 @@ pub enum LootCondition {
/// Passes only if all the given conditions are met. /// Passes only if all the given conditions are met.
AllOf, AllOf,
/// Passes based on a random chance. /// Passes based on a random chance.
RandomChance, RandomChance { chance: f32 },
/// Passes based on a random chance modified by item enchantments. /// Passes based on a random chance modified by item enchantments.
RandomChanceWithEnchantedBonus, RandomChanceWithEnchantedBonus,
/// Checks properties of the entity (e.g. type, attributes). /// Checks properties of the entity (e.g. type, attributes).
@ -181,7 +188,7 @@ pub enum LootFunctionBonusParameter {
} }
/// Single entry in a loot pool with optional conditions and functions. /// Single entry in a loot pool with optional conditions and functions.
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, Debug)]
pub struct LootPoolEntry { pub struct LootPoolEntry {
/// The type of entry. /// The type of entry.
pub content: LootPoolEntryTypes, pub content: LootPoolEntryTypes,

View File

@ -1,4 +1,5 @@
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::tag;
use pumpkin_data::{Block, BlockState, item::Item}; use pumpkin_data::{Block, BlockState, item::Item};
use pumpkin_util::{ use pumpkin_util::{
loot_table::{ loot_table::{
@ -168,7 +169,33 @@ impl LootPoolEntryTypesExt for LootPoolEntryTypes {
} }
Self::LootTable => todo!(), Self::LootTable => todo!(),
Self::Dynamic => todo!(), Self::Dynamic => todo!(),
Self::Tag => todo!(), Self::Tag(tag) => {
let key = tag.name.strip_prefix("minecraft:").unwrap_or(tag.name);
let items = pumpkin_data::tag::get_tag_values(tag::RegistryKey::Item, key)
.unwrap_or_default()
.iter()
.filter_map(|registry_key| {
let item_key = registry_key
.strip_prefix("minecraft:")
.unwrap_or(registry_key);
Item::from_registry_key(item_key)
})
.collect::<Vec<_>>();
if items.is_empty() {
return Vec::new();
}
if tag.expand {
// Pick one random item from the tag
let index = rand::random_range(0..items.len() as i32) as usize;
vec![ItemStack::new(1, items[index])]
} else {
// Yield one stack of every item in the tag
items.iter().map(|&item| ItemStack::new(1, item)).collect()
}
}
Self::Alternatives(alternative_entry) => { Self::Alternatives(alternative_entry) => {
for entry in alternative_entry.children { for entry in alternative_entry.children {
if let Some(loot) = entry.get_loot(params) { if let Some(loot) = entry.get_loot(params) {
@ -177,8 +204,35 @@ impl LootPoolEntryTypesExt for LootPoolEntryTypes {
} }
Vec::new() Vec::new()
} }
Self::Sequence => todo!(), Self::Sequence(sequence_entry) => {
Self::Group => todo!(), let mut stacks = Vec::new();
for entry in sequence_entry.children {
if entry
.conditions
.as_ref()
.is_some_and(|c| !c.iter().all(|cond| cond.is_fulfilled(params)))
{
break;
}
match entry.get_loot(params) {
Some(loot) => stacks.extend(loot),
// get_loot returning None also signals failure — stop.
None => break,
}
}
stacks
}
Self::Group(group_entry) => {
let mut stacks = Vec::new();
for entry in group_entry.children {
if let Some(loot) = entry.get_loot(params) {
stacks.extend(loot);
}
}
stacks
}
} }
} }
} }
@ -197,6 +251,7 @@ impl LootConditionExt for LootCondition {
} }
true true
} }
Self::RandomChance { chance } => rand::rng().random::<f32>() < *chance,
Self::KilledByPlayer => params.killed_by_player.unwrap_or(false), Self::KilledByPlayer => params.killed_by_player.unwrap_or(false),
Self::BlockStateProperty { Self::BlockStateProperty {
block: _, block: _,