Merge pull request #7395 from arismoko/v2/ses_01M6E91JFQZ707DB55V2730YTX-w1-bow

🤖 Agent: queued at d9b0388195. Checked: clippy, the bow and native lib tests and the arrow harness pass on the branch before fa876269f3. That commit only puts the crossbow mob's attack-delay draw back before the pitch draw, as main has it. Its head builds and passes xtask mod-api --check and the stamp tests on today's main.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-02 21:55:18 +01:00
commit 3fc4c4ca05
12 files changed
+426 -38

No files matched your search

+15 -9
View File
@@ -6443,7 +6443,14 @@ impl Game<'_> {
return Ok(true);
}
if !on_block && &*stack.item == "minecraft:bow" {
if self.creative || self.projectile_slot("minecraft:bow").is_some() {
// BowItem.use: ArrowNockEvent, then FAIL without ammunition.
let found_projectile = self.get_projectile(&stack).is_some();
let has_ammo = found_projectile || self.creative;
let player = horizon_client::lan::playerdata::ints_uuid(self.acting_player_uuid());
let name = self.acting_player_name().to_owned();
let (hand, mut cancel) = (self.hand, false);
self.native_arrow_nock(player, &name, &stack, hand, has_ammo, &mut cancel);
if has_ammo && !cancel {
self.start_using_item();
}
return Ok(true);
@@ -13422,23 +13429,22 @@ impl Game<'_> {
}
/// BowItem.releaseUsing: power from charge time, arrows at power * 3.
fn release_bow(&mut self, stack: &ItemStack, ticks: u32) {
let arrow_slot = self.projectile_slot("minecraft:bow");
let has_ammo = arrow_slot.is_some() || self.creative;
let ammo = self.get_projectile(stack);
let mut charge = i32::try_from(ticks).unwrap_or(i32::MAX);
let mut canceled = false;
self.native_arrow_loose(stack, has_ammo, &mut charge, &mut canceled);
let Some(power) = item_use::bow_release_power(has_ammo, charge, canceled) else {
self.native_arrow_loose(stack, ammo.is_some(), &mut charge, &mut canceled);
let power = item_use::bow_release_power(ammo.is_some(), charge, canceled);
let (Some(power), Some(ammo)) = (power, ammo) else {
return;
};
let arrow_slot = ammo.slot;
let weapon = Some(stack.clone());
let Some(eye) = self.eye_position() else {
return;
};
// BowItem.shootProjectile: shootFromRotation(power * 3, inaccuracy 1).
let p = Vec3::new(eye[0], eye[1] - 0.1, eye[2]);
let ammo = arrow_slot
.and_then(|i| self.stack_in(i).cloned())
.unwrap_or_else(|| ItemStack::new("minecraft:arrow", 1));
let weapon = self.held().cloned();
let ammo = ammo.stack;
let (power_bonus, infinity) = self.bow_enchantments(&ammo.item);
let effect = dispenser::tipped_effect(&ammo).or_else(|| {
(&*ammo.item == "minecraft:spectral_arrow").then_some(("minecraft:glowing", 200))
+46 -10
View File
@@ -1786,6 +1786,19 @@ impl Game<'_> {
);
}
}
/// A mob's projectile enters the level.
fn launch_mob_projectile(&mut self, mut p: Projectile) {
self.next_entity += 1;
p.id = self.next_entity;
let (owner, at) = (p.owner, [p.position.x, p.position.y, p.position.z]);
// LlamaSpit.recreateFromPacket: the spit's burst on the client.
if p.kind == ProjectileKind::LlamaSpit {
let v = p.velocity;
self.particles.llama_spit_burst(at, [v.x, v.y, v.z]);
}
self.add_projectile(p);
self.projectile_event("minecraft:projectile_shoot", owner, at);
}
pub(super) fn handle_mob_events(&mut self, events: Vec<MobEvent>) -> Result<()> {
for e in events {
match e {
@@ -2291,17 +2304,40 @@ impl Game<'_> {
}
}
}
MobEvent::Projectile(mut p) => {
self.next_entity += 1;
p.id = self.next_entity;
let (owner, at) = (p.owner, [p.position.x, p.position.y, p.position.z]);
// LlamaSpit.recreateFromPacket: the spit's burst on the client.
if p.kind == ProjectileKind::LlamaSpit {
let v = p.velocity;
self.particles.llama_spit_burst(at, [v.x, v.y, v.z]);
MobEvent::Projectile(p) => self.launch_mob_projectile(p),
MobEvent::BowShot {
mob,
weapon,
mut arrow,
} => {
self.mob_bow_ammo(mob, &weapon, &mut arrow);
self.launch_mob_projectile(arrow);
}
MobEvent::CrossbowShot { ammo, mut arrow } => {
if &*ammo.item == "minecraft:firework_rocket" {
// `CrossbowItem.createProjectile`: a rocket, from 0.15 under the eyes.
let mut at = arrow.position;
at.y -= f64::from(0.15f32) - 0.1;
self.launch_angled_rocket(at, arrow.velocity, &ammo, arrow.owner);
} else {
Self::arrow_from_ammo(&ammo, &mut arrow);
self.launch_mob_projectile(arrow);
}
}
MobEvent::CrossbowLoad {
mob,
weapon,
at,
pitch,
} => {
if self.mob_crossbow_ammo(mob, &weapon) {
self.audio.play(
"minecraft:item.crossbow.loading_end",
Some(at),
1.0,
pitch,
);
}
self.add_projectile(p);
self.projectile_event("minecraft:projectile_shoot", owner, at);
}
MobEvent::Spawn(mut m) => {
self.next_entity += 1;
+125 -7
View File
@@ -15,6 +15,14 @@ use horizon_core::{geometry::Vec3, random::RandomSource};
const CROSSBOW: &str = "minecraft:crossbow";
const FIREWORK_ROCKET: &str = "minecraft:firework_rocket";
const ARROW: &str = "minecraft:arrow";
/// What `getProjectile` returned: the ammunition and, while it is still the
/// stack found in the inventory, the slot it is taken from.
pub(super) struct Ammo {
pub(super) stack: ItemStack,
pub(super) slot: Option<usize>,
}
/// `ProjectileWeaponItem.ARROW_ONLY`, for the arrows Horizon shoots.
fn is_arrow(stack: &ItemStack) -> bool {
@@ -101,6 +109,115 @@ impl Game<'_> {
let hands = [OFFHAND_SLOT, self.selected].map(|slot| (slot, self.stack_in(slot)));
projectile_slot(weapon, hands, &self.inventory)
}
/// `Player.getProjectile` for `weapon`, as `LivingGetProjectileEvent` leaves
/// it: a creative player without arrows has `hasInfiniteMaterials`' arrow.
pub(super) fn get_projectile(&mut self, weapon: &ItemStack) -> Option<Ammo> {
let slot = self.projectile_slot(&weapon.item);
let vanilla = match slot {
Some(i) => self.stack_in(i).cloned(),
None => self.creative.then(|| ItemStack::new(ARROW, 1)),
};
if horizon_client::mod_api::mods().is_empty() {
return vanilla.map(|stack| Ammo { stack, slot });
}
let mut projectile = vanilla.clone();
// A client predicting its own use has the profile it logged in with.
let player = match self
.remote
.as_ref()
.filter(|_| self.lan_acting_uuid.is_none())
{
Some(r) => r.profile(),
None => horizon_client::lan::playerdata::ints_uuid(self.acting_player_uuid()),
};
let name = self.own_profile_name();
self.native_living_get_projectile(
Some(player),
&name,
None,
"minecraft:player",
weapon,
&mut projectile,
);
let slot = slot.filter(|_| projectile == vanilla);
projectile.map(|stack| Ammo { stack, slot })
}
/// `AbstractSkeleton.performRangedAttack`'s `getProjectile`: the arrow
/// `mob` shot becomes the replaced ammunition's (`ProjectileUtil.getMobArrow`).
pub(super) fn mob_bow_ammo(&mut self, mob: u32, weapon: &ItemStack, arrow: &mut Projectile) {
if horizon_client::mod_api::mods().is_empty() {
return;
}
let Some(shooter) = self.mobs.iter().find(|m| m.id == mob) else {
return;
};
let kind = shooter.kind.id.clone();
let vanilla = Self::mob_vanilla_ammo(&shooter.extra, weapon);
let mut projectile = Some(vanilla.clone());
self.native_living_get_projectile(None, "", Some(mob), &kind, weapon, &mut projectile);
let Some(ammo) = projectile.filter(|ammo| *ammo != vanilla && is_arrow(ammo)) else {
return;
};
Self::arrow_from_ammo(&ammo, arrow);
}
/// `Monster.getProjectile`: the projectile `weapon` supports in the off
/// hand, then the main hand (`getHeldProjectile`), else a new arrow.
fn mob_vanilla_ammo(extra: &horizon_client::mobs::Extra, weapon: &ItemStack) -> ItemStack {
let supported =
|s: &ItemStack| is_arrow(s) || &*weapon.item == CROSSBOW && &*s.item == FIREWORK_ROCKET;
extra
.off_hand
.as_deref()
.map(|item| ItemStack::new(item, 1))
.into_iter()
.chain(extra.main_hand_stack().cloned())
.find(supported)
.unwrap_or_else(|| ItemStack::new(ARROW, 1))
}
/// `ProjectileUtil.getMobArrow`: `arrow` carries `ammo`'s item and effect.
pub(super) fn arrow_from_ammo(ammo: &ItemStack, arrow: &mut Projectile) {
if let ProjectileKind::Arrow { effect, .. } = &mut arrow.kind {
if &*ammo.item == "minecraft:spectral_arrow" {
*effect = Some(("minecraft:glowing", 200));
} else if effect.is_none()
&& let Some(tipped) = super::dispenser::tipped_effect(ammo)
{
// Stray.getArrow and Bogged.getArrow add their effect to the tipped arrow.
*effect = Some(tipped);
}
}
if let Some(a) = arrow.arrow.as_mut() {
a.set_item(&horizon_client::arrows::drawn_ammo(ammo, false));
}
}
/// `CrossbowItem.onUseTick`'s `getProjectile` for a mob that finished
/// charging `weapon` (pillagers and piglins: `Monster.getProjectile`). The
/// replaced ammunition is what the crossbow holds; none leaves it uncharged
/// and returns false (`tryLoadProjectiles`).
pub(super) fn mob_crossbow_ammo(&mut self, mob: u32, weapon: &ItemStack) -> bool {
let Some(shooter) = self.mobs.iter().find(|m| m.id == mob) else {
return false;
};
let kind = shooter.kind.id.clone();
let mut projectile = Some(Self::mob_vanilla_ammo(&shooter.extra, weapon));
self.native_living_get_projectile(None, "", Some(mob), &kind, weapon, &mut projectile);
// A crossbow takes arrows and firework rockets only.
let projectile = projectile.filter(|ammo| is_arrow(ammo) || &*ammo.item == FIREWORK_ROCKET);
let Some(shooter) = self.mobs.iter_mut().find(|m| m.id == mob) else {
return false;
};
let Some(mut crossbow) = shooter.extra.main_hand_stack().cloned() else {
return false;
};
let loaded = projectile.is_some();
crossbow.data_mut().charged = projectile
.iter()
.map(|ammo| horizon_client::arrows::drawn_ammo(ammo, false))
.collect();
crossbow.tidy();
shooter.extra.set_main_hand_stack(Some(crossbow));
loaded
}
/// `CrossbowItem.use`: shoot when charged, else start charging.
pub(super) fn use_crossbow(&mut self, stack: &ItemStack) -> bool {
if &*stack.item != CROSSBOW {
@@ -112,7 +229,7 @@ impl Game<'_> {
}
if stack.data().is_some_and(|d| !d.charged.is_empty()) {
self.shoot_crossbow();
} else if self.projectile_slot(CROSSBOW).is_some() || self.creative {
} else if self.get_projectile(stack).is_some() {
self.start_using_item();
}
true
@@ -145,12 +262,12 @@ impl Game<'_> {
} else {
1
};
let slot_ammo = self.projectile_slot(CROSSBOW);
let ammo = slot_ammo
.and_then(|i| self.stack_in(i).cloned())
.unwrap_or_else(|| ItemStack::new("minecraft:arrow", 1));
if !self.creative {
let Some(i) = slot_ammo else { return };
let Some(ammo) = self.get_projectile(stack) else {
return;
};
if !self.creative
&& let Some(i) = ammo.slot
{
let cell = self.stack_in_mut(i);
if let Some(s) = cell.as_mut() {
s.count -= 1;
@@ -159,6 +276,7 @@ impl Game<'_> {
}
}
}
let ammo = ammo.stack;
// useAmmo: the extra multishot arrows and creative's are intangible.
let drawn = (0..shots)
.map(|i| horizon_client::arrows::drawn_ammo(&ammo, self.creative || i > 0))
+53 -1
View File
@@ -5108,6 +5108,58 @@ impl Game<'_> {
mod_api::dispatch_entity_shoot_bow(self, mod_api::mods(), player, name, projectile, force);
}
/// A player is about to nock `bow` with `hand` (`BowItem.use`, NeoForge's
/// `ArrowNockEvent`): `has_ammo` is whether `getProjectile` found ammunition
/// or the player is in creative. Set `cancel` to fail the use.
/// @API:arrow_nock@.
pub(super) fn native_arrow_nock(
&mut self,
player: u128,
name: &str,
bow: &ItemStack,
hand: Hand,
has_ammo: bool,
cancel: &mut bool,
) {
mod_api::dispatch_arrow_nock(
self,
mod_api::mods(),
player,
name,
bow,
hand,
has_ammo,
cancel,
);
}
/// `LivingEntity.getProjectile(weapon)` (NeoForge's
/// `LivingGetProjectileEvent`) for a player (`player` and `name`) or a
/// mob (`entity`, `kind`): a skeleton or illusioner's shot, or the crossbow
/// load of a pillager or piglin. `projectile` is the vanilla
/// ammunition; replace it to choose another. A replaced stack is not taken
/// from the inventory. @API:living_get_projectile@.
pub(super) fn native_living_get_projectile(
&mut self,
player: Option<u128>,
name: &str,
entity: Option<u32>,
kind: &str,
weapon: &ItemStack,
projectile: &mut Option<ItemStack>,
) {
mod_api::dispatch_living_get_projectile(
self,
mod_api::mods(),
player,
name,
entity,
kind,
weapon,
projectile,
);
}
/// A guest's fall flying state changed on the server. The change has
/// already been applied. 222.
pub(super) fn native_entity_toggle_glide(&mut self, player: u128, name: &str, gliding: bool) {
@@ -6085,7 +6137,7 @@ impl Game<'_> {
/// release; `native_entity_shoot_bow` reports a spawned arrow afterward
/// with read-only force. Fires on the Game running the bow release, including
/// LAN guests; a remote client's own release path is not covered. Crossbows
/// and `ArrowNockEvent` are not covered. 318.
/// are not covered; `native_arrow_nock` is the bow's start. 318.
pub(super) fn native_arrow_loose(
&mut self,
stack: &ItemStack,
+2 -2
View File
@@ -8097,14 +8097,14 @@ impl Game<'_> {
// BundleItem.use: always starts; the server drops the contents.
_ if bundle => true,
// BowItem.use: with an arrow, or in creative.
"minecraft:bow" => self.creative || self.projectile_slot("minecraft:bow").is_some(),
"minecraft:bow" => self.get_projectile(held).is_some() || self.creative,
// CrossbowItem.use: charging when not charged and with ammunition;
// a charged one shoots on the server.
"minecraft:crossbow" => {
if held.data().is_some_and(|d| !d.charged.is_empty()) {
return true;
}
self.creative || self.projectile_slot("minecraft:crossbow").is_some()
self.get_projectile(held).is_some()
}
// TridentItem.use: not about to break, Riptide only in water or rain.
"minecraft:trident" => {
+21
View File
@@ -1627,6 +1627,27 @@ pub enum MobEvent {
vehicle: u32,
},
Projectile(crate::projectiles::Projectile),
/// An arrow `mob` shot with `weapon` (`AbstractSkeleton.performRangedAttack`):
/// the game asks for the ammunition (`getProjectile`) before it flies.
BowShot {
mob: u32,
weapon: crate::items::ItemStack,
arrow: crate::projectiles::Projectile,
},
/// `mob` finished charging `weapon` (`CrossbowItem.onUseTick`): the game
/// asks for the ammunition (`getProjectile`), and plays the end sound
/// at `at` only when the crossbow loaded.
CrossbowLoad {
mob: u32,
weapon: crate::items::ItemStack,
at: [f64; 3],
pitch: f32,
},
/// `arrow` shot from the `ammo` a mob's crossbow was charged with.
CrossbowShot {
ammo: crate::items::ItemStack,
arrow: crate::projectiles::Projectile,
},
/// Spawn a new mob (breeding, slime splitting, chicken eggs...).
Spawn(Box<Mob>),
/// Spawn a mob by type id at a position (conversions, splits).
+23 -7
View File
@@ -564,14 +564,21 @@ impl Goal for CrossbowAttack {
m.extra.using_item = 0;
m.extra.flag = false;
self.state = Crossbow::Charged;
let weapon = m
.extra
.main_hand_stack()
.cloned()
.unwrap_or_else(|| ItemStack::new("minecraft:crossbow", 1));
set_crossbow_charged(m, true);
// The attack delay is drawn before the sound's pitch, as before.
self.attack_delay = 20 + m.random.next_int_bound(20).unwrap_or(0);
cx.events.push(MobEvent::Sound(
"item.crossbow.loading_end",
m.center(),
1.0,
1.0 / (m.random.next_float() * 0.5 + 1.0) + 0.2,
));
let pitch = 1.0 / (m.random.next_float() * 0.5 + 1.0) + 0.2;
cx.events.push(MobEvent::CrossbowLoad {
mob: m.id,
at: m.center(),
pitch,
weapon,
});
}
}
Crossbow::Charged => {
@@ -596,6 +603,15 @@ fn shoot_crossbow(m: &mut Mob, who: Who, cx: &mut Cx) {
let Some(t) = cx.w.target_info(who) else {
return;
};
// CrossbowItem.shootAll: only the charged projectile flies.
let Some(ammo) = m
.extra
.main_hand_stack()
.and_then(|s| s.data())
.and_then(|d| d.charged.first().cloned())
else {
return;
};
let start = Vec3::new(m.position.x, m.eye_y() - 0.1, m.position.z);
let dx = t.position.x - m.position.x;
let dz = t.position.z - m.position.z;
@@ -620,7 +636,7 @@ fn shoot_crossbow(m: &mut Mob, who: Who, cx: &mut Cx) {
1.6,
(14 - i32::from(cx.w.difficulty.id) * 4) as f32,
);
cx.events.push(MobEvent::Projectile(p));
cx.events.push(MobEvent::CrossbowShot { ammo, arrow: p });
let pitch = 1.0 / (m.random.next_float() * 0.5 + 1.8) + 0.53;
cx.events.push(MobEvent::Sound(
"item.crossbow.shoot",
+14 -1
View File
@@ -905,7 +905,20 @@ pub fn perform_ranged_attack(m: &mut Mob, who: Who, cx: &mut Cx, power: f32) {
1.6,
(14 - id * 4) as f32,
);
cx.events.push(MobEvent::Projectile(p));
cx.events.push(MobEvent::BowShot {
mob: m.id,
// ProjectileUtil.getWeaponHoldingHand(this, Items.BOW).
weapon: if m.extra.using_off_hand {
m.extra
.off_hand
.as_ref()
.map(|item| crate::items::ItemStack::new(item, 1))
} else {
m.extra.main_hand_stack().cloned()
}
.unwrap_or_else(|| crate::items::ItemStack::new("minecraft:bow", 1)),
arrow: p,
});
let pitch = 1.0 / (m.random.next_float() * 0.4 + 0.8);
cx.events.push(MobEvent::Sound(
"entity.skeleton.shoot",
+92 -1
View File
@@ -2477,7 +2477,7 @@ pub trait Hooks: Send + Sync {
/// release; `native_entity_shoot_bow` reports a spawned arrow afterward
/// with read-only force. Fires on the Game running the bow release, including
/// LAN guests; a remote client's own release path is not covered. Crossbows
/// and `ArrowNockEvent` are not covered. 318.
/// are not covered; `native_arrow_nock` is the bow's start. 318.
///
/// Return `Flow::Cancel` to take it: this sets `canceled`, and later mods are skipped.
fn arrow_loose(&self, host: &mut dyn Host, event: &mut ArrowLooseEvent<'_>) -> Flow {
@@ -4512,6 +4512,17 @@ pub trait Hooks: Send + Sync {
fn living_jump(&self, event: &mut LivingJumpEvent<'_>) {
let _ = event;
}
/// A player is about to nock `bow` with `hand` (`BowItem.use`, NeoForge's
/// `ArrowNockEvent`): `has_ammo` is whether `getProjectile` found ammunition
/// or the player is in creative. Set `cancel` to fail the use.
/// @API:arrow_nock@.
///
/// Return `Flow::Cancel` to take it: this sets `cancel`, and later mods are skipped.
fn arrow_nock(&self, host: &mut dyn Host, event: &mut ArrowNockEvent<'_>) -> Flow {
let _ = host;
let _ = event;
Flow::Continue
}
/// Player FOV scale, after vanilla effects and before zoom and easing.
/// Invalid values or values outside 0.01..=10 revert to base. API @API:compute_fov_modifier@.
fn compute_fov_modifier(&self, host: &dyn Host, event: &mut ComputeFovModifierEvent<'_>) {
@@ -4547,6 +4558,18 @@ pub trait Hooks: Send + Sync {
let _ = event;
None
}
/// `LivingEntity.getProjectile(weapon)` (NeoForge's
/// `LivingGetProjectileEvent`) for a player (`player` and `name`) or a
/// mob (`entity`, `kind`): a skeleton or illusioner's shot, or the crossbow
/// load of a pillager or piglin. `projectile` is the vanilla
/// ammunition; replace it to choose another. A replaced stack is not taken
/// from the inventory. @API:living_get_projectile@.
///
/// Every mod gets `event.projectile` in order and may add, change or remove projectile.
fn living_get_projectile(&self, host: &mut dyn Host, event: &mut LivingGetProjectileEvent<'_>) {
let _ = host;
let _ = event;
}
/// Local player movement input, after reading it and before applying it.
fn movement_input(&self, host: &dyn Host, event: &mut MovementInputEvent) {
let _ = host;
@@ -13884,6 +13907,42 @@ pub fn dispatch_living_jump(mods: &[Box<dyn Hooks>], entity: u32, kind: &str) {
mod_hook.living_jump(&mut event);
}
}
/// What [`Hooks::arrow_nock`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct ArrowNockEvent<'a> {
pub player: u128,
pub name: &'a str,
pub bow: &'a ItemStack,
pub hand: Hand,
pub has_ammo: bool,
}
pub fn dispatch_arrow_nock(
host: &mut dyn Host,
mods: &[Box<dyn Hooks>],
player: u128,
name: &str,
bow: &ItemStack,
hand: Hand,
has_ammo: bool,
cancel: &mut bool,
) {
if *cancel {
return;
}
let mut event = ArrowNockEvent {
player,
name,
bow,
hand,
has_ammo,
};
for mod_hook in super::entered(mods, "arrow_nock") {
if matches!(mod_hook.arrow_nock(host, &mut event), Flow::Cancel) {
*cancel = true;
break;
}
}
}
/// What [`Hooks::compute_fov_modifier`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct ComputeFovModifierEvent<'a> {
@@ -13959,6 +14018,38 @@ pub fn dispatch_item_container(
}
}
}
/// What [`Hooks::living_get_projectile`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct LivingGetProjectileEvent<'a> {
pub player: Option<u128>,
pub name: &'a str,
pub entity: Option<u32>,
pub kind: &'a str,
pub weapon: &'a ItemStack,
pub projectile: &'a mut Option<ItemStack>,
}
pub fn dispatch_living_get_projectile(
host: &mut dyn Host,
mods: &[Box<dyn Hooks>],
player: Option<u128>,
name: &str,
entity: Option<u32>,
kind: &str,
weapon: &ItemStack,
projectile: &mut Option<ItemStack>,
) {
let mut event = LivingGetProjectileEvent {
player,
name,
entity,
kind,
weapon,
projectile,
};
for mod_hook in super::entered(mods, "living_get_projectile") {
mod_hook.living_get_projectile(host, &mut event);
}
}
/// What [`Hooks::movement_input`] is told. Fields may be added: the struct is `#[non_exhaustive]`.
#[non_exhaustive]
pub struct MovementInputEvent {
+2
View File
@@ -2041,6 +2041,8 @@ For mods that run where the world is simulated: a dedicated server, a LAN host,
- **`native_entity_pose_change(player, name, from, to)`**: a guest player's synchronized pose changed. Pose names are `STANDING`, `CROUCHING`, `SWIMMING`, `FALL_FLYING`, `SPIN_ATTACK` or `SLEEPING`.
- **`native_entity_toggle_glide(player, name, gliding)`**: a guest player's fall flying state changed after the server accepted or ended a glide.
- **`native_entity_shoot_bow(player, name, projectile, force)`**: a player fired an arrow from a bow or crossbow, after the arrow entered the level. `force` is the shooting power. This reports the completed shot.
- **`native_arrow_nock(player, name, bow, hand, has_ammo, cancel)`**: a player is about to start using a bow, where `BowItem.use` runs on the server (NeoForge's `ArrowNockEvent`). `has_ammo` is whether `getProjectile` found ammunition, always true in creative. Set `cancel` to fail the use: the bow is not drawn. Crossbows and tridents do not ask, and the client's own prediction does not.
- **`native_living_get_projectile(player, name, entity, kind, weapon, projectile)`**: the ammunition `LivingEntity.getProjectile(weapon)` returns (NeoForge's `LivingGetProjectileEvent`). Every mod gets the event with `projectile` set to the vanilla ammunition (the stack found in the hands or inventory, a mob's held projectile or a new arrow, a creative player's default arrow, or empty) and may change or empty it. It is asked on the bow's start and release, the crossbow's start and draw, the client's prediction of those starts, a skeleton's or illusioner's shot, and a pillager's or piglin's crossbow load (`entity` and `kind` name the mob, `player` is empty); a mob's arrow takes the replaced arrow's item and effect, a mob's crossbow also fires a replaced firework rocket, and an emptied stack leaves its crossbow uncharged and silent. A replaced stack is not taken from the inventory: the hook owns it.
- **`native_projectile_launch(player, name, entity, kind)`**: a projectile was added to the server level. `kind` is its entity type; `player` and `name` identify a player launcher, and are `None` and empty for other sources.
- **`native_entity_portal(dimension, entity, pos)`**: a server entity finished its portal wait and is beginning to leave through the portal block at `pos`. The destination follows the existing portal rules; the hook reports the transfer without changing it.
- **`native_furnace_extract(player, name, pos, item, count)`**: a server player takes furnace output at `pos`, after the output is removed; the player state is not swapped in.
@@ -0,0 +1,17 @@
{
"hooks": [
{
"name": "arrow_nock",
"target": "horizon_client::game::native_hooks::Game<'_>::native_arrow_nock",
"side": "both",
"cancellable": true,
"neoforge": "net.neoforged.neoforge.event.entity.player.ArrowNockEvent (NeoForge.EVENT_BUS, ICancellableEvent)",
"mod_api": {
"contract": "cancel",
"out": "cancel"
}
}
],
"rust_summary": "A player is about to nock a bow (`native_arrow_nock`).",
"doc_summary": "A player is about to nock a bow: `native_arrow_nock(player, name, bow, hand, has_ammo, cancel)` is asked where `BowItem.use` would start using the item. `has_ammo` is whether `getProjectile` found ammunition (creative always has). Set `cancel` to make the use fail, as `InteractionResult.FAIL`. Crossbows and tridents do not ask. Bundles need \"hooks\": @API:arrow_nock@."
}
@@ -0,0 +1,16 @@
{
"hooks": [
{
"name": "living_get_projectile",
"target": "horizon_client::game::native_hooks::Game<'_>::native_living_get_projectile",
"side": "both",
"neoforge": "net.neoforged.neoforge.event.entity.living.LivingGetProjectileEvent (NeoForge.EVENT_BUS, not cancellable)",
"mod_api": {
"contract": "collect",
"out": "projectile"
}
}
],
"rust_summary": "The ammunition a bow or crossbow shooter finds (`native_living_get_projectile`).",
"doc_summary": "The ammunition `LivingEntity.getProjectile(weapon)` returns for a player or a mob (skeleton, illusioner, pillager, piglin): `native_living_get_projectile(player, name, entity, kind, weapon, projectile)` hands every mod the event with `projectile` set to the vanilla ammunition (the held or inventory stack, a mob's held projectile or a new arrow, a creative player's default arrow, empty when a survival player has none). A mod reads it, and sets or empties it. It is asked for the bow's start and release, the crossbow's start and draw, a skeleton's or illusioner's shot, a pillager's or piglin's crossbow load, and the client's prediction of the use. A replaced stack is not taken from the inventory: the hook owns it. A mob's shot uses the stack's arrow item and effect; an emptied stack leaves a mob's crossbow uncharged. `entity` is the mob's id, empty for a player. Bundles need \"hooks\": @API:living_get_projectile@."
}