mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Run a mod's loot function on the items engine's drops
The items engine (block drops, entity drops, loot_table, interact_loot) answered a mod's loot condition but had no arm for a mod's loot function, so a block's own table could not carry one. Its modifiers now build the stack as the main loot engine's LootStack, call native_loot_function, and write back only what the hook changed: item, count, damage, enchantments, potion, ominous bottle amplifier, stew effects, names and the components the engine keeps as saved.
This commit is contained in:
1 parent
01e150e30d
commit
25f3bbd2ce
2 files changed
+271
-26
No files matched your search
@@ -1378,6 +1378,33 @@ impl LootContext<'_> {
|
||||
fn enchantment_level(&self, id: &str) -> u32 {
|
||||
self.held.map_or(0, |h| h.enchantment_level(id))
|
||||
}
|
||||
/// The context a mod's own loot kind sees in this engine: the block and
|
||||
/// the tool it has, and a seed from the roll's random. The entity, the
|
||||
/// damage, the player and the biome belong to the main loot engine's
|
||||
/// rolls; they are None here (docs/native-mods.md, API 133).
|
||||
fn native_loot_context<R: RandomSource>(
|
||||
&self,
|
||||
random: &mut R,
|
||||
) -> horizon_world::loot::NativeLootContext<'_> {
|
||||
horizon_world::loot::NativeLootContext {
|
||||
entity: None,
|
||||
damage: None,
|
||||
player: None,
|
||||
block: (!self.block.is_empty()).then_some(self.block),
|
||||
tool: self.held.map(|h| horizon_world::loot::NativeLootTool {
|
||||
item: h.item.to_string(),
|
||||
enchantments: h
|
||||
.enchantments()
|
||||
.iter()
|
||||
.map(|(id, lvl)| (id.to_string(), *lvl as i32))
|
||||
.collect(),
|
||||
}),
|
||||
luck: 0.0,
|
||||
biome: None,
|
||||
seed: random.next_long(),
|
||||
place: horizon_world::loot::current_native_loot_place(),
|
||||
}
|
||||
}
|
||||
fn number<R: RandomSource>(v: &Value, random: &mut R) -> f32 {
|
||||
match v {
|
||||
Value::Number(n) => n.as_f64().unwrap_or(0.0) as f32,
|
||||
@@ -1551,28 +1578,9 @@ impl LootContext<'_> {
|
||||
// does not have: held true (a double plant's other half is taken
|
||||
// as there). Unknown types fail, as in loot.rs.
|
||||
"minecraft:location_check" => true,
|
||||
// A mod's own kind: the condition hook answers. This engine
|
||||
// (block drops, entity drops, loot_table, interact_loot) has
|
||||
// no LootStack, so the function hook isn't here yet.
|
||||
// A mod's own kind: the condition hook answers.
|
||||
k if !c.is_null() && k.contains(':') && !k.starts_with("minecraft:") => {
|
||||
let ctx = horizon_world::loot::NativeLootContext {
|
||||
entity: None,
|
||||
damage: None,
|
||||
player: None,
|
||||
block: (!self.block.is_empty()).then_some(self.block),
|
||||
tool: self.held.map(|h| horizon_world::loot::NativeLootTool {
|
||||
item: h.item.to_string(),
|
||||
enchantments: h
|
||||
.enchantments()
|
||||
.iter()
|
||||
.map(|(id, lvl)| (id.to_string(), *lvl as i32))
|
||||
.collect(),
|
||||
}),
|
||||
luck: 0.0,
|
||||
biome: None,
|
||||
seed: random.next_long(),
|
||||
place: horizon_world::loot::current_native_loot_place(),
|
||||
};
|
||||
let ctx = self.native_loot_context(random);
|
||||
let mut holds = false;
|
||||
horizon_world::loot::native_loot_condition(k, c, &ctx, &mut holds);
|
||||
holds
|
||||
@@ -1870,12 +1878,155 @@ impl LootContext<'_> {
|
||||
stack.count = stack.count.min(max as u32);
|
||||
}
|
||||
}
|
||||
// A mod's own kind: the function hook changes the stack, as it
|
||||
// does in the main loot engine.
|
||||
k if k.contains(':') && !k.starts_with("minecraft:") => {
|
||||
let ctx = self.native_loot_context(random);
|
||||
mod_function(k, f, &ctx, stack, horizon_world::loot::native_loot_function);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The items engine's stack as the loot engine's (`LootStack`), for a mod's
|
||||
/// function hook: the parts of it the hook can change and the engine takes
|
||||
/// back. The components the engine keeps as saved travel as their JSON, as
|
||||
/// `set_components` writes them.
|
||||
fn loot_of(stack: &ItemStack) -> horizon_world::loot::LootStack {
|
||||
let mut components: Vec<(String, Value)> = Vec::new();
|
||||
if let Some(data) = stack.data() {
|
||||
for (id, value) in &data.other {
|
||||
components.push((
|
||||
id.clone(),
|
||||
horizon_client_core::scoreboard::tag_to_json(value),
|
||||
));
|
||||
}
|
||||
if let Some(name) = &data.custom_name {
|
||||
components.push((
|
||||
"minecraft:custom_name".to_owned(),
|
||||
horizon_client_core::scoreboard::tag_to_json(name),
|
||||
));
|
||||
}
|
||||
if let Some((material, pattern)) = &data.trim {
|
||||
components.push((
|
||||
"minecraft:trim".to_owned(),
|
||||
serde_json::json!({"material": &**material, "pattern": &**pattern}),
|
||||
));
|
||||
}
|
||||
}
|
||||
horizon_world::loot::LootStack {
|
||||
item: stack.item.to_string(),
|
||||
count: stack.count as i32,
|
||||
damage: stack.damage as i32,
|
||||
enchantments: stack
|
||||
.crafting_enchantments()
|
||||
.iter()
|
||||
.map(|(id, level)| (id.to_string(), *level as i32))
|
||||
.collect(),
|
||||
potion: stack.potion().map(str::to_owned),
|
||||
map: false,
|
||||
exploration: None,
|
||||
ominous_amplifier: stack
|
||||
.data()
|
||||
.and_then(|d| d.ominous_amplifier)
|
||||
.map(i32::from),
|
||||
stew_effects: stack
|
||||
.data()
|
||||
.map(|d| {
|
||||
d.stew_effects
|
||||
.iter()
|
||||
.map(|(effect, ticks)| (effect.to_string(), *ticks))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
names: Vec::new(),
|
||||
components,
|
||||
}
|
||||
}
|
||||
|
||||
/// What a mod's loot function left in its stack, applied to the engine's.
|
||||
/// Only what changed is written, so a field the hook did not touch keeps the
|
||||
/// engine's own form of it.
|
||||
fn apply_loot(
|
||||
before: &horizon_world::loot::LootStack,
|
||||
after: &horizon_world::loot::LootStack,
|
||||
stack: &mut ItemStack,
|
||||
) {
|
||||
if after.item != before.item {
|
||||
stack.item = Arc::from(after.item.as_str());
|
||||
}
|
||||
if after.count != before.count {
|
||||
stack.count = after.count.max(0) as u32;
|
||||
}
|
||||
if after.damage != before.damage {
|
||||
stack.damage = after.damage.max(0) as u32;
|
||||
}
|
||||
if after.enchantments != before.enchantments {
|
||||
let levels = after
|
||||
.enchantments
|
||||
.iter()
|
||||
.filter(|(_, level)| *level > 0)
|
||||
.map(|(id, level)| (Arc::from(id.as_str()), *level as u32))
|
||||
.collect();
|
||||
stack.set_crafting_enchantments(levels);
|
||||
}
|
||||
if after.potion != before.potion {
|
||||
stack.data_mut().potion = after.potion.as_deref().map(Arc::from);
|
||||
}
|
||||
if after.ominous_amplifier != before.ominous_amplifier {
|
||||
stack.data_mut().ominous_amplifier = after.ominous_amplifier.map(|a| a.clamp(0, 255) as u8);
|
||||
}
|
||||
if after.stew_effects != before.stew_effects {
|
||||
stack.data_mut().stew_effects = after
|
||||
.stew_effects
|
||||
.iter()
|
||||
.map(|(effect, ticks)| (Arc::from(effect.as_str()), *ticks))
|
||||
.collect();
|
||||
}
|
||||
// `SetNameFunction`'s components: the names the hook set.
|
||||
for (id, text) in &after.names {
|
||||
if !before.names.iter().any(|(i, t)| i == id && t == text) {
|
||||
stack.data_mut().set_loot_name(id, text);
|
||||
}
|
||||
}
|
||||
// `SetComponentsFunction`'s patch: the components the hook changed or
|
||||
// added are set, and the ones it dropped (gone from the stack) removed.
|
||||
for (id, value) in &after.components {
|
||||
if !before.components.iter().any(|(i, v)| i == id && v == value) {
|
||||
stack.data_mut().set_loot_component(id, value);
|
||||
}
|
||||
}
|
||||
for (id, _) in &before.components {
|
||||
if !after.components.iter().any(|(i, _)| i == id) {
|
||||
stack.data_mut().set_loot_component(id, &Value::Null);
|
||||
}
|
||||
}
|
||||
stack.tidy();
|
||||
}
|
||||
|
||||
/// `native_loot_function` with the items engine's stack: the stack as the
|
||||
/// loot engine's, the hook, and back what it changed. `hook` is the hook
|
||||
/// point (`horizon_world::loot::native_loot_function`); a test passes its own.
|
||||
fn mod_function(
|
||||
kind: &str,
|
||||
data: &Value,
|
||||
ctx: &horizon_world::loot::NativeLootContext<'_>,
|
||||
stack: &mut ItemStack,
|
||||
hook: impl FnOnce(
|
||||
&str,
|
||||
&Value,
|
||||
&horizon_world::loot::NativeLootContext<'_>,
|
||||
&mut horizon_world::loot::LootStack,
|
||||
),
|
||||
) {
|
||||
let before = loot_of(stack);
|
||||
let mut after = before.clone();
|
||||
hook(kind, data, ctx, &mut after);
|
||||
apply_loot(&before, &after, stack);
|
||||
}
|
||||
|
||||
/// `DispensibleContainerItem`: shared contents for hand use and dispensers.
|
||||
pub fn bucket_contents(
|
||||
item: &str,
|
||||
@@ -2159,6 +2310,96 @@ mod tests {
|
||||
assert_eq!(drops(Value::Null, "minecraft:stone"), 1);
|
||||
}
|
||||
|
||||
/// A mod's function kind changes the stack the items engine drops: what
|
||||
/// the hook leaves in the stack comes back (item, count, a component),
|
||||
/// through the conversion the block-drop path uses.
|
||||
#[test]
|
||||
fn a_mod_function_changes_the_stack_the_items_engine_drops() {
|
||||
let items = Items::for_tests(HashMap::new());
|
||||
let props = horizon_world::chunk::BlockProperties::new();
|
||||
let mut random = LegacyRandom::new(7);
|
||||
let block = ctx(&items, None, &props, "minecraft:stone");
|
||||
let native = block.native_loot_context(&mut random);
|
||||
let mut stack = ItemStack::new("minecraft:stone", 1);
|
||||
mod_function(
|
||||
"mymod:seven",
|
||||
&json!({"type": "mymod:seven"}),
|
||||
&native,
|
||||
&mut stack,
|
||||
|_, _, _, loot| {
|
||||
loot.item = "minecraft:diamond".to_owned();
|
||||
loot.count = 7;
|
||||
loot.components.push((
|
||||
"minecraft:custom_data".to_owned(),
|
||||
json!({"machine": "furnace"}),
|
||||
));
|
||||
},
|
||||
);
|
||||
assert_eq!(&*stack.item, "minecraft:diamond");
|
||||
assert_eq!(stack.count, 7);
|
||||
assert_eq!(
|
||||
stack.data().and_then(|d| d.other("minecraft:custom_data")),
|
||||
Some(&horizon_client_core::scoreboard::json_to_tag(
|
||||
&json!({"machine": "furnace"})
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/// A function that leaves the stack alone keeps its own form: the round
|
||||
/// trip through the loot engine's stack changes nothing.
|
||||
#[test]
|
||||
fn a_mod_function_that_changes_nothing_keeps_the_stacks_form() {
|
||||
let items = Items::for_tests(HashMap::new());
|
||||
let props = horizon_world::chunk::BlockProperties::new();
|
||||
let mut random = LegacyRandom::new(7);
|
||||
let block = ctx(&items, None, &props, "minecraft:stone");
|
||||
let native = block.native_loot_context(&mut random);
|
||||
let mut stack = ItemStack::new("minecraft:diamond_sword", 1);
|
||||
stack.enchant("minecraft:sharpness", 3);
|
||||
stack
|
||||
.data_mut()
|
||||
.set_loot_name("minecraft:custom_name", &json!("Excalibur"));
|
||||
let before = stack.clone();
|
||||
mod_function(
|
||||
"mymod:nothing",
|
||||
&json!({"type": "mymod:nothing"}),
|
||||
&native,
|
||||
&mut stack,
|
||||
|_, _, _, _| {},
|
||||
);
|
||||
assert_eq!(stack, before);
|
||||
}
|
||||
|
||||
/// The items engine reaches the function hook: a pool whose function kind
|
||||
/// is a mod's draws the hook's one `next_long` where a vanilla pool draws
|
||||
/// none. The hook is unclaimed here, so the drop keeps its count.
|
||||
#[test]
|
||||
fn a_mod_function_kind_reaches_the_items_engines_roll() {
|
||||
let items = Items::for_tests(HashMap::new());
|
||||
let props = horizon_world::chunk::BlockProperties::new();
|
||||
let vanilla = json!({"pools":[{"rolls":1,"entries":[
|
||||
{"type":"minecraft:item","name":"minecraft:stone"}]}]});
|
||||
let modded = json!({"pools":[{"rolls":1,"entries":[
|
||||
{"type":"minecraft:item","name":"minecraft:stone"}],
|
||||
"modifier":[{"type":"mymod:duplicate","factor":2}]}]});
|
||||
let mut a = LegacyRandom::new(100);
|
||||
let mut b = LegacyRandom::new(100);
|
||||
let _ = ctx(&items, None, &props, "minecraft:stone").evaluate(&vanilla, &mut a);
|
||||
let expected: Vec<i64> = (0..5).map(|_| b.next_long()).collect();
|
||||
let got: Vec<i64> = (0..5).map(|_| a.next_long()).collect();
|
||||
assert_eq!(got, expected, "a vanilla table shifted the random");
|
||||
|
||||
let mut c = LegacyRandom::new(100);
|
||||
let mut d = LegacyRandom::new(100);
|
||||
let drops = ctx(&items, None, &props, "minecraft:stone").evaluate(&modded, &mut c);
|
||||
let _ = d.next_long(); // the hook's seed
|
||||
let expected: Vec<i64> = (0..5).map(|_| d.next_long()).collect();
|
||||
let got: Vec<i64> = (0..5).map(|_| c.next_long()).collect();
|
||||
assert_eq!(got, expected, "the mod function did not draw its seed");
|
||||
assert_eq!(drops.len(), 1);
|
||||
assert_eq!(drops[0].count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fortune_silk_touch_and_looting() {
|
||||
let items = Items::for_tests(HashMap::new());
|
||||
|
||||
+9
-5
@@ -425,10 +425,14 @@ tool, the block, the luck, the biome and a seed from the roll's random. An uncla
|
||||
stack, an unclaimed condition fails, as unknown kinds do; a bare type name is minecraft's own.
|
||||
|
||||
The tables the items engine rolls (block drops, entity drops, `loot_table`, `interact_loot` and the
|
||||
built-in tables) answer the condition hook, with the tool and the block in the context; the function hook
|
||||
there follows when the stack types converge. In the items engine's context the `entity`, `damage`, `player`
|
||||
and `biome` fields are `None` and `luck` is 0; in the main loot engine's context the `tool` and `block`
|
||||
fields are `None`.
|
||||
built-in tables) answer both hooks, with the tool and the block in the context. The function hook there
|
||||
gets the stack as the main engine's (`LootStack`): its item, count, damage, enchantments (a book's stored
|
||||
ones), potion, ominous bottle amplifier, stew effects and the components the engine keeps as saved, as
|
||||
their JSON; what it changes comes back to the engine's stack, and a component it adds arrives with the
|
||||
drop. A function that asks for a map's structure search (`map`/`exploration`) changes nothing there: the
|
||||
items engine has no level to search. In the items engine's context the `entity`, `damage`, `player` and
|
||||
`biome` fields are `None` and `luck` is 0; in the main loot engine's context the `tool` and `block` fields
|
||||
are `None`.
|
||||
|
||||
Both engines' contexts carry `place`, where the roll happens: `origin` (a broken block's centre, a dead mob's
|
||||
position), `block_entity` (the broken block's entity as it was before the break, saved with its id, position and
|
||||
@@ -849,7 +853,7 @@ API 131 adds an item's use tick and release, as vanilla's `Item.onUseTick` and `
|
||||
|
||||
API 132 adds `native_inventory(uuid, kind)`: a player's inventory (`Player`: 41 slots, main, armour, off hand; `Ender`: 27), live when they are on the server, else from their file, never while they log in or are dead; `native_move_stack((from, kind, slot), expected, (to, kind))`: one stack moved, checked against `expected`, where it fits whole, or nothing moves (at most one side a file, written first); `native_send_map(uuid, map_id, colors)`: a 128 x 128 map picture sent to a player, for a filled map with that `map_id`. What an operator's `/invsee` and a price chart on a map need. Bundles need "hooks": 132.
|
||||
|
||||
API 133 adds a loot table's function and condition of a mod's own kind. The entry that names a `type` in a namespace outside `minecraft:` — where the loot roll used to silently skip it — reaches `native_loot_function(kind, data, ctx, stack)` or `native_loot_condition(kind, data, ctx, holds)` when the table rolls. The context brings the entity, the damage, the player, the tool, the block, the luck, the biome and a seed from the roll's random. The condition hook answers in the tables the items engine rolls (block drops, entity drops, loot_table, interact_loot), where entity, damage, player and biome are None; the function hook there follows when the stack types converge. An unclaimed function leaves the stack, an unclaimed condition fails, as unknown kinds do. Bundles that use them need "hooks": 133.
|
||||
API 133 adds a loot table's function and condition of a mod's own kind. The entry that names a `type` in a namespace outside `minecraft:` — where the loot roll used to silently skip it — reaches `native_loot_function(kind, data, ctx, stack)` or `native_loot_condition(kind, data, ctx, holds)` when the table rolls. The context brings the entity, the damage, the player, the tool, the block, the luck, the biome and a seed from the roll's random. Both hooks answer in the tables the items engine rolls (block drops, entity drops, loot_table, interact_loot), where entity, damage, player and biome are None; the function hook there gets the stack as the main engine's (its item, count, damage, enchantments, potion and saved components) and takes back what it changes, but a function that asks for a map's structure search changes nothing there. An unclaimed function leaves the stack, an unclaimed condition fails, as unknown kinds do. Bundles that use them need "hooks": 133.
|
||||
|
||||
API 134 adds a server's levels opening and closing, on the single player server thread and on a dedicated server: `native_level_load(dimension)` when a level opens, before any of its chunks (NeoForge's `LevelEvent.Load`), and `native_level_unload(dimension)` once it has closed, after its last save (`LevelEvent.Unload`). The level the world opens in comes before `WorldOpenReady`; the Nether and the End open when someone first goes there and close after 300 ticks with nobody in them. Every level still open closes when the server stops, before `WorldCloseFinal`, or when its open or its loop fails, so each load has one unload. Bundles that use them need "hooks": 134.
|
||||
|
||||
|
||||
Reference in new issue
Block a user