mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #11099 from Delete228/native-living-item-use-upstream
Native mods: item use after a mob passes interaction
This commit is contained in:
8 files changed
+465
-10
No files matched your search
@@ -160,6 +160,8 @@ mod interaction_shapes;
|
||||
mod interactions;
|
||||
mod item_areas;
|
||||
mod item_draw;
|
||||
#[cfg(test)]
|
||||
mod item_interact_mob_tests;
|
||||
mod item_particles;
|
||||
mod item_use;
|
||||
mod jigsaw_blocks;
|
||||
@@ -6704,7 +6706,35 @@ impl Game<'_> {
|
||||
|| self.interact_cart(target)
|
||||
|| self.interact_mob(target)?
|
||||
// A guest's Interact naming a knot's id goes to `use_knot` before this.
|
||||
|| target.is_none() && self.interact_knot())
|
||||
|| target.is_none() && self.interact_knot()
|
||||
|| self.item_interact_mob(target))
|
||||
}
|
||||
/// The `ItemStack.interactLivingEntity` fallback in `Player.interactOn`,
|
||||
/// currently for native Mob entries after the entity's own action passed.
|
||||
fn item_interact_mob(&mut self, target: Option<InteractTarget>) -> bool {
|
||||
if self.spectator() {
|
||||
return false;
|
||||
}
|
||||
let Some(index) = self.mob_interaction_target(target) else {
|
||||
return false;
|
||||
};
|
||||
let mob = &self.mobs[index];
|
||||
if mob.removed || !mob.alive() {
|
||||
return false;
|
||||
}
|
||||
let (entity, kind) = (
|
||||
mob.id as i32,
|
||||
horizon_client::command::args::normalize(&mob.kind.id),
|
||||
);
|
||||
let Some(stack) = self.held().filter(|stack| stack.count > 0).cloned() else {
|
||||
return false;
|
||||
};
|
||||
let mut swing = None;
|
||||
self.native_item_interact_mob(entity, &kind, Some(&stack), &mut swing);
|
||||
if swing == Some(true) {
|
||||
self.swing_arm();
|
||||
}
|
||||
swing.is_some()
|
||||
}
|
||||
/// `ItemStack.applyAfterUseComponentSideEffects`: a used stack with a
|
||||
/// `use_cooldown` component starts its entry (`UseCooldown.apply`). A
|
||||
|
||||
@@ -5605,11 +5605,10 @@ impl Game<'_> {
|
||||
};
|
||||
self.audio.play(sound, Some(at), 1.0, 1.0);
|
||||
}
|
||||
/// Right click on a mob (Mob.interact / mobInteract), on the mob under the
|
||||
/// crosshair or on the one a guest's interact packet named: returns true
|
||||
/// when consumed.
|
||||
pub(super) fn interact_mob(&mut self, target: Option<InteractTarget>) -> Result<bool> {
|
||||
let Some((index, distance, _)) = (match target {
|
||||
/// The mob a normal interaction can reach: a packet's exact validated ID,
|
||||
/// or the crosshair's mob before its blocking shape.
|
||||
pub(super) fn mob_interaction_target(&self, target: Option<InteractTarget>) -> Option<usize> {
|
||||
let (index, distance, _) = (match target {
|
||||
// The id the packet named, under the filters the crosshair pick
|
||||
// applies (`mob_hit`): a mob in its death animation is not clicked
|
||||
// by either path, so naming one in a packet must not reach the
|
||||
@@ -5621,9 +5620,7 @@ impl Game<'_> {
|
||||
})
|
||||
.map(|i| (i, 0.0, [0.0; 3])),
|
||||
None => self.mob_hit(),
|
||||
}) else {
|
||||
return Ok(false);
|
||||
};
|
||||
})?;
|
||||
// Only a client's own pick is stopped by a block: the range check passed.
|
||||
if target.is_none()
|
||||
&& let Some(hit) = self.pick_unfiltered()
|
||||
@@ -5634,9 +5631,18 @@ impl Game<'_> {
|
||||
+ (hit.point[2] - eye[2]).powi(2))
|
||||
.sqrt();
|
||||
if d < distance {
|
||||
return Ok(false);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(index)
|
||||
}
|
||||
/// Right click on a mob (Mob.interact / mobInteract), on the mob under the
|
||||
/// crosshair or on the one a guest's interact packet named: returns true
|
||||
/// when consumed.
|
||||
pub(super) fn interact_mob(&mut self, target: Option<InteractTarget>) -> Result<bool> {
|
||||
let Some(index) = self.mob_interaction_target(target) else {
|
||||
return Ok(false);
|
||||
};
|
||||
// PlayerInteractTrigger: the name tag as it was before it named the mob.
|
||||
let used = self.held().cloned();
|
||||
if self.name_mob(index) {
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
//! Run alone with private prepared resources: installs process-wide trait mods.
|
||||
use super::*;
|
||||
use crate::mod_api::{self, Flow, Hooks, Host, InteractEntityEvent, ItemInteractMobEvent};
|
||||
use horizon_client::{
|
||||
chunk::Chunk,
|
||||
merchant::{ItemCost, MerchantOffer, Trader},
|
||||
mobs::Mob,
|
||||
};
|
||||
use std::sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, AtomicU8, Ordering},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Probe {
|
||||
calls: Mutex<Vec<(i32, String, String)>>,
|
||||
answer: AtomicU8,
|
||||
claim_entity: AtomicBool,
|
||||
discard: AtomicBool,
|
||||
}
|
||||
impl Hooks for Arc<Probe> {
|
||||
fn interact_entity(&self, _: &mut dyn Host, _: &mut InteractEntityEvent<'_>) -> Flow {
|
||||
if self.claim_entity.load(Ordering::Relaxed) {
|
||||
Flow::Cancel
|
||||
} else {
|
||||
Flow::Continue
|
||||
}
|
||||
}
|
||||
fn item_interact_mob(
|
||||
&self,
|
||||
host: &mut dyn Host,
|
||||
event: &mut ItemInteractMobEvent<'_>,
|
||||
) -> Option<bool> {
|
||||
let stack = event.stack.expect("only a nonempty item has this fallback");
|
||||
assert!(stack.count > 0);
|
||||
self.calls
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((event.entity, event.kind.into(), stack.item.to_string()));
|
||||
let answer = self.answer.load(Ordering::Relaxed);
|
||||
if answer != 0 && self.discard.load(Ordering::Relaxed) {
|
||||
assert!(host.remove_mob("minecraft:overworld", event.entity as u32));
|
||||
}
|
||||
match answer {
|
||||
0 => None,
|
||||
1 => Some(false),
|
||||
_ => Some(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn setup(game: &mut Game<'_>, probe: &Probe, kind: &str, id: u32) {
|
||||
game.mobs.clear();
|
||||
let kind = game.res.mob_types().get(kind).unwrap().clone();
|
||||
game.mobs
|
||||
.push(Mob::new(id, kind, Vec3::new(0.5, 64.0, 2.5), 0.0));
|
||||
game.station = None;
|
||||
game.screen = Screen::Playing;
|
||||
game.inventory[0] = Some(ItemStack::new("minecraft:stick", 2));
|
||||
game.offhand = Some(ItemStack::new("minecraft:paper", 3));
|
||||
game.swing = Default::default();
|
||||
game.swings_started = 0;
|
||||
game.tick_hit = None;
|
||||
game.tick_mob = None;
|
||||
game.riding = None;
|
||||
probe.calls.lock().unwrap().clear();
|
||||
probe.answer.store(1, Ordering::Relaxed);
|
||||
probe.claim_entity.store(false, Ordering::Relaxed);
|
||||
probe.discard.store(false, Ordering::Relaxed);
|
||||
}
|
||||
fn target(id: i32) -> Option<InteractTarget> {
|
||||
Some(InteractTarget {
|
||||
id,
|
||||
location: [0.0, 1.0, 0.0],
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "private prepared resources; process-wide mods, run this test alone"]
|
||||
fn actual_item_interact_mob_fallback_priority_and_filters() {
|
||||
let root = PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
root.clone(),
|
||||
None,
|
||||
&root,
|
||||
&settings,
|
||||
Instant::now(),
|
||||
false,
|
||||
true,
|
||||
&Progress::new(),
|
||||
&mut |_, _| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut options = Options::parse(std::iter::empty()).unwrap().unwrap();
|
||||
options.resources = root.clone();
|
||||
options.execution_role = execution_role::ExecutionRole::IntegratedServer;
|
||||
let probe = Arc::new(Probe::default());
|
||||
assert!(
|
||||
mod_api::MODS
|
||||
.set(Arc::from(vec![Box::new(probe.clone()) as Box<dyn Hooks>]))
|
||||
.is_ok()
|
||||
);
|
||||
let mut game = Game::new(
|
||||
&loaded.resources,
|
||||
loaded.pack,
|
||||
loaded.gui.unwrap(),
|
||||
options,
|
||||
settings,
|
||||
Audio::silent(&root),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
game.world = World::new(game.res.facts().air, 1);
|
||||
game.world
|
||||
.chunks
|
||||
.insert((0, 0), Arc::new(Chunk::empty(0, 0)));
|
||||
game.player = Some(PlayerMotion::new(game.motion, Vec3::new(0.5, 64.0, 0.5)).unwrap());
|
||||
game.yaw = 0.0;
|
||||
game.pitch = 0.0;
|
||||
game.selected = 0;
|
||||
game.set_mode_id(0);
|
||||
|
||||
// A custom item willing to own every mob still cannot preempt trading.
|
||||
setup(&mut game, &probe, "minecraft:villager", 42);
|
||||
game.mobs[0].extra.trade = Some(Box::new(Trader {
|
||||
offers: vec![MerchantOffer {
|
||||
base_cost_a: ItemCost {
|
||||
stack: ItemStack::new("minecraft:emerald", 1),
|
||||
},
|
||||
cost_b: None,
|
||||
result: ItemStack::new("minecraft:bread", 1),
|
||||
uses: 0,
|
||||
max_uses: 12,
|
||||
reward_exp: true,
|
||||
special_price_diff: 0,
|
||||
demand: 0,
|
||||
price_multiplier: 0.0,
|
||||
xp: 1,
|
||||
}],
|
||||
..Default::default()
|
||||
}));
|
||||
assert!(game.interact_entity(target(42)).unwrap());
|
||||
assert!(probe.calls.lock().unwrap().is_empty());
|
||||
assert!(!game.mobs[0].removed);
|
||||
assert_eq!(
|
||||
game.station
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.merchant
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.mob,
|
||||
Some(42)
|
||||
);
|
||||
|
||||
// Existing API 29 owns the earlier phase and retains its usual swing.
|
||||
setup(&mut game, &probe, "minecraft:zombie", 42);
|
||||
probe.claim_entity.store(true, Ordering::Relaxed);
|
||||
assert!(game.interact_entity(target(42)).unwrap());
|
||||
assert!(probe.calls.lock().unwrap().is_empty());
|
||||
assert_eq!(game.swings_started, 1);
|
||||
for (answer, consumed, swings) in [(0, false, 0), (1, true, 0), (2, true, 1)] {
|
||||
setup(&mut game, &probe, "minecraft:zombie", 42);
|
||||
probe.answer.store(answer, Ordering::Relaxed);
|
||||
assert_eq!(game.interact_entity(target(42)).unwrap(), consumed);
|
||||
assert_eq!(game.swings_started, swings);
|
||||
assert_eq!(
|
||||
*probe.calls.lock().unwrap(),
|
||||
[(42, "minecraft:zombie".into(), "minecraft:stick".into())]
|
||||
);
|
||||
}
|
||||
|
||||
// An ordinary mob with no own item action reaches the fallback exactly
|
||||
// once. The mod may discard it with no loot and end without an arm swing.
|
||||
setup(&mut game, &probe, "minecraft:blaze", 42);
|
||||
probe.discard.store(true, Ordering::Relaxed);
|
||||
assert!(game.interact_entity(None).unwrap());
|
||||
assert!(game.mobs[0].removed);
|
||||
assert!(game.dropped.is_empty());
|
||||
assert_eq!(game.swings_started, 0);
|
||||
assert_eq!(probe.calls.lock().unwrap().len(), 1);
|
||||
assert_eq!(game.inventory[0].as_ref().unwrap().count, 2);
|
||||
|
||||
// Panda's early false is still a normal pass into the outer fallback.
|
||||
setup(&mut game, &probe, "minecraft:panda", 42);
|
||||
assert!(game.interact_entity(target(42)).unwrap());
|
||||
assert_eq!(probe.calls.lock().unwrap().len(), 1);
|
||||
assert_eq!(game.swings_started, 0);
|
||||
|
||||
setup(&mut game, &probe, "minecraft:zombie", 42);
|
||||
assert!(
|
||||
game.with_hand(Hand::Off, |g| g.interact_entity(target(42)))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(probe.calls.lock().unwrap()[0].2, "minecraft:paper");
|
||||
assert_eq!(game.hand, Hand::Main);
|
||||
assert_eq!(game.inventory[0].as_ref().unwrap().count, 2);
|
||||
assert_eq!(game.offhand.as_ref().unwrap().count, 3);
|
||||
assert_eq!(game.swings_started, 0);
|
||||
|
||||
for invalid in ["dead", "removed", "riding", "wrong_id", "empty", "zero"] {
|
||||
setup(&mut game, &probe, "minecraft:zombie", 42);
|
||||
match invalid {
|
||||
"dead" => game.mobs[0].health = 0.0,
|
||||
"removed" => game.mobs[0].removed = true,
|
||||
"riding" => game.riding = Some(42),
|
||||
"empty" => game.inventory[0] = None,
|
||||
"zero" => game.inventory[0].as_mut().unwrap().count = 0,
|
||||
_ => {}
|
||||
}
|
||||
assert!(
|
||||
!game
|
||||
.interact_entity(target(if invalid == "wrong_id" { 99 } else { 42 }))
|
||||
.unwrap(),
|
||||
"{invalid}"
|
||||
);
|
||||
assert!(probe.calls.lock().unwrap().is_empty(), "{invalid}");
|
||||
}
|
||||
setup(&mut game, &probe, "minecraft:zombie", 42);
|
||||
game.mobs[0].health = 0.0;
|
||||
assert!(!game.interact_entity(None).unwrap());
|
||||
assert!(probe.calls.lock().unwrap().is_empty());
|
||||
|
||||
// Local selection is occluded; a validated server packet keeps its exact
|
||||
// entity ID, independent of the acting player's camera and local ray.
|
||||
setup(&mut game, &probe, "minecraft:blaze", 42);
|
||||
let stone = game
|
||||
.pack
|
||||
.assets
|
||||
.registry
|
||||
.default_state("minecraft:stone")
|
||||
.unwrap();
|
||||
game.world.set(0, 65, 1, stone).unwrap();
|
||||
assert!(!game.interact_entity(None).unwrap());
|
||||
assert!(probe.calls.lock().unwrap().is_empty());
|
||||
assert!(game.interact_entity(target(42)).unwrap());
|
||||
assert_eq!(probe.calls.lock().unwrap().len(), 1);
|
||||
setup(&mut game, &probe, "minecraft:zombie", 42);
|
||||
game.set_mode_id(3);
|
||||
assert_eq!(game.use_item_as_player().unwrap(), None);
|
||||
assert!(probe.calls.lock().unwrap().is_empty());
|
||||
}
|
||||
@@ -2620,6 +2620,24 @@ impl Game<'_> {
|
||||
) {
|
||||
mod_api::dispatch_interact_entity(self, mod_api::mods(), entity, kind, stack, claimed);
|
||||
}
|
||||
/// An item's fallback on a living native Mob, after ordinary entity
|
||||
/// interactions and `native_interact_entity` passed. The exact packet
|
||||
/// target or unobstructed crosshair mob must still be alive; `stack` is
|
||||
/// nonempty and belongs to the acting hand. First `Some` owns the turn:
|
||||
/// true swings once, false ends it without a swing; None passes.
|
||||
/// Server-side for every acting player. This covers native Mob entries,
|
||||
/// not armor stands, other real players or dragon-part interaction tables.
|
||||
/// `ItemStack.interactLivingEntity` in `Player.interactOn`.
|
||||
/// API @API:item_interact_mob@.
|
||||
pub(super) fn native_item_interact_mob(
|
||||
&mut self,
|
||||
entity: i32,
|
||||
kind: &str,
|
||||
stack: Option<&ItemStack>,
|
||||
swing: &mut Option<bool>,
|
||||
) {
|
||||
mod_api::dispatch_item_interact_mob(self, mod_api::mods(), entity, kind, stack, swing);
|
||||
}
|
||||
/// A player left-clicks the block `state` at `pos`, `face` the face hit
|
||||
/// (`[0, 1, 0]` is the top): where `ServerPlayerGameMode.handleBlockBreakAction`
|
||||
/// takes a dig's start, after its reach, spawn protection and world border
|
||||
|
||||
@@ -1056,4 +1056,73 @@ mod tests {
|
||||
let mut claimed = false;
|
||||
dispatch_use_item_on(&mut host, &mods, &stick(), [0; 3], [0, 1, 0], &mut claimed);
|
||||
}
|
||||
|
||||
struct ItemMobReply {
|
||||
answer: Option<bool>,
|
||||
calls: Arc<Mutex<Vec<Option<bool>>>>,
|
||||
}
|
||||
impl Hooks for ItemMobReply {
|
||||
fn item_interact_mob(
|
||||
&self,
|
||||
_: &mut dyn Host,
|
||||
event: &mut ItemInteractMobEvent<'_>,
|
||||
) -> Option<bool> {
|
||||
assert_eq!(event.entity, 17);
|
||||
assert_eq!(event.kind, "minecraft:zombie");
|
||||
assert_eq!(event.stack, Some(&stick()));
|
||||
self.calls.lock().unwrap().push(self.answer);
|
||||
self.answer
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_interact_mob_first_response_controls_swing() {
|
||||
for (answers, expected, expected_calls) in [
|
||||
([None, Some(false), Some(true)], Some(false), 2),
|
||||
([None, Some(true), Some(false)], Some(true), 2),
|
||||
([None, None, None], None, 3),
|
||||
] {
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
let mods: Vec<Box<dyn Hooks>> = answers
|
||||
.into_iter()
|
||||
.map(|answer| {
|
||||
Box::new(ItemMobReply {
|
||||
answer,
|
||||
calls: calls.clone(),
|
||||
}) as Box<dyn Hooks>
|
||||
})
|
||||
.collect();
|
||||
let mut host = Reader {
|
||||
block: "minecraft:air",
|
||||
lines: Vec::new(),
|
||||
};
|
||||
let mut swing = None;
|
||||
dispatch_item_interact_mob(
|
||||
&mut host,
|
||||
&mods,
|
||||
17,
|
||||
"minecraft:zombie",
|
||||
Some(&stick()),
|
||||
&mut swing,
|
||||
);
|
||||
assert_eq!(swing, expected);
|
||||
assert_eq!(calls.lock().unwrap().len(), expected_calls);
|
||||
calls.lock().unwrap().clear();
|
||||
swing = Some(false);
|
||||
dispatch_item_interact_mob(
|
||||
&mut host,
|
||||
&mods,
|
||||
17,
|
||||
"minecraft:zombie",
|
||||
Some(&stick()),
|
||||
&mut swing,
|
||||
);
|
||||
assert_eq!(
|
||||
swing,
|
||||
Some(false),
|
||||
"an earlier mixin already owned the result"
|
||||
);
|
||||
assert!(calls.lock().unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3726,6 +3726,26 @@ dispatch_use_item_on(&mut Stone, &mods, &stack, [0, 64, 0], [0, 1, 0], &mut clai
|
||||
assert!(claimed);
|
||||
```
|
||||
|
||||
### Item fallback on native mobs
|
||||
|
||||
`native_item_interact_mob(entity, kind, stack, swing)` is a pending fragment for
|
||||
an item's fallback after a native Mob's ordinary interaction passes. Existing
|
||||
`native_interact_entity` (API 29), the entity's own handlers and built-in item
|
||||
actions retain priority; errors propagate before the fallback. This is the
|
||||
`ItemStack.interactLivingEntity` part of `Player.interactOn`, currently limited
|
||||
to native Mob entries. Armor stands, other real players and dragon-part tables
|
||||
are not covered by this hook.
|
||||
|
||||
The caller reuses normal mob selection: an already validated packet names its
|
||||
exact entity ID; a local crosshair must reach the mob before a blocking shape.
|
||||
The target must still be alive and not removed, and the acting hand must hold a
|
||||
nonempty item. `stack` is that hand's item snapshot; a hook changes inventories
|
||||
through ordinary Host helpers when needed. First `Some(true)` owns the turn and
|
||||
swings once, `Some(false)` owns it without a swing, and `None` passes to the next
|
||||
mod. An earlier mixin result also owns the output. No item count or world change
|
||||
is performed merely because a hook answers. The fragment is unstamped; it adds
|
||||
no manually assigned global API number.
|
||||
|
||||
### Adding a hook
|
||||
|
||||
A new hook needs its fragment in `tools/native-hook-fragments/` with its `mod_api` entry (for example
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Item fallback after a mob interaction
|
||||
|
||||
The pending `item_interact_mob` fragment adds a server hook after normal entity
|
||||
interactions have passed. It covers native Mob entries, including an acting
|
||||
guest's exact named target. Armor stands, other real players and dragon-part
|
||||
interaction tables remain outside this bounded addition. Existing API 29 keeps
|
||||
its earlier phase and ordinary swing behavior.
|
||||
|
||||
Minecraft 26.3 `Player.interactOn` calls the entity's `interact` first and returns
|
||||
when it consumes the action, before the nonempty item's
|
||||
`ItemStack.interactLivingEntity` fallback. The new hook follows that ordering.
|
||||
Existing fallible ordinary handling retains `?`, so an error returns before the
|
||||
fallback. No artificial error-producing interaction is introduced for testing.
|
||||
|
||||
`native_item_interact_mob(entity, kind, stack, swing)` uses the existing replace
|
||||
contract: first `Some` owns the result, including an earlier mixin answer.
|
||||
`Some(true)` ends the turn with the normal acting-hand swing; `Some(false)` ends
|
||||
it without one; `None` passes. The caller reuses the ordinary mob selection and
|
||||
block-occlusion checks, then requires an alive, nonremoved mob and a nonempty
|
||||
acting-hand stack. Packet reach validation remains at the normal server entry;
|
||||
the fallback does not repick another entity or impose a new line-of-sight rule
|
||||
on an already validated named packet.
|
||||
|
||||
The actual prepared-resource Game test installs a process-local trait probe and
|
||||
checks a real villager merchant menu before the fallback, earlier API 29
|
||||
ownership, all three results, ordinary Blaze discard without loot or swing,
|
||||
Panda's early pass, correct offhand stack, unchanged inventories, invalid/dead/
|
||||
removed/ridden targets, empty and zero-count stacks, local block occlusion,
|
||||
exact named-target handling and the normal spectator guard. A separate pure
|
||||
dispatch test verifies first-response ownership and that later hooks never run
|
||||
after either `Some(false)` or `Some(true)`.
|
||||
|
||||
Run with private prepared resources and fresh homes, no window or network
|
||||
listener:
|
||||
|
||||
```sh
|
||||
cargo test --offline -p horizon-client --lib item_interact_mob \
|
||||
-- --include-ignored --nocapture
|
||||
cargo test --offline -p horizon-mod-api-gen -p horizon-mod-api-gen-tests --lib
|
||||
cargo clippy --offline -p horizon-client --lib --tests -- -D warnings
|
||||
cargo fmt --all -- --check
|
||||
python3 tools/check_public_tree.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Set `HORIZON_TEST_RESOURCES` to private prepared 26.3 resources and set
|
||||
`HORIZON_HOME`, `HORIZON_LAUNCHER_HOME`, `XDG_DATA_HOME`, `XDG_CONFIG_HOME`,
|
||||
`HORIZON_NO_AUDIO=1` and `HORIZON_LAUNCHER_NO_SELF_UPDATE=1`. The ignored Game test
|
||||
installs process-wide trait mods, so run its filter in a separate test process.
|
||||
No Create bundle, third-party decoder, original asset bytes or installed player
|
||||
world is involved. The hook fragment and generated dispatcher source were
|
||||
processed with the official `cargo run -p xtask -- mod-api`; global catalog/API
|
||||
history stamping is deliberately left to the normal upstream process.
|
||||
|
||||
Validation: both the actual Game and first-response dispatch tests passed;
|
||||
the generator's 15 tests and generated-dispatch suite's 12 tests passed.
|
||||
Full `horizon-client` library/test Clippy passed with warnings denied and no
|
||||
exceptions, as did workspace formatting, public-tree and diff checks.
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"name": "item_interact_mob",
|
||||
"target": "horizon_client::game::native_hooks::Game<'_>::native_item_interact_mob",
|
||||
"side": "server",
|
||||
"mod_api": {"contract": "replace", "out": "swing"}
|
||||
}
|
||||
],
|
||||
"rust_summary": "an item fallback after a native mob's ordinary interaction passed.",
|
||||
"doc_summary": "`native_item_interact_mob(entity, kind, stack, swing)` runs after ordinary entity interactions pass, for an alive native Mob selected by the exact packet ID or unobstructed crosshair. First `Some(true)` owns and swings once; `Some(false)` owns without a swing; `None` passes. The nonempty stack belongs to the acting hand. Armor stands, other real players and dragon-part tables are not covered. Bundles need \"hooks\": @API:item_interact_mob@."
|
||||
}
|
||||
Reference in new issue
Block a user