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.
#[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 }
}

View File

@ -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,
},

View File

@ -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,
},
]),

View File

@ -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,

View File

@ -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::<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) => {
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::<f32>() < *chance,
Self::KilledByPlayer => params.killed_by_player.unwrap_or(false),
Self::BlockStateProperty {
block: _,