diff --git a/pumpkin-codegen/src/loot.rs b/pumpkin-codegen/src/loot.rs index 31d343bd0..3b1287627 100644 --- a/pumpkin-codegen/src/loot.rs +++ b/pumpkin-codegen/src/loot.rs @@ -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, +} + +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, +} + +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. #[derive(Deserialize, Clone, Debug)] #[serde(tag = "type")] @@ -141,28 +225,27 @@ pub enum LootPoolEntryTypesStruct { /// Yields a specific item. #[serde(rename = "minecraft:item")] Item(ItemEntryStruct), - /// References another loot table. + /// References another loot table by namespaced key. #[serde(rename = "minecraft:loot_table")] LootTable, /// Yields dynamically determined drops (e.g., shulker box contents). #[serde(rename = "minecraft:dynamic")] - Dynamic, - /// Yields all items in a tag. + Dynamic(DynamicEntryStruct), + /// Yields all items (or one random item) in an item tag. #[serde(rename = "minecraft:tag")] - Tag, + Tag(TagEntryStruct), /// Tries each child entry until one succeeds. #[serde(rename = "minecraft:alternatives")] Alternatives(AlternativeEntryStruct), /// Evaluates all children in order, stopping on the first failure. #[serde(rename = "minecraft:sequence")] - Sequence, + Sequence(SequenceEntryStruct), /// Evaluates all children regardless of individual success. #[serde(rename = "minecraft:group")] - Group, + Group(GroupEntryStruct), } impl ToTokens for LootPoolEntryTypesStruct { - /// Emits the matching `LootPoolEntryTypes::*` token stream for code generation. fn to_tokens(&self, tokens: &mut TokenStream) { match self { Self::Empty => { @@ -174,20 +257,21 @@ impl ToTokens for LootPoolEntryTypesStruct { Self::LootTable => { tokens.extend(quote! { LootPoolEntryTypes::LootTable }); } - Self::Dynamic => { + Self::Dynamic(entry) => { + // TODO tokens.extend(quote! { LootPoolEntryTypes::Dynamic }); } - Self::Tag => { - tokens.extend(quote! { LootPoolEntryTypes::Tag }); + Self::Tag(entry) => { + tokens.extend(quote! { LootPoolEntryTypes::Tag(#entry) }); } Self::Alternatives(alt) => { tokens.extend(quote! { LootPoolEntryTypes::Alternatives(#alt) }); } - Self::Sequence => { - tokens.extend(quote! { LootPoolEntryTypes::Sequence }); + Self::Sequence(seq) => { + tokens.extend(quote! { LootPoolEntryTypes::Sequence(#seq) }); } - Self::Group => { - tokens.extend(quote! { LootPoolEntryTypes::Group }); + Self::Group(grp) => { + tokens.extend(quote! { LootPoolEntryTypes::Group(#grp) }); } } } @@ -208,7 +292,9 @@ pub enum LootConditionStruct { AllOf, /// Passes with the given probability. #[serde(rename = "minecraft:random_chance")] - RandomChance, + RandomChance { + chance: f32, + }, /// Passes with probability scaled by an enchantment level. #[serde(rename = "minecraft:random_chance_with_enchanted_bonus")] RandomChanceWithEnchantedBonus, @@ -268,7 +354,9 @@ impl ToTokens for LootConditionStruct { Self::Inverted => quote! { LootCondition::Inverted }, Self::AnyOf => quote! { LootCondition::AnyOf }, Self::AllOf => quote! { LootCondition::AllOf }, - Self::RandomChance => quote! { LootCondition::RandomChance }, + Self::RandomChance { chance } => { + quote! { LootCondition::RandomChance { chance: #chance } } + } Self::RandomChanceWithEnchantedBonus => { quote! { LootCondition::RandomChanceWithEnchantedBonus } } diff --git a/pumpkin-data/src/generated/block.rs b/pumpkin-data/src/generated/block.rs index fe486ef0d..932010e41 100644 --- a/pumpkin-data/src/generated/block.rs +++ b/pumpkin-data/src/generated/block.rs @@ -41378,7 +41378,9 @@ impl Block { content: LootPoolEntryTypes::Item(ItemEntry { name: "minecraft:wheat_seeds", }), - conditions: Some(&[LootCondition::RandomChance]), + conditions: Some(&[LootCondition::RandomChance { + chance: 0.125f32, + }]), functions: Some(&[ LootFunction { content: LootFunctionTypes::ApplyBonus { @@ -41468,7 +41470,9 @@ impl Block { content: LootPoolEntryTypes::Item(ItemEntry { name: "minecraft:wheat_seeds", }), - conditions: Some(&[LootCondition::RandomChance]), + conditions: Some(&[LootCondition::RandomChance { + chance: 0.125f32, + }]), functions: Some(&[ LootFunction { content: LootFunctionTypes::ApplyBonus { @@ -166451,7 +166455,7 @@ impl Block { content: LootPoolEntryTypes::Item(ItemEntry { name: "minecraft:poisonous_potato", }), - conditions: Some(&[LootCondition::RandomChance]), + conditions: Some(&[LootCondition::RandomChance { chance: 0.02f32 }]), functions: None, }], rolls: LootNumberProviderTypes::Constant(1f32), @@ -201193,7 +201197,7 @@ impl Block { }), conditions: Some(&[ LootCondition::SurvivesExplosion, - LootCondition::RandomChance, + LootCondition::RandomChance { chance: 0.125f32 }, ]), functions: None, }, @@ -201238,7 +201242,7 @@ impl Block { }), conditions: Some(&[ LootCondition::SurvivesExplosion, - LootCondition::RandomChance, + LootCondition::RandomChance { chance: 0.125f32 }, ]), functions: None, }, @@ -201345,7 +201349,7 @@ impl Block { }), conditions: Some(&[ LootCondition::SurvivesExplosion, - LootCondition::RandomChance, + LootCondition::RandomChance { chance: 0.125f32 }, ]), functions: None, }, @@ -201390,7 +201394,7 @@ impl Block { }), conditions: Some(&[ LootCondition::SurvivesExplosion, - LootCondition::RandomChance, + LootCondition::RandomChance { chance: 0.125f32 }, ]), functions: None, }, diff --git a/pumpkin-data/src/generated/entity_type.rs b/pumpkin-data/src/generated/entity_type.rs index 0e83aed43..adb466c0e 100644 --- a/pumpkin-data/src/generated/entity_type.rs +++ b/pumpkin-data/src/generated/entity_type.rs @@ -1441,7 +1441,7 @@ impl EntityType { }], rolls: LootNumberProviderTypes::Constant(1f32), bonus_rolls: 0f32, - conditions: Some(&[LootCondition::RandomChance]), + conditions: Some(&[LootCondition::RandomChance { chance: 0.05f32 }]), functions: None, }, ]), @@ -1774,7 +1774,10 @@ impl EntityType { }, LootPool { entries: &[LootPoolEntry { - content: LootPoolEntryTypes::Tag, + content: LootPoolEntryTypes::Tag(TagEntry { + name: "minecraft:creeper_drop_music_discs", + expand: true, + }), conditions: None, functions: None, }], @@ -5590,7 +5593,7 @@ impl EntityType { }], rolls: LootNumberProviderTypes::Constant(1f32), bonus_rolls: 0f32, - conditions: Some(&[LootCondition::RandomChance]), + conditions: Some(&[LootCondition::RandomChance { chance: 0.05f32 }]), functions: None, }, ]), @@ -5856,7 +5859,7 @@ impl EntityType { }], rolls: LootNumberProviderTypes::Constant(1f32), bonus_rolls: 0f32, - conditions: Some(&[LootCondition::RandomChance]), + conditions: Some(&[LootCondition::RandomChance { chance: 0.05f32 }]), functions: None, }, ]), @@ -7358,7 +7361,7 @@ impl EntityType { }], rolls: LootNumberProviderTypes::Constant(1f32), bonus_rolls: 0f32, - conditions: Some(&[LootCondition::RandomChance]), + conditions: Some(&[LootCondition::RandomChance { chance: 0.05f32 }]), functions: None, }, ]), diff --git a/pumpkin-util/src/loot_table.rs b/pumpkin-util/src/loot_table.rs index 564d266c9..4d385cabe 100644 --- a/pumpkin-util/src/loot_table.rs +++ b/pumpkin-util/src/loot_table.rs @@ -5,7 +5,7 @@ use serde::Deserialize; use crate::random::RandomImpl; /// Represents a loot table used to generate items or rewards. -#[derive(Clone, Copy, PartialEq, Debug)] +#[derive(Clone, Copy, Debug)] pub struct LootTable { /// The type of loot table (e.g., `Entity`, `Block`, `Chest`). pub r#type: LootTableType, @@ -16,7 +16,7 @@ pub struct LootTable { } /// Defines a loot pool containing entries, rolls, and conditions. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Debug)] pub struct LootPool { /// Entries contained in this loot pool. pub entries: &'static [LootPoolEntry], @@ -38,35 +38,42 @@ pub struct ItemEntry { } /// Represents an alternative loot entry, which chooses one of its children. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Debug)] pub struct AlternativeEntry { /// Child entries for this alternative. pub children: &'static [LootPoolEntry], } -/// Types of entries that can exist in a loot pool. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, 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 { - /// Empty entry; no item is generated. Empty, - /// Entry that generates a specific item. Item(ItemEntry), - /// Entry that references another loot table. LootTable, - /// Dynamic entry, resolved at runtime. Dynamic, - /// Entry that references a tag. - Tag, - /// Entry that provides alternatives. + Tag(TagEntry), Alternatives(AlternativeEntry), - /// Entry that executes sequentially. - Sequence, - /// Entry that groups multiple entries. - Group, + Sequence(SequenceEntry), + Group(GroupEntry), } /// Conditions that can modify whether loot entries are applied. -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Debug)] pub enum LootCondition { /// Inverts the result of another condition. Inverted, @@ -75,7 +82,7 @@ pub enum LootCondition { /// Passes only if all the given conditions are met. AllOf, /// Passes based on a random chance. - RandomChance, + RandomChance { chance: f32 }, /// Passes based on a random chance modified by item enchantments. RandomChanceWithEnchantedBonus, /// 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. -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Debug)] pub struct LootPoolEntry { /// The type of entry. pub content: LootPoolEntryTypes, diff --git a/pumpkin/src/world/loot.rs b/pumpkin/src/world/loot.rs index 08384f0bf..9a2ad0a86 100644 --- a/pumpkin/src/world/loot.rs +++ b/pumpkin/src/world/loot.rs @@ -1,4 +1,5 @@ use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::tag; use pumpkin_data::{Block, BlockState, item::Item}; use pumpkin_util::{ loot_table::{ @@ -168,7 +169,33 @@ impl LootPoolEntryTypesExt for LootPoolEntryTypes { } Self::LootTable => 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::>(); + + 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) => { for entry in alternative_entry.children { if let Some(loot) = entry.get_loot(params) { @@ -177,8 +204,35 @@ impl LootPoolEntryTypesExt for LootPoolEntryTypes { } Vec::new() } - Self::Sequence => todo!(), - Self::Group => todo!(), + Self::Sequence(sequence_entry) => { + 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 } + Self::RandomChance { chance } => rand::rng().random::() < *chance, Self::KilledByPlayer => params.killed_by_player.unwrap_or(false), Self::BlockStateProperty { block: _,