mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 17:19:15 -10:00
/loot rolls loot_table entries
The items engine, which /loot rolls tables with, had no case for a minecraft:loot_table entry, so a table that picks another table (the trial chambers' reward tables, many data pack and mod tables) dropped nothing: '/loot spawn ~ ~ ~ loot minecraft:chests/trial_chambers/reward' answered 'Dropped 0 items'. Chests were right: they roll through horizon_world::loot. A chosen loot_table entry now rolls its table (by id or inline) in full and applies its own functions to each stack (NestedLootTable); a table already rolling further out adds nothing (LootContext.pushVisitedElement). /loot also counted items where vanilla's CommandResponseTracker counts stacks: two stacks of two returned 4 instead of 2. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
1952b56c6c
commit
79ae7cfe19
3 files changed
+105
-3
No files matched your search
@@ -735,8 +735,9 @@ impl Game<'_> {
|
||||
let mut tracker = Tracker::default();
|
||||
for u in &used {
|
||||
let name = self.item_display_name(&u.item);
|
||||
tracker.track(&holder(name), u.count as i32);
|
||||
let _ = u;
|
||||
// CommandResponseTracker.track(drop): one per stack, so the
|
||||
// command returns how many stacks it used, not how many items.
|
||||
tracker.track(&holder(name), 1);
|
||||
}
|
||||
let counts: Vec<u32> = used.iter().map(|u| u.count).collect();
|
||||
let (message, total) = tracker.message(
|
||||
|
||||
@@ -362,6 +362,14 @@ fn registry_order(root: &Path, order: &[Arc<str>]) -> Vec<Arc<str>> {
|
||||
}
|
||||
/// Item ids standing for a loot table's `minecraft:dynamic` entries.
|
||||
const DYNAMIC: &str = "#dynamic:";
|
||||
/// The item id standing for a chosen `minecraft:loot_table` entry, whose
|
||||
/// table then rolls in full (`NestedLootTable`).
|
||||
const NESTED: &str = "#loot_table";
|
||||
thread_local! {
|
||||
/// The nested tables rolling now, outermost first: a table never rolls
|
||||
/// inside itself (`LootContext.pushVisitedElement`).
|
||||
static NESTED_ROLLS: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
}
|
||||
pub struct Items {
|
||||
pub items: HashMap<Arc<str>, ItemInfo>,
|
||||
pub order: Vec<Arc<str>>,
|
||||
@@ -1231,7 +1239,11 @@ impl Items {
|
||||
explosion_radius: None,
|
||||
this_entity: false,
|
||||
};
|
||||
ctx.evaluate(table, random)
|
||||
// LootTable.getRandomItemsRaw visits the table itself first.
|
||||
NESTED_ROLLS.with(|v| v.borrow_mut().push(id.to_owned()));
|
||||
let out = ctx.evaluate(table, random);
|
||||
NESTED_ROLLS.with(|v| v.borrow_mut().pop());
|
||||
out
|
||||
}
|
||||
/// The server random sequence a loot table draws from (`random_sequence`):
|
||||
/// vanilla's `LootContext.Builder.create` uses it instead of the level random.
|
||||
@@ -1646,6 +1658,21 @@ impl LootContext<'_> {
|
||||
));
|
||||
true
|
||||
}
|
||||
// NestedLootTable: chosen by its weight like an item; `choose`
|
||||
// then rolls its table (an id or inline) and applies its functions.
|
||||
"minecraft:loot_table" => {
|
||||
out.push((
|
||||
e["weight"].as_i64().unwrap_or(1) as i32,
|
||||
ItemStack {
|
||||
item: Arc::from(NESTED),
|
||||
count: 1,
|
||||
damage: 0,
|
||||
extra: None,
|
||||
},
|
||||
e.clone(),
|
||||
));
|
||||
true
|
||||
}
|
||||
"minecraft:empty" => {
|
||||
out.push((
|
||||
e["weight"].as_i64().unwrap_or(1) as i32,
|
||||
@@ -1691,9 +1718,46 @@ impl LootContext<'_> {
|
||||
chosen
|
||||
};
|
||||
let (_, mut stack, modifiers) = candidates.swap_remove(pick);
|
||||
if &*stack.item == NESTED {
|
||||
self.nested(&modifiers, random, produced);
|
||||
return;
|
||||
}
|
||||
self.modifiers(&modifiers, &mut stack, random);
|
||||
produced.push(stack);
|
||||
}
|
||||
/// `NestedLootTable.createItemStack`: the entry's table rolls in full
|
||||
/// (its pools and its own functions), then the entry's functions apply to
|
||||
/// each stack. A table already rolling further out adds nothing.
|
||||
fn nested<R: RandomSource>(
|
||||
&self,
|
||||
entry: &Value,
|
||||
random: &mut R,
|
||||
produced: &mut Vec<ItemStack>,
|
||||
) {
|
||||
let (name, table) = match &entry["value"] {
|
||||
Value::String(name) => (name.as_str(), self.items.loot.get(name.as_str())),
|
||||
inline @ Value::Object(_) => ("<inline>", Some(inline)),
|
||||
_ => return,
|
||||
};
|
||||
let Some(table) = table else { return };
|
||||
let entered = NESTED_ROLLS.with(|v| {
|
||||
let mut v = v.borrow_mut();
|
||||
let fresh = !v.iter().any(|n| n == name);
|
||||
if fresh {
|
||||
v.push(name.to_owned());
|
||||
}
|
||||
fresh
|
||||
});
|
||||
if !entered {
|
||||
return;
|
||||
}
|
||||
let stacks = self.evaluate(table, random);
|
||||
NESTED_ROLLS.with(|v| v.borrow_mut().pop());
|
||||
for mut stack in stacks {
|
||||
self.modifiers(&entry["modifier"], &mut stack, random);
|
||||
produced.push(stack);
|
||||
}
|
||||
}
|
||||
/// An enchantment function's `options` (`HolderSet`): a tag, a list or
|
||||
/// one id; every enchantment when absent (`registryAccess().listElements`).
|
||||
fn enchant_pool(e: &crate::enchant::Enchantments, options: &Value) -> Vec<Arc<str>> {
|
||||
@@ -2206,6 +2270,40 @@ mod tests {
|
||||
assert!(!items.item_in_tag("test:missing", "test:apple"));
|
||||
}
|
||||
|
||||
/// `NestedLootTable`: a `loot_table` entry, by id or inline, rolls that
|
||||
/// table in full and applies its own functions to each stack; a table
|
||||
/// never rolls inside itself.
|
||||
#[test]
|
||||
fn loot_table_entries_roll_their_table() {
|
||||
let mut items = Items::for_tests(HashMap::new());
|
||||
let diamond = json!({"pools":[{"rolls":1,"entries":[{"type":"minecraft:item","name":"minecraft:diamond"}]}]});
|
||||
items.loot = Arc::new(
|
||||
[
|
||||
("test:inner", diamond.clone()),
|
||||
("test:outer", json!({"pools":[{"rolls":1,"entries":[{"type":"minecraft:loot_table",
|
||||
"value":"test:inner","modifier":[{"type":"minecraft:set_count","count":3}]}]}]})),
|
||||
("test:inline", json!({"pools":[{"rolls":1,"entries":[{"type":"minecraft:loot_table",
|
||||
"value":diamond}]}]})),
|
||||
("test:loop", json!({"pools":[{"rolls":1,"entries":[{"type":"minecraft:loot_table",
|
||||
"value":"test:loop"}]},{"rolls":1,"entries":[{"type":"minecraft:item","name":"minecraft:stick"}]}]})),
|
||||
]
|
||||
.map(|(id, t)| (id.to_owned(), t))
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
let roll = |id: &str| {
|
||||
items
|
||||
.loot_table(id, false, None, &mut LegacyRandom::new(1))
|
||||
.into_iter()
|
||||
.map(|s| format!("{} {}", s.item, s.count))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(roll("test:outer"), ["minecraft:diamond 3"]);
|
||||
assert_eq!(roll("test:inline"), ["minecraft:diamond 1"]);
|
||||
// `test:loop` is already rolling: its own entry adds nothing.
|
||||
assert_eq!(roll("test:loop"), ["minecraft:stick 1"]);
|
||||
}
|
||||
|
||||
/// `Blocks.wallVariant`: a wall block without a table of its own drops
|
||||
/// from its standing block's (vanilla's tables: `blocks/torch` for
|
||||
/// `wall_torch`, `blocks/oak_sign` for `oak_wall_sign`...).
|
||||
|
||||
@@ -801,6 +801,9 @@ def items_and_loot():
|
||||
cmd('/loot replace block 3 206 3 container.8 loot minecraft:blocks/cobblestone'),
|
||||
cmd('/loot replace entity @s hotbar.5 loot minecraft:blocks/glass'),
|
||||
cmd('/loot spawn 3 210 3 loot minecraft:blocks/stone'),
|
||||
# NestedLootTable: the reward tables pick another table and roll it.
|
||||
cmd('/loot spawn 3 210 3 loot minecraft:chests/trial_chambers/reward'),
|
||||
cmd('/loot give @s loot minecraft:chests/trial_chambers/reward_ominous'),
|
||||
cmd('/loot give @s loot {pools:[{rolls:1,entries:[{type:"minecraft:item",name:"minecraft:apple",functions:[{type:"minecraft:set_count",count:4}]}]}]}'),
|
||||
cmd('/setblock 2 205 2 minecraft:stone'),
|
||||
cmd('/loot give @s mine 2 205 2'),
|
||||
|
||||
Reference in new issue
Block a user