Native mods: living_incoming_damage_v2 fires after the victim's invulnerability check

An invulnerable or dead mob fired v2 although vanilla's hurtServer returns early
and NeoForge's LivingIncomingDamageEvent comes after that check. Mob::refuses_damage
is the guard damage_with already ran, now shared.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
[email protected]andClaude Sonnet 5.5 committed 2026-11-02 20:27:54 +01:00
1 parent 37b8a8bfe2
commit f152b155a6
6 files changed
+15 -11

No files matched your search

+2 -1
View File
@@ -186,7 +186,8 @@ impl Game<'_> {
let aim = self.attacker_aim(source.attacker);
let kind = self.mobs[index].kind.id.clone();
let mut amount = amount;
if !crate::mod_api::mods().is_empty() {
// After the invulnerability guard, as `LivingIncomingDamageEvent` fires.
if !crate::mod_api::mods().is_empty() && !self.mobs[index].refuses_damage(&source) {
let victim = &self.mobs[index];
let position = victim.position;
let view = super::view_vector(victim.head_yaw, victim.head_pitch);
@@ -7041,7 +7041,7 @@ impl Game<'_> {
/// this has a host and snapshots of the weapon, victim position/view and
/// DamageSource position. The acting player's network id is `causing`
/// (also the direct entity). Weapon enchantments include their levels.
/// Runs before the victim's immunity guards and v1; cancellation refuses
/// Runs after the victim's invulnerability check (an invulnerable or dead victim fires nothing) and before v1; cancellation refuses
/// delivery. Other damage routes still only fire v1. API @API:living_incoming_damage_v2@.
pub(super) fn native_living_incoming_damage_v2(
&mut self,
+8 -5
View File
@@ -2787,6 +2787,13 @@ impl Mob {
pub fn damage(&mut self, amount: f32, source: Damage) -> bool {
self.damage_with(amount, source, None)
}
/// `LivingEntity.isInvulnerableTo` and dead or removed: only #bypasses_invulnerability gets
/// past the flag. The guard every damage hook runs after (`LivingIncomingDamageEvent`).
pub fn refuses_damage(&self, source: &Damage) -> bool {
!self.alive()
|| self.removed
|| (self.invulnerable_flag && !source.kind.bypasses_invulnerability())
}
/// `damage` by a source with a weapon: `armor_effectiveness` runs the
/// weapon's armor-effectiveness enchantments (breach) over the armor fraction.
pub fn damage_with(
@@ -2807,11 +2814,7 @@ impl Mob {
// about, so `Player.attack` goes on with its extra knockback and its aftermath.
return sulfur_cube::immune_to(&source);
}
// LivingEntity.isInvulnerableTo: only #bypasses_invulnerability gets past it.
if !self.alive()
|| self.removed
|| (self.invulnerable_flag && !source.kind.bypasses_invulnerability())
{
if self.refuses_damage(&source) {
return false;
}
// Ghast.isReflectedFireball: a fireball a player turned back is
+1 -1
View File
@@ -4523,7 +4523,7 @@ pub trait Hooks: Send + Sync {
/// this has a host and snapshots of the weapon, victim position/view and
/// DamageSource position. The acting player's network id is `causing`
/// (also the direct entity). Weapon enchantments include their levels.
/// Runs before the victim's immunity guards and v1; cancellation refuses
/// Runs after the victim's invulnerability check (an invulnerable or dead victim fires nothing) and before v1; cancellation refuses
/// delivery. Other damage routes still only fire v1. API @API:living_incoming_damage_v2@.
///
/// Return `Flow::Cancel` to take it: this sets `canceled`, and later mods are skipped.
+2 -2
View File
@@ -357,8 +357,8 @@ catalog and the hook points' sources (`native_hooks.rs`, `gfx/native_render.rs`,
It receives a mutable `Host`, attacker/victim network ids, damage type, optional source position, victim position
and current view vector, and an optional weapon snapshot. `weapon.enchantments()` exposes enchantment ids and
levels. Change `amount` or return `Flow::Cancel`; the host can read the world and play sounds through its helpers.
It currently fires for player-to-mob melee, sweep and spear hits (including LAN guests), before the victim's
immunity guards and v1. Unless canceled, v1 receives the modified amount. It is not a replacement for v1 on
It currently fires for player-to-mob melee, sweep and spear hits (including LAN guests), after the victim's invulnerability check (an invulnerable or dead victim fires nothing) and before
v1. Unless canceled, v1 receives the modified amount. It is not a replacement for v1 on
projectiles, player victims, dragons, mob attacks or environmental damage. There is no extra per-hit allocation
without loaded mods. The fragment `living_incoming_damage_v2.json` receives its API number at merge.
@@ -13,5 +13,5 @@
}
],
"rust_summary": "a host-aware player-weapon damage hook (`native_living_incoming_damage_v2`), leaving v1 compatible.",
"doc_summary": "`native_living_incoming_damage_v2`: a host-aware damage hook for a player's melee, sweep and spear hits on mobs, including LAN guests. It supplies the attacker/victim network ids, damage type, optional source position, victim position/view vector and the weapon snapshot (read `ItemStack::enchantments()` for ids and levels). Mods can change damage, cancel delivery and use Host helpers, including sounds. It fires before the victim's immunity guards and v1; v1 then sees the changed amount unless v2 canceled. Projectiles, player victims, dragons, mob attacks and environmental damage still only fire v1. No extra work or allocation when no mods are loaded. This is additive, like `menu_button_v2`, not a signature change to v1. Bundles need \"hooks\": @API:living_incoming_damage_v2@."
"doc_summary": "`native_living_incoming_damage_v2`: a host-aware damage hook for a player's melee, sweep and spear hits on mobs, including LAN guests. It supplies the attacker/victim network ids, damage type, optional source position, victim position/view vector and the weapon snapshot (read `ItemStack::enchantments()` for ids and levels). Mods can change damage, cancel delivery and use Host helpers, including sounds. It fires after the victim's invulnerability check (an invulnerable or dead victim fires nothing) and before v1; v1 then sees the changed amount unless v2 canceled. Projectiles, player victims, dragons, mob attacks and environmental damage still only fire v1. No extra work or allocation when no mods are loaded. This is additive, like `menu_button_v2`, not a signature change to v1. Bundles need \"hooks\": @API:living_incoming_damage_v2@."
}