diff --git a/.github/workflows/build-prs.yml b/.github/workflows/build-prs.yml index f74b9ff476..e6f7e26618 100644 --- a/.github/workflows/build-prs.yml +++ b/.github/workflows/build-prs.yml @@ -31,23 +31,31 @@ jobs: run: git switch -C pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }} + - name: Validate wrapper + uses: gradle/actions/wrapper-validation@v3 + - name: Setup JDK 21 uses: actions/setup-java@v2 with: java-version: '21' distribution: 'temurin' - - name: Setup with Gradle - uses: gradle/gradle-build-action@v2 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 with: - arguments: setup cache-read-only: false + - name: Setup with Gradle + run: ./gradlew setup + - name: Build with Gradle - uses: gradle/gradle-build-action@v2 - with: - arguments: assemble checkFormatting - cache-read-only: false + run: ./gradlew assemble checkFormatting + + - name: Run JCC + run: ./gradlew checkJarCompatibility + + - name: Upload JCC + uses: neoforged/action-jar-compatibility/upload@v1 - name: Publish artifacts uses: neoforged/action-pr-publishing/upload@v1 diff --git a/.github/workflows/check-local-changes.yml b/.github/workflows/check-local-changes.yml new file mode 100644 index 0000000000..6a58660482 --- /dev/null +++ b/.github/workflows/check-local-changes.yml @@ -0,0 +1,48 @@ +name: Check PR local changes + +on: + pull_request: + types: + - synchronize + - opened + - ready_for_review + - reopened + +jobs: + check-local-changes: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1000 + fetch-tags: true + + # GradleUtils will append the branch name to the version, + # but for that we need a properly checked out branch + - name: Create branch for commit + run: + git switch -C pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }} + + - name: Setup JDK 21 + uses: actions/setup-java@v2 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-read-only: false + + - name: Setup with Gradle + run: ./gradlew setup + + - name: Run datagen with Gradle + run: ./gradlew :neoforge:runData :tests:runData + + - name: Check no local changes are present + run: | + # Print status for easier debugging + git status + if [ -n "$(git status --porcelain)" ]; then exit 1; fi diff --git a/.github/workflows/publish-jcc.yml b/.github/workflows/publish-jcc.yml new file mode 100644 index 0000000000..f4c78789bc --- /dev/null +++ b/.github/workflows/publish-jcc.yml @@ -0,0 +1,20 @@ +# File generated by the GradleUtils `setupGitHubActionsWorkflows` task, avoid modifying it directly +# The template can be found at https://github.com/neoforged/GradleUtils/blob/a65628b0c89dec60b357ce3f8f6bfa62934b8357/src/actionsTemplate/resources/.github/workflows/publish-jcc.yml + +name: Publish PR JCC output + +on: + workflow_run: + workflows: [Build PRs] + types: + - completed + +jobs: + publish-jcc: + if: true + uses: neoforged/actions/.github/workflows/publish-jcc.yml@main + with: + beta_version_pattern: .*-beta.* + secrets: + JCC_GH_APP_ID: ${{ secrets.JCC_GH_APP_ID }} + JCC_GH_APP_KEY: ${{ secrets.JCC_GH_APP_KEY }} diff --git a/.github/workflows/test-prs.yml b/.github/workflows/test-prs.yml index fd33722179..0fff8674c4 100644 --- a/.github/workflows/test-prs.yml +++ b/.github/workflows/test-prs.yml @@ -34,23 +34,19 @@ jobs: java-version: '21' distribution: 'temurin' - - name: Setup with Gradle - uses: gradle/gradle-build-action@v2 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 with: - arguments: setup cache-read-only: false + - name: Setup with Gradle + run: ./gradlew setup + - name: Run game tests with Gradle - uses: gradle/gradle-build-action@v2 - with: - arguments: :tests:runGameTestServer - cache-read-only: false + run: ./gradlew :tests:runGameTestServer - name: Run JUnit tests with Gradle - uses: gradle/gradle-build-action@v2 - with: - arguments: :tests:runUnitTests - cache-read-only: false + run: ./gradlew :tests:runUnitTests - name: Store reports if: failure() diff --git a/build.gradle b/build.gradle index 183cf8203f..0dcce49b15 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ import java.util.regex.Matcher import java.util.regex.Pattern plugins { - id 'net.neoforged.gradleutils' version '3.0.0-alpha.10' apply false + id 'net.neoforged.gradleutils' version '3.0.0-alpha.13' apply false id 'com.diffplug.spotless' version '6.22.0' apply false id 'net.neoforged.licenser' version '0.7.2' id 'neoforge.formatting-conventions' diff --git a/patches/net/minecraft/client/player/LocalPlayer.java.patch b/patches/net/minecraft/client/player/LocalPlayer.java.patch index eeebdce01e..632b558ef9 100644 --- a/patches/net/minecraft/client/player/LocalPlayer.java.patch +++ b/patches/net/minecraft/client/player/LocalPlayer.java.patch @@ -1,13 +1,5 @@ --- a/net/minecraft/client/player/LocalPlayer.java +++ b/net/minecraft/client/player/LocalPlayer.java -@@ -161,6 +_,7 @@ - - @Override - public boolean hurt(DamageSource p_108662_, float p_108663_) { -+ net.neoforged.neoforge.common.CommonHooks.onPlayerAttack(this, p_108662_, p_108663_); - return false; - } - @@ -297,6 +_,7 @@ ServerboundPlayerActionPacket.Action serverboundplayeractionpacket$action = p_108701_ ? ServerboundPlayerActionPacket.Action.DROP_ALL_ITEMS diff --git a/patches/net/minecraft/client/player/RemotePlayer.java.patch b/patches/net/minecraft/client/player/RemotePlayer.java.patch deleted file mode 100644 index afa4ad0f6c..0000000000 --- a/patches/net/minecraft/client/player/RemotePlayer.java.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- a/net/minecraft/client/player/RemotePlayer.java -+++ b/net/minecraft/client/player/RemotePlayer.java -@@ -33,6 +_,7 @@ - - @Override - public boolean hurt(DamageSource p_108772_, float p_108773_) { -+ net.neoforged.neoforge.common.CommonHooks.onPlayerAttack(this, p_108772_, p_108773_); - return true; - } - diff --git a/patches/net/minecraft/client/renderer/block/model/ItemTransforms.java.patch b/patches/net/minecraft/client/renderer/block/model/ItemTransforms.java.patch index 6356131987..a341611fbb 100644 --- a/patches/net/minecraft/client/renderer/block/model/ItemTransforms.java.patch +++ b/patches/net/minecraft/client/renderer/block/model/ItemTransforms.java.patch @@ -40,7 +40,7 @@ this.thirdPersonLeftHand = p_111798_; this.thirdPersonRightHand = p_111799_; this.firstPersonLeftHand = p_111800_; -@@ -64,6 +_,7 @@ +@@ -64,9 +_,21 @@ this.gui = p_111803_; this.ground = p_111804_; this.fixed = p_111805_; @@ -48,6 +48,20 @@ } public ItemTransform getTransform(ItemDisplayContext p_270619_) { ++ if (p_270619_.isModded()) { ++ ItemTransform moddedTransform = moddedTransforms.get(p_270619_); ++ if (moddedTransform != null) { ++ return moddedTransform; ++ } ++ ItemDisplayContext moddedFallback = p_270619_.fallback(); ++ if (moddedFallback == null) { ++ return ItemTransform.NO_TRANSFORM; ++ } ++ p_270619_ = moddedFallback; ++ } + return switch (p_270619_) { + case THIRD_PERSON_LEFT_HAND -> this.thirdPersonLeftHand; + case THIRD_PERSON_RIGHT_HAND -> this.thirdPersonRightHand; @@ -104,9 +_,23 @@ ItemTransform itemtransform5 = this.getTransform(p_111822_, jsonobject, ItemDisplayContext.GUI); ItemTransform itemtransform6 = this.getTransform(p_111822_, jsonobject, ItemDisplayContext.GROUND); diff --git a/patches/net/minecraft/world/entity/Entity.java.patch b/patches/net/minecraft/world/entity/Entity.java.patch index 88b10396c3..9df92f179a 100644 --- a/patches/net/minecraft/world/entity/Entity.java.patch +++ b/patches/net/minecraft/world/entity/Entity.java.patch @@ -291,6 +291,19 @@ } public boolean is(Entity p_20356_) { +@@ -2516,10 +_,11 @@ + } + + public boolean isInvulnerableTo(DamageSource p_20122_) { +- return this.isRemoved() ++ boolean isVanillaInvulnerable = this.isRemoved() + || this.invulnerable && !p_20122_.is(DamageTypeTags.BYPASSES_INVULNERABILITY) && !p_20122_.isCreativePlayer() + || p_20122_.is(DamageTypeTags.IS_FIRE) && this.fireImmune() + || p_20122_.is(DamageTypeTags.IS_FALL) && this.getType().is(EntityTypeTags.FALL_DAMAGE_IMMUNE); ++ return net.neoforged.neoforge.common.CommonHooks.isEntityInvulnerableTo(this, p_20122_, isVanillaInvulnerable); + } + + public boolean isInvulnerable() { @@ -2544,6 +_,7 @@ @Nullable diff --git a/patches/net/minecraft/world/entity/LivingEntity.java.patch b/patches/net/minecraft/world/entity/LivingEntity.java.patch index e99e001ebf..e5fd781754 100644 --- a/patches/net/minecraft/world/entity/LivingEntity.java.patch +++ b/patches/net/minecraft/world/entity/LivingEntity.java.patch @@ -9,6 +9,21 @@ private static final Logger LOGGER = LogUtils.getLogger(); private static final String TAG_ACTIVE_EFFECTS = "active_effects"; private static final ResourceLocation SPEED_MODIFIER_POWDER_SNOW_ID = ResourceLocation.withDefaultNamespace("powder_snow"); +@@ -254,6 +_,14 @@ + private boolean skipDropExperience; + private final Reference2ObjectMap> activeLocationDependentEnchantments = new Reference2ObjectArrayMap<>(); + protected float appliedScale = 1.0F; ++ /** ++ * This field stores information about damage dealt to this entity. ++ * a new {@link net.neoforged.neoforge.common.damagesource.DamageContainer} is instantiated ++ * via {@link #hurt(DamageSource, float)} after invulnerability checks, and is removed from ++ * the stack before the method's return. ++ **/ ++ @Nullable ++ protected java.util.Stack damageContainers = new java.util.Stack<>(); + + protected LivingEntity(EntityType p_20966_, Level p_20967_) { + super(p_20966_, p_20967_); @@ -320,7 +_,9 @@ .add(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE) .add(Attributes.WATER_MOVEMENT_EFFICIENCY) @@ -152,36 +167,70 @@ float f = this.getHealth(); if (f > 0.0F) { this.setHealth(f + p_21116_); -@@ -1081,6 +_,7 @@ - - @Override - public boolean hurt(DamageSource p_21016_, float p_21017_) { -+ if (!net.neoforged.neoforge.common.CommonHooks.onLivingAttack(this, p_21016_, p_21017_)) return false; - if (this.isInvulnerableTo(p_21016_)) { +@@ -1090,23 +_,30 @@ + } else if (p_21016_.is(DamageTypeTags.IS_FIRE) && this.hasEffect(MobEffects.FIRE_RESISTANCE)) { return false; - } else if (this.level().isClientSide) { -@@ -1099,14 +_,17 @@ + } else { ++ this.damageContainers.push(new net.neoforged.neoforge.common.damagesource.DamageContainer(p_21016_, p_21017_)); ++ if (net.neoforged.neoforge.common.CommonHooks.onEntityIncomingDamage(this, this.damageContainers.peek())) return false; + if (this.isSleeping() && !this.level().isClientSide) { + this.stopSleeping(); + } + + this.noActionTime = 0; ++ p_21017_ = this.damageContainers.peek().getNewDamage(); //Neo: enforce damage container as source of truth for damage amount + float f = p_21017_; boolean flag = false; float f1 = 0.0F; - if (p_21017_ > 0.0F && this.isDamageSourceBlocked(p_21016_)) { +- if (p_21017_ > 0.0F && this.isDamageSourceBlocked(p_21016_)) { - this.hurtCurrentlyUsedShield(p_21017_); - f1 = p_21017_; - p_21017_ = 0.0F; -+ net.neoforged.neoforge.event.entity.living.ShieldBlockEvent ev = net.neoforged.neoforge.common.CommonHooks.onShieldBlock(this, p_21016_, p_21017_); -+ if(!ev.isCanceled()) { -+ if(ev.shieldTakesDamage()) this.hurtCurrentlyUsedShield(p_21017_); ++ net.neoforged.neoforge.event.entity.living.LivingShieldBlockEvent ev; ++ if (p_21017_ > 0.0F && (ev = net.neoforged.neoforge.common.CommonHooks.onDamageBlock(this, this.damageContainers.peek(), this.isDamageSourceBlocked(p_21016_))).getBlocked()) { ++ this.damageContainers.peek().setBlockedDamage(ev); ++ if(ev.shieldDamage() > 0) { ++ this.hurtCurrentlyUsedShield(ev.shieldDamage()); ++ } + f1 = ev.getBlockedDamage(); -+ p_21017_ -= ev.getBlockedDamage(); ++ p_21017_ = ev.getDamageContainer().getNewDamage(); if (!p_21016_.is(DamageTypeTags.IS_PROJECTILE) && p_21016_.getDirectEntity() instanceof LivingEntity livingentity) { this.blockUsingShield(livingentity); } - flag = true; + flag = p_21017_ <= 0; -+ } } if (p_21016_.is(DamageTypeTags.IS_FREEZING) && this.getType().is(EntityTypeTags.FREEZE_HURTS_EXTRA_TYPES)) { +@@ -1118,10 +_,12 @@ + p_21017_ *= 0.75F; + } + ++ this.damageContainers.peek().setNewDamage(p_21017_); //update container with vanilla changes + this.walkAnimation.setSpeed(1.5F); + boolean flag1 = true; + if ((float)this.invulnerableTime > 10.0F && !p_21016_.is(DamageTypeTags.BYPASSES_COOLDOWN)) { + if (p_21017_ <= this.lastHurt) { ++ this.damageContainers.pop(); + return false; + } + +@@ -1130,12 +_,13 @@ + flag1 = false; + } else { + this.lastHurt = p_21017_; +- this.invulnerableTime = 20; ++ this.invulnerableTime = this.damageContainers.peek().getPostAttackInvulnerabilityTicks(); + this.actuallyHurt(p_21016_, p_21017_); + this.hurtDuration = 10; + this.hurtTime = this.hurtDuration; + } + ++ p_21017_ = this.damageContainers.peek().getNewDamage(); //update local with container value + Entity entity = p_21016_.getEntity(); + if (entity != null) { + if (entity instanceof LivingEntity livingentity1 @@ -1147,9 +_,9 @@ if (entity instanceof Player player1) { this.lastHurtByPlayerTime = 100; @@ -203,6 +252,14 @@ } } +@@ -1220,6 +_,7 @@ + CriteriaTriggers.PLAYER_HURT_ENTITY.trigger((ServerPlayer)entity, this, p_21016_, f, p_21017_, flag); + } + ++ this.damageContainers.pop(); + return flag2; + } + } @@ -1240,7 +_,7 @@ for (InteractionHand interactionhand : InteractionHand.values()) { @@ -330,9 +387,20 @@ this.playSound(soundtype.getFallSound(), soundtype.getVolume() * 0.5F, soundtype.getPitch() * 0.75F); } } -@@ -1649,9 +_,9 @@ +@@ -1616,6 +_,8 @@ + if (!(p_330394_ <= 0.0F)) { + int i = (int)Math.max(1.0F, p_330394_ / 4.0F); + ++ net.neoforged.neoforge.common.CommonHooks.onArmorHurt(p_330843_, p_331314_, p_330394_, this); ++ if (true) return; //Neo: invalidates the loop. armor damage happens in common hook + for (EquipmentSlot equipmentslot : p_331314_) { + ItemStack itemstack = this.getItemBySlot(equipmentslot); + if (itemstack.getItem() instanceof ArmorItem && itemstack.canBeHurtBy(p_330843_)) { +@@ -1648,10 +_,11 @@ + p_21194_ = Math.max(f / 25.0F, 0.0F); float f2 = f1 - p_21194_; if (f2 > 0.0F && f2 < 3.4028235E37F) { ++ this.damageContainers.peek().setReduction(net.neoforged.neoforge.common.damagesource.DamageContainer.Reduction.MOB_EFFECTS, f2); if (this instanceof ServerPlayer) { - ((ServerPlayer)this).awardStat(Stats.DAMAGE_RESISTED, Math.round(f2 * 10.0F)); + ((ServerPlayer)this).awardStat(Stats.CUSTOM.get(Stats.DAMAGE_RESISTED), Math.round(f2 * 10.0F)); @@ -342,23 +410,44 @@ } } } -@@ -1679,6 +_,8 @@ +@@ -1670,6 +_,7 @@ + + if (f3 > 0.0F) { + p_21194_ = CombatRules.getDamageAfterMagicAbsorb(p_21194_, f3); ++ this.damageContainers.peek().setReduction(net.neoforged.neoforge.common.damagesource.DamageContainer.Reduction.ENCHANTMENTS,this.damageContainers.peek().getNewDamage() - p_21194_); + } + + return p_21194_; +@@ -1679,11 +_,14 @@ protected void actuallyHurt(DamageSource p_21240_, float p_21241_) { if (!this.isInvulnerableTo(p_21240_)) { -+ p_21241_ = net.neoforged.neoforge.common.CommonHooks.onLivingHurt(this, p_21240_, p_21241_); -+ if (p_21241_ <= 0) return; - p_21241_ = this.getDamageAfterArmorAbsorb(p_21240_, p_21241_); - p_21241_ = this.getDamageAfterMagicAbsorb(p_21240_, p_21241_); - float f1 = Math.max(p_21241_ - this.getAbsorptionAmount(), 0.0F); -@@ -1688,6 +_,7 @@ +- p_21241_ = this.getDamageAfterArmorAbsorb(p_21240_, p_21241_); +- p_21241_ = this.getDamageAfterMagicAbsorb(p_21240_, p_21241_); +- float f1 = Math.max(p_21241_ - this.getAbsorptionAmount(), 0.0F); +- this.setAbsorptionAmount(this.getAbsorptionAmount() - (p_21241_ - f1)); +- float f = p_21241_ - f1; ++ this.damageContainers.peek().setReduction(net.neoforged.neoforge.common.damagesource.DamageContainer.Reduction.ARMOR, this.damageContainers.peek().getNewDamage() - this.getDamageAfterArmorAbsorb(p_21240_, this.damageContainers.peek().getNewDamage())); ++ this.getDamageAfterMagicAbsorb(p_21240_, this.damageContainers.peek().getNewDamage()); ++ this.damageContainers.peek().setReduction(net.neoforged.neoforge.common.damagesource.DamageContainer.Reduction.ABSORPTION, this.getAbsorptionAmount()); ++ float f1 = Math.max(this.damageContainers.peek().getNewDamage() - this.damageContainers.peek().getReduction(net.neoforged.neoforge.common.damagesource.DamageContainer.Reduction.ABSORPTION), 0.0F); ++ this.setAbsorptionAmount(this.damageContainers.peek().getReduction(net.neoforged.neoforge.common.damagesource.DamageContainer.Reduction.ABSORPTION) - (this.damageContainers.peek().getNewDamage() - f1)); ++ f1 = net.neoforged.neoforge.common.CommonHooks.onLivingDamagePre(this, this.damageContainers.peek()); ++ if (f1 <= 0) return; ++ float f = this.damageContainers.peek().getNewDamage() - f1; + if (f > 0.0F && f < 3.4028235E37F && p_21240_.getEntity() instanceof ServerPlayer serverplayer) { serverplayer.awardStat(Stats.DAMAGE_DEALT_ABSORBED, Math.round(f * 10.0F)); } - -+ f1 = net.neoforged.neoforge.common.CommonHooks.onLivingDamage(this, p_21240_, f1); - if (f1 != 0.0F) { - this.getCombatTracker().recordDamage(p_21240_, f1); +@@ -1693,7 +_,9 @@ this.setHealth(this.getHealth() - f1); + this.setAbsorptionAmount(this.getAbsorptionAmount() - f1); + this.gameEvent(GameEvent.ENTITY_DAMAGE); ++ this.onDamageTaken(this.damageContainers.peek()); + } ++ net.neoforged.neoforge.common.CommonHooks.onLivingDamagePost(this, this.damageContainers.peek()); + } + } + @@ -1747,6 +_,8 @@ } diff --git a/patches/net/minecraft/world/entity/player/Player.java.patch b/patches/net/minecraft/world/entity/player/Player.java.patch index 1ee123df92..c28d275637 100644 --- a/patches/net/minecraft/world/entity/player/Player.java.patch +++ b/patches/net/minecraft/world/entity/player/Player.java.patch @@ -116,14 +116,6 @@ @Override public void readAdditionalSaveData(CompoundTag p_36215_) { super.readAdditionalSaveData(p_36215_); -@@ -859,6 +_,7 @@ - - @Override - public boolean hurt(DamageSource p_36154_, float p_36155_) { -+ if (!net.neoforged.neoforge.common.CommonHooks.onPlayerAttack(this, p_36154_, p_36155_)) return false; - if (this.isInvulnerableTo(p_36154_)) { - return false; - } else if (this.abilities.invulnerable && !p_36154_.is(DamageTypeTags.BYPASSES_INVULNERABILITY)) { @@ -872,7 +_,9 @@ this.removeEntitiesOnShoulder(); } @@ -159,20 +151,36 @@ if (this.useItem.isEmpty()) { if (interactionhand == InteractionHand.MAIN_HAND) { this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY); -@@ -952,10 +_,13 @@ +@@ -952,11 +_,14 @@ @Override protected void actuallyHurt(DamageSource p_36312_, float p_36313_) { if (!this.isInvulnerableTo(p_36312_)) { -+ p_36313_ = net.neoforged.neoforge.common.CommonHooks.onLivingHurt(this, p_36312_, p_36313_); -+ if (p_36313_ <= 0) return; - p_36313_ = this.getDamageAfterArmorAbsorb(p_36312_, p_36313_); - p_36313_ = this.getDamageAfterMagicAbsorb(p_36312_, p_36313_); - float f1 = Math.max(p_36313_ - this.getAbsorptionAmount(), 0.0F); - this.setAbsorptionAmount(this.getAbsorptionAmount() - (p_36313_ - f1)); -+ f1 = net.neoforged.neoforge.common.CommonHooks.onLivingDamage(this, p_36312_, f1); - float f = p_36313_ - f1; +- p_36313_ = this.getDamageAfterArmorAbsorb(p_36312_, p_36313_); +- p_36313_ = this.getDamageAfterMagicAbsorb(p_36312_, p_36313_); +- float f1 = Math.max(p_36313_ - this.getAbsorptionAmount(), 0.0F); +- this.setAbsorptionAmount(this.getAbsorptionAmount() - (p_36313_ - f1)); +- float f = p_36313_ - f1; ++ this.damageContainers.peek().setReduction(net.neoforged.neoforge.common.damagesource.DamageContainer.Reduction.ARMOR, this.damageContainers.peek().getNewDamage() - this.getDamageAfterArmorAbsorb(p_36312_, this.damageContainers.peek().getNewDamage())); ++ this.getDamageAfterMagicAbsorb(p_36312_, this.damageContainers.peek().getNewDamage()); ++ this.damageContainers.peek().setReduction(net.neoforged.neoforge.common.damagesource.DamageContainer.Reduction.ABSORPTION, this.getAbsorptionAmount()); ++ float f1 = Math.max(this.damageContainers.peek().getNewDamage() - this.damageContainers.peek().getReduction(net.neoforged.neoforge.common.damagesource.DamageContainer.Reduction.ABSORPTION), 0.0F); ++ this.setAbsorptionAmount(this.damageContainers.peek().getReduction(net.neoforged.neoforge.common.damagesource.DamageContainer.Reduction.ABSORPTION) - (this.damageContainers.peek().getNewDamage() - f1)); ++ f1 = net.neoforged.neoforge.common.CommonHooks.onLivingDamagePre(this, this.damageContainers.peek()); ++ if (f1 <= 0) return; ++ float f = this.damageContainers.peek().getNewDamage() - f1; if (f > 0.0F && f < 3.4028235E37F) { this.awardStat(Stats.DAMAGE_ABSORBED, Math.round(f * 10.0F)); + } +@@ -970,7 +_,9 @@ + } + + this.gameEvent(GameEvent.ENTITY_DAMAGE); ++ this.onDamageTaken(this.damageContainers.peek()); + } ++ net.neoforged.neoforge.common.CommonHooks.onLivingDamagePost(this, this.damageContainers.peek()); + } + } + @@ -1014,6 +_,8 @@ return InteractionResult.PASS; diff --git a/projects/neoforge/build.gradle b/projects/neoforge/build.gradle index 119dcbea8e..8fdcec818e 100644 --- a/projects/neoforge/build.gradle +++ b/projects/neoforge/build.gradle @@ -1,6 +1,10 @@ +import net.neoforged.jarcompatibilitychecker.gradle.JCCPlugin +import net.neoforged.jarcompatibilitychecker.gradle.ProvideNeoForgeJarTask + plugins { id 'java-library' id 'maven-publish' + id 'net.neoforged.jarcompatibilitychecker' version '0.1.9' } apply plugin: 'net.neoforged.gradleutils' @@ -18,6 +22,24 @@ dynamicProject { rootProject.layout.projectDirectory.dir('rejects')) } +final checkVersion = JCCPlugin.providePreviousVersion( + project.providers, project.providers.provider({['https://maven.neoforged.net/releases']}), project.providers.provider({'net.neoforged:neoforge'}) +) +final createCompatJar = tasks.register('createCompatibilityCheckJar', ProvideNeoForgeJarTask) { + // Use the same jar that the patches were generated against + cleanJar.set(tasks.generateClientBinaryPatches.clean) + maven.set('https://maven.neoforged.net/releases') + artifact.set('net.neoforged:neoforge') + version.set(checkVersion) + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(java_version) + } +} +checkJarCompatibility { + isAPI = true + baseJar = createCompatJar.flatMap { it.output } +} + installerProfile { profile = 'NeoForge' } diff --git a/settings.gradle b/settings.gradle index 1ac035355b..589f0b3ad1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,7 +7,7 @@ pluginManagement { } plugins { - id 'net.neoforged.gradle.platform' version '7.0.142' + id 'net.neoforged.gradle.platform' version '7.0.149' } rootProject.name = rootDir.name diff --git a/src/generated/resources/data/c/tags/item/tools/bow.json b/src/generated/resources/data/c/tags/item/tools/bow.json index d0dad42854..0e7a3eb3a2 100644 --- a/src/generated/resources/data/c/tags/item/tools/bow.json +++ b/src/generated/resources/data/c/tags/item/tools/bow.json @@ -1,8 +1,8 @@ { "values": [ - "minecraft:brush", + "minecraft:bow", { - "id": "#c:tools/brushes", + "id": "#forge:tools/bows", "required": false }, { diff --git a/src/generated/resources/reports/registry_order.json b/src/generated/resources/reports/registry_order.json index c7e0a8ef88..17ea12b63d 100644 --- a/src/generated/resources/reports/registry_order.json +++ b/src/generated/resources/reports/registry_order.json @@ -81,7 +81,6 @@ "neoforge:attachment_types", "neoforge:biome_modifier_serializers", "neoforge:condition_codecs", - "neoforge:display_contexts", "neoforge:entity_data_serializers", "neoforge:fluid_ingredient_type", "neoforge:fluid_type", diff --git a/src/main/java/net/neoforged/neoforge/common/CommonHooks.java b/src/main/java/net/neoforged/neoforge/common/CommonHooks.java index 874ce193eb..494cbf2652 100644 --- a/src/main/java/net/neoforged/neoforge/common/CommonHooks.java +++ b/src/main/java/net/neoforged/neoforge/common/CommonHooks.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -98,6 +99,7 @@ import net.minecraft.world.inventory.ContainerLevelAccess; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.AdventureModePredicate; +import net.minecraft.world.item.ArmorItem; import net.minecraft.world.item.BucketItem; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.EnchantedBookItem; @@ -124,7 +126,6 @@ import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.GameMasterBlock; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.pattern.BlockInWorld; import net.minecraft.world.level.chunk.ChunkAccess; @@ -146,6 +147,7 @@ import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.neoforge.client.ClientHooks; import net.neoforged.neoforge.common.conditions.ConditionalOps; +import net.neoforged.neoforge.common.damagesource.DamageContainer; import net.neoforged.neoforge.common.extensions.IEntityExtension; import net.neoforged.neoforge.common.loot.IGlobalLootModifier; import net.neoforged.neoforge.common.loot.LootModifierManager; @@ -165,10 +167,11 @@ import net.neoforged.neoforge.event.entity.EntityAttributeCreationEvent; import net.neoforged.neoforge.event.entity.EntityAttributeModificationEvent; import net.neoforged.neoforge.event.entity.EntityEvent; +import net.neoforged.neoforge.event.entity.EntityInvulnerabilityCheckEvent; import net.neoforged.neoforge.event.entity.EntityTravelToDimensionEvent; import net.neoforged.neoforge.event.entity.item.ItemTossEvent; +import net.neoforged.neoforge.event.entity.living.ArmorHurtEvent; import net.neoforged.neoforge.event.entity.living.EnderManAngerEvent; -import net.neoforged.neoforge.event.entity.living.LivingAttackEvent; import net.neoforged.neoforge.event.entity.living.LivingBreatheEvent; import net.neoforged.neoforge.event.entity.living.LivingChangeTargetEvent; import net.neoforged.neoforge.event.entity.living.LivingDamageEvent; @@ -178,12 +181,12 @@ import net.neoforged.neoforge.event.entity.living.LivingEvent; import net.neoforged.neoforge.event.entity.living.LivingFallEvent; import net.neoforged.neoforge.event.entity.living.LivingGetProjectileEvent; -import net.neoforged.neoforge.event.entity.living.LivingHurtEvent; +import net.neoforged.neoforge.event.entity.living.LivingIncomingDamageEvent; import net.neoforged.neoforge.event.entity.living.LivingKnockBackEvent; +import net.neoforged.neoforge.event.entity.living.LivingShieldBlockEvent; import net.neoforged.neoforge.event.entity.living.LivingSwapItemsEvent; import net.neoforged.neoforge.event.entity.living.LivingUseTotemEvent; import net.neoforged.neoforge.event.entity.living.MobEffectEvent; -import net.neoforged.neoforge.event.entity.living.ShieldBlockEvent; import net.neoforged.neoforge.event.entity.player.AnvilRepairEvent; import net.neoforged.neoforge.event.entity.player.AttackEntityEvent; import net.neoforged.neoforge.event.entity.player.CriticalHitEvent; @@ -236,12 +239,32 @@ public static LivingChangeTargetEvent onLivingChangeTarget(LivingEntity entity, return event; } - public static boolean onLivingAttack(LivingEntity entity, DamageSource src, float amount) { - return entity instanceof Player || !NeoForge.EVENT_BUS.post(new LivingAttackEvent(entity, src, amount)).isCanceled(); + /** + * Creates and posts an {@link EntityInvulnerabilityCheckEvent}. This is invoked in + * {@link Entity#isInvulnerableTo(DamageSource)} and returns a post-listener result + * to the invulnerability status of the entity to the damage source. + * + * @param entity the entity being checked for invulnerability + * @param source the damage source being applied for this check + * @param isInvul whether this entity is invulnerable according to preceding/vanilla logic + * @return if this entity is invulnerable + */ + public static boolean isEntityInvulnerableTo(Entity entity, DamageSource source, boolean isInvul) { + return NeoForge.EVENT_BUS.post(new EntityInvulnerabilityCheckEvent(entity, source, isInvul)).isInvulnerable(); } - public static boolean onPlayerAttack(LivingEntity entity, DamageSource src, float amount) { - return !NeoForge.EVENT_BUS.post(new LivingAttackEvent(entity, src, amount)).isCanceled(); + /** + * Called after invulnerability checks in {@link LivingEntity#hurt(DamageSource, float)}, + * this method creates and posts the first event in the LivingEntity damage sequence, + * {@link LivingIncomingDamageEvent}. + * + * @param entity the entity to receive damage + * @param container the newly instantiated container for damage to be dealt. Most properties of + * the container will be empty at this stage. + * @return if the event is cancelled and no damage will be applied to the entity + */ + public static boolean onEntityIncomingDamage(LivingEntity entity, DamageContainer container) { + return NeoForge.EVENT_BUS.post(new LivingIncomingDamageEvent(entity, container)).isCanceled(); } public static LivingKnockBackEvent onLivingKnockBack(LivingEntity target, float strength, double ratioX, double ratioZ) { @@ -254,14 +277,57 @@ public static boolean onLivingUseTotem(LivingEntity entity, DamageSource damageS return !NeoForge.EVENT_BUS.post(new LivingUseTotemEvent(entity, damageSource, totem, hand)).isCanceled(); } - public static float onLivingHurt(LivingEntity entity, DamageSource src, float amount) { - LivingHurtEvent event = new LivingHurtEvent(entity, src, amount); - return (NeoForge.EVENT_BUS.post(event).isCanceled() ? 0 : event.getAmount()); + /** + * Creates and posts an {@link LivingDamageEvent.Pre}. This is invoked in + * {@link LivingEntity#actuallyHurt(DamageSource, float)} and {@link Player#actuallyHurt(DamageSource, float)} + * and requires access to the internal field {@link LivingEntity#damageContainers} as a parameter. + * + * @param entity the entity to receive damage + * @param container the container object holding the final values of the damage pipeline while they are still mutable + * @return the current damage value to be applied to the entity's health + * + */ + public static float onLivingDamagePre(LivingEntity entity, DamageContainer container) { + return NeoForge.EVENT_BUS.post(new LivingDamageEvent.Pre(entity, container)).getContainer().getNewDamage(); + } + + /** + * Creates and posts a {@link LivingDamageEvent.Post}. This is invoked in + * {@link LivingEntity#actuallyHurt(DamageSource, float)} and {@link Player#actuallyHurt(DamageSource, float)} + * and requires access to the internal field {@link LivingEntity#damageContainers} as a parameter. + * + * @param entity the entity to receive damage + * @param container the container object holding the truly final values of the damage pipeline. The values + * of this container and used to instantiate final fields in the event. + */ + public static void onLivingDamagePost(LivingEntity entity, DamageContainer container) { + NeoForge.EVENT_BUS.post(new LivingDamageEvent.Post(entity, container)); } - public static float onLivingDamage(LivingEntity entity, DamageSource src, float amount) { - LivingDamageEvent event = new LivingDamageEvent(entity, src, amount); - return (NeoForge.EVENT_BUS.post(event).isCanceled() ? 0 : event.getAmount()); + /** + * This is invoked in {@link LivingEntity#doHurtEquipment(DamageSource, float, EquipmentSlot...)} + * and replaces the existing item hurt and break logic with an event-sensitive version. + *
+ * Each armor slot is collected and passed into a {@link ArmorHurtEvent} and posted. If not cancelled, + * the final durability loss values for each equipment item from the event will be applied. + * + * @param source the damage source applied to the entity and armor + * @param slots an array of applicable slots for damage + * @param damage the durability damage individual items will receive + * @param armoredEntity the entity wearing the armor + */ + public static void onArmorHurt(DamageSource source, EquipmentSlot[] slots, float damage, LivingEntity armoredEntity) { + EnumMap armorMap = new EnumMap<>(EquipmentSlot.class); + for (EquipmentSlot slot : slots) { + ItemStack armorPiece = armoredEntity.getItemBySlot(slot); + if (armorPiece.isEmpty()) continue; + float damageAfterFireResist = (armorPiece.getItem() instanceof ArmorItem && armorPiece.canBeHurtBy(source)) ? damage : 0; + armorMap.put(slot, new ArmorHurtEvent.ArmorEntry(armorPiece, damageAfterFireResist)); + } + + ArmorHurtEvent event = NeoForge.EVENT_BUS.post(new ArmorHurtEvent(armorMap, armoredEntity)); + if (event.isCanceled()) return; + event.getArmorMap().forEach((slot, entry) -> entry.armorItemStack.hurtAndBreak((int) entry.newDamage, armoredEntity, slot)); } public static boolean onLivingDeath(LivingEntity entity, DamageSource src) { @@ -459,7 +525,7 @@ public static void handleBlockDrops(ServerLevel level, BlockPos pos, BlockState * Fires {@link BlockEvent.BreakEvent}, pre-emptively canceling the event based on the conditions that will cause the block to not be broken anyway. *

* Note that undoing the pre-cancel will not permit breaking the block, since the vanilla conditions will always be checked. - * + * * @param level The level * @param gameType The game type of the breaking player * @param player The breaking player @@ -789,7 +855,7 @@ public interface BiomeCallbackFunction { /** * Checks if a crop can grow by firing {@link CropGrowEvent.Pre}. - * + * * @param level The level the crop is in * @param pos The position of the crop * @param state The state of the crop @@ -808,7 +874,7 @@ public static void fireCropGrowPost(Level level, BlockPos pos, BlockState state) /** * Fires the {@link CriticalHitEvent} and returns the resulting event. - * + * * @param player The attacking player * @param target The attack target * @param vanillaCritical If the attack would have been a critical hit by vanilla's rules in {@link Player#attack(Entity)}. @@ -999,8 +1065,19 @@ public static void onEntityEnterSection(Entity entity, long packedOldPos, long p NeoForge.EVENT_BUS.post(new EntityEvent.EnteringSection(entity, packedOldPos, packedNewPos)); } - public static ShieldBlockEvent onShieldBlock(LivingEntity blocker, DamageSource source, float blocked) { - ShieldBlockEvent e = new ShieldBlockEvent(blocker, source, blocked); + /** + * Creates, posts, and returns a {@link LivingShieldBlockEvent}. This method is invoked in + * {@link LivingEntity#hurt(DamageSource, float)} and requires internal access to the top entry + * in the protected field {@link LivingEntity#damageContainers} as a parameter. + * + * @param blocker the entity performing the block + * @param container the entity's internal damage container for accessing current values + * in the damage pipeline at the time of this invocation. + * @param originalBlocked whether this entity is blocking according to preceding/vanilla logic + * @return the event object after event listeners have been invoked. + */ + public static LivingShieldBlockEvent onDamageBlock(LivingEntity blocker, DamageContainer container, boolean originalBlocked) { + LivingShieldBlockEvent e = new LivingShieldBlockEvent(blocker, container, originalBlocked); NeoForge.EVENT_BUS.post(e); return e; } @@ -1375,7 +1452,7 @@ public static void onChunkUnload(PoiManager poiManager, ChunkAccess chunkAccess) /** * Checks if a mob effect can be applied to an entity by firing {@link MobEffectEvent.Applicable}. - * + * * @param entity The target entity the mob effect is being applied to. * @param effect The mob effect being applied. * @return True if the mob effect can be applied, otherwise false. @@ -1389,7 +1466,7 @@ public static boolean canMobEffectBeApplied(LivingEntity entity, MobEffectInstan * Attempts to resolve a {@link RegistryLookup} using the current global state. *

* Prioritizes the server's lookup, only attempting to retrieve it from the client if the server is unavailable. - * + * * @param The type of registry being looked up * @param key The resource key for the target registry * @return A registry access, if one was available. diff --git a/src/main/java/net/neoforged/neoforge/common/damagesource/DamageContainer.java b/src/main/java/net/neoforged/neoforge/common/damagesource/DamageContainer.java new file mode 100644 index 0000000000..5a912d1d87 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/damagesource/DamageContainer.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.common.damagesource; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.LivingEntity; +import net.neoforged.neoforge.event.entity.EntityInvulnerabilityCheckEvent; +import net.neoforged.neoforge.event.entity.living.LivingDamageEvent; +import net.neoforged.neoforge.event.entity.living.LivingIncomingDamageEvent; +import net.neoforged.neoforge.event.entity.living.LivingShieldBlockEvent; +import org.jetbrains.annotations.ApiStatus; + +/** + * DamageContainer encapsulates aspects of the entity damage sequence so that + * relevant context related to damage dealt is accessible throughout the entire + * sequence. + *

Note: certain values will be defaults until the stage in the sequence when they are set.

+ *

The Damage Sequence

+ *
    + *
  1. {@link LivingEntity#hurt} is invoked on the recipient from the source of + * the attack.
  2. + *
  3. {@link Entity#isInvulnerableTo} is invoked and fires {@link EntityInvulnerabilityCheckEvent}.
  4. + *
  5. After determining the entity is vulnerable, the {@link DamageContainer} in instantiated for the entity.
  6. + *
  7. {@link LivingIncomingDamageEvent} is fired.
  8. + *
  9. {@link LivingShieldBlockEvent} fires and the result determines if shield effects apply.
  10. + *
  11. {@link LivingEntity#actuallyHurt} is called.
  12. + *
  13. armor, magic, mob_effect, and absorption reductions are captured in the DamageContainer.
  14. + *
  15. {@link LivingDamageEvent.Pre} is fired.
  16. + *
  17. if the damage is not zero, entity health is modified and recorded and {@link LivingDamageEvent.Post} is fired.
  18. + *
+ */ +@ApiStatus.Internal +public class DamageContainer { + public enum Reduction { + /** Damage reduced from the effects of armor. */ + ARMOR, + /** Damage reduced from enchantments on armor. */ + ENCHANTMENTS, + /** Damage reduced from active mob effects. */ + MOB_EFFECTS, + /** Damage absorbed by absorption. */ + ABSORPTION + } + + private final EnumMap> reductionFunctions = new EnumMap<>(Reduction.class); + private final float originalDamage; + private final DamageSource source; + private float newDamage; + private final EnumMap reductions = new EnumMap<>(Reduction.class); + private float blockedDamage = 0f; + private float shieldDamage = 0; + private int invulnerabilityTicksAfterAttack = 20; + + public DamageContainer(DamageSource source, float originalDamage) { + this.source = source; + this.originalDamage = originalDamage; + this.newDamage = originalDamage; + } + + /** {@return the value passed into {@link LivingEntity#hurt(DamageSource, float)} before any modifications are made} */ + public float getOriginalDamage() { + return originalDamage; + } + + /** {@return the damage source for this damage sequence} */ + public DamageSource getSource() { + return source; + } + + /** + * This sets the current damage value for the entity at the stage of the damage sequence in which it is set. + * Subsequent steps in the damage sequence will use and modify this value accordingly. If this is called in + * the final step of the sequence, this value will be applied against the entity's health. + * + * @param damage the amount to harm this entity at the end of the damage sequence + */ + public void setNewDamage(float damage) { + this.newDamage = damage; + } + + /** {@return the current amount expected to be applied to the entity or used in subsequent damage calculations} */ + public float getNewDamage() { + return newDamage; + } + + /** + * Adds a callback modifier to the vanilla damage reductions. Each function will be performed in sequence + * on the vanilla value at the time the {@link DamageContainer.Reduction} type is set by vanilla. + *

Note: only the {@link LivingIncomingDamageEvent EntityPreDamageEvent} + * happens early enough in the sequence for this method to have any effect.

+ * + * @param type The reduction type your function will apply to + * @param reductionFunction takes the current reduction from vanilla and any preceding functions and returns a new + * value for the reduction. These are always executed in insertion order. if sequence + * matters, use {@link net.neoforged.bus.api.EventPriority} to order your function. + */ + + public void addModifier(Reduction type, IReductionFunction reductionFunction) { + this.reductionFunctions.computeIfAbsent(type, a -> new ArrayList<>()).add(reductionFunction); + } + + /** {@return the damage blocked during the {@link LivingShieldBlockEvent}} */ + public float getBlockedDamage() { + return blockedDamage; + } + + /** {@return the durability applied to the applicable shield after {@link LivingShieldBlockEvent} returned a successful block} */ + public float getShieldDamage() { + return shieldDamage; + } + + /** + * Explicitly sets the invulnerability ticks after the damage has been applied. + * + * @param ticks Ticks of invulnerability after this damage sequence + */ + public void setPostAttackInvulnerabilityTicks(int ticks) { + this.invulnerabilityTicksAfterAttack = ticks; + } + + /** {@return the number of ticks this entity will be invulnerable after damage is applied} */ + public int getPostAttackInvulnerabilityTicks() { + return invulnerabilityTicksAfterAttack; + } + + /** + * This provides a post-reduction value for the reduction and modifiers. This will always return zero + * before {@link LivingDamageEvent.Pre} and will consume all + * modifiers prior to the event. + * + * @param type the specific source type of the damage reduction + * @return The amount of damage reduced by armor after vanilla armor reductions and added modifiers + */ + public float getReduction(Reduction type) { + return reductions.getOrDefault(type, 0f); + } + + //=============INTERNAL METHODS - DO NOT USE=================== + + @ApiStatus.Internal + public void setBlockedDamage(LivingShieldBlockEvent event) { + if (event.getBlocked()) { + this.blockedDamage = event.getBlockedDamage(); + this.shieldDamage = event.shieldDamage(); + this.newDamage -= this.blockedDamage; + } + } + + @ApiStatus.Internal + public void setReduction(Reduction reduction, float amount) { + this.reductions.put(reduction, modifyReduction(Reduction.ABSORPTION, amount)); + this.newDamage -= Math.max(0, amount); + } + + private float modifyReduction(Reduction type, float reduction) { + for (var func : reductionFunctions.getOrDefault(type, new ArrayList<>())) { + reduction = func.modify(this, reduction); + } + return reduction; + } +} diff --git a/src/main/java/net/neoforged/neoforge/common/damagesource/IReductionFunction.java b/src/main/java/net/neoforged/neoforge/common/damagesource/IReductionFunction.java new file mode 100644 index 0000000000..03e16f18a9 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/damagesource/IReductionFunction.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.common.damagesource; + +/** + * An {@link IReductionFunction} is used by {@link DamageContainer} instances.
+ * This allows sequential modification of damage reduction values to be stored and + * later invoked before actual reductions are applied to the damage sequence. + */ +@FunctionalInterface +public interface IReductionFunction { + /** + * Consumes an existing reduction value and produces a modified value. + * + * @param container the {@link DamageContainer} representing the damage sequence + * values for the reduction being modified + * @param reductionIn the initial or preceding reduction value to this operation + * @return the new reduction value + */ + float modify(DamageContainer container, float reductionIn); +} diff --git a/src/main/java/net/neoforged/neoforge/common/extensions/ILivingEntityExtension.java b/src/main/java/net/neoforged/neoforge/common/extensions/ILivingEntityExtension.java index 3c071ad62c..2d03a767b5 100644 --- a/src/main/java/net/neoforged/neoforge/common/extensions/ILivingEntityExtension.java +++ b/src/main/java/net/neoforged/neoforge/common/extensions/ILivingEntityExtension.java @@ -5,10 +5,12 @@ package net.neoforged.neoforge.common.extensions; +import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.phys.Vec3; import net.neoforged.neoforge.common.NeoForgeMod; +import net.neoforged.neoforge.common.damagesource.DamageContainer; import net.neoforged.neoforge.fluids.FluidType; public interface ILivingEntityExtension extends IEntityExtension { @@ -64,4 +66,15 @@ default boolean canDrownInFluidType(FluidType type) { default boolean moveInFluid(FluidState state, Vec3 movementVector, double gravity) { return state.move(self(), movementVector, gravity); } + + /** + * Executes in {@link LivingEntity#hurt(DamageSource, float)} after all damage and + * effects have applied. Overriding this method is preferred over overriding the + * hurt method in custom entities where special behavior is desired after vanilla + * logic. + * + * @param damageContainer The aggregated damage details preceding this hook, which + * includes changes made to the damage sequence by events. + */ + default void onDamageTaken(DamageContainer damageContainer) {} } diff --git a/src/main/java/net/neoforged/neoforge/event/entity/EntityInvulnerabilityCheckEvent.java b/src/main/java/net/neoforged/neoforge/event/entity/EntityInvulnerabilityCheckEvent.java new file mode 100644 index 0000000000..51889eb3a0 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/event/entity/EntityInvulnerabilityCheckEvent.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.event.entity; + +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import org.jetbrains.annotations.ApiStatus; + +/** + * Fired when {@link Entity#isInvulnerableTo(DamageSource)} is invoked and determines if + * downstream hurt logic should apply. This event is fired on both sides in + * {@link Entity#isInvulnerableTo(DamageSource)} + *
+ * Note: This event may be unable to change the invulnerable status of some entities + * that override isInvulnerableTo against certain damage sources + */ +public class EntityInvulnerabilityCheckEvent extends EntityEvent { + private final boolean originallyInvulnerable; + private boolean isInvulnerable; + private final DamageSource source; + + @ApiStatus.Internal + public EntityInvulnerabilityCheckEvent(Entity entity, DamageSource source, boolean isVanillaInvulnerable) { + super(entity); + this.originallyInvulnerable = isVanillaInvulnerable; + this.isInvulnerable = isVanillaInvulnerable; + this.source = source; + } + + /** + * Sets the invulnerable status of the entity. By default, the invulnerability will be + * set by value passed into the event invocation. + */ + public void setInvulnerable(boolean isInvulnerable) { + this.isInvulnerable = isInvulnerable; + } + + /** @return the current invulnerability state */ + public boolean isInvulnerable() { + return isInvulnerable; + } + + /** @return an immutable reference to the damage source being applied to this entity */ + public DamageSource getSource() { + return source; + } + + /** @return the invulnerability status passed into the event by vanilla */ + public boolean getOriginalInvulnerability() { + return originallyInvulnerable; + } +} diff --git a/src/main/java/net/neoforged/neoforge/event/entity/living/ArmorHurtEvent.java b/src/main/java/net/neoforged/neoforge/event/entity/living/ArmorHurtEvent.java new file mode 100644 index 0000000000..56057ef1e4 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/event/entity/living/ArmorHurtEvent.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.event.entity.living; + +import java.util.EnumMap; +import java.util.Map; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.neoforged.bus.api.ICancellableEvent; +import org.jetbrains.annotations.ApiStatus; + +/** + * Fired on both sides when a {@link LivingEntity}'s armor is dealt damage in + * {@link LivingEntity#doHurtEquipment(DamageSource, float, EquipmentSlot...) doHurtEquipment}. + */ +public class ArmorHurtEvent extends LivingEvent implements ICancellableEvent { + public static class ArmorEntry { + public ItemStack armorItemStack; + public final float originalDamage; + public float newDamage; + + public ArmorEntry(ItemStack armorStack, float damageIn) { + this.armorItemStack = armorStack; + this.originalDamage = damageIn; + this.newDamage = damageIn; + } + } + + private final EnumMap armorEntries; + + @ApiStatus.Internal + public ArmorHurtEvent(EnumMap armorMap, LivingEntity player) { + super(player); + this.armorEntries = armorMap; + } + + /** + * Provides the Itemstack for the given slot. Hand slots will always return {@link ItemStack#EMPTY} + * + * @return the {@link ItemStack} to be hurt for the given slot + */ + public ItemStack getArmorItemStack(EquipmentSlot slot) { + return armorEntries.containsKey(slot) ? armorEntries.get(slot).armorItemStack : ItemStack.EMPTY; + } + + /** {@return the original damage before any event modifications} */ + public Float getOriginalDamage(EquipmentSlot slot) { + return armorEntries.containsKey(slot) ? armorEntries.get(slot).originalDamage : 0f; + } + + /** {@return the amount to hurt the armor if the event is not cancelled} */ + public Float getNewDamage(EquipmentSlot slot) { + return armorEntries.containsKey(slot) ? armorEntries.get(slot).newDamage : 0f; + } + + /** + * Sets new damage for the armor. Setting damage for empty slots will have no effect. + * + * @param damage the new amount to hurt the armor. Values below zero will be set to zero. + */ + public void setNewDamage(EquipmentSlot slot, float damage) { + if (this.armorEntries.containsKey(slot)) this.armorEntries.get(slot).newDamage = damage; + } + + /** Used internally to get the full map of {@link ItemStack}s to be hurt */ + public Map getArmorMap() { + return armorEntries; + } +} diff --git a/src/main/java/net/neoforged/neoforge/event/entity/living/LivingAttackEvent.java b/src/main/java/net/neoforged/neoforge/event/entity/living/LivingAttackEvent.java deleted file mode 100644 index 7ab20fe52b..0000000000 --- a/src/main/java/net/neoforged/neoforge/event/entity/living/LivingAttackEvent.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.event.entity.living; - -import net.minecraft.world.damagesource.DamageSource; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.player.Player; -import net.neoforged.bus.api.ICancellableEvent; -import net.neoforged.neoforge.common.CommonHooks; -import net.neoforged.neoforge.common.NeoForge; - -/** - * LivingAttackEvent is fired when a living Entity is attacked.
- * This event is fired whenever an Entity is attacked in - * {@link LivingEntity#hurt(DamageSource, float)} and - * {@link Player#hurt(DamageSource, float)}.
- *
- * This event is fired via the {@link CommonHooks#onLivingAttack(LivingEntity, DamageSource, float)}.
- *
- * {@link #source} contains the DamageSource of the attack.
- * {@link #amount} contains the amount of damage dealt to the entity.
- *
- * This event is {@link net.neoforged.bus.api.ICancellableEvent}.
- * If this event is canceled, the Entity does not take attack damage.
- *
- * This event does not have a result. {@link HasResult}
- *
- * This event is fired on the {@link NeoForge#EVENT_BUS}. - **/ -public class LivingAttackEvent extends LivingEvent implements ICancellableEvent { - private final DamageSource source; - private final float amount; - - public LivingAttackEvent(LivingEntity entity, DamageSource source, float amount) { - super(entity); - this.source = source; - this.amount = amount; - } - - public DamageSource getSource() { - return source; - } - - public float getAmount() { - return amount; - } -} diff --git a/src/main/java/net/neoforged/neoforge/event/entity/living/LivingDamageEvent.java b/src/main/java/net/neoforged/neoforge/event/entity/living/LivingDamageEvent.java index 91b3a98486..b406448858 100644 --- a/src/main/java/net/neoforged/neoforge/event/entity/living/LivingDamageEvent.java +++ b/src/main/java/net/neoforged/neoforge/event/entity/living/LivingDamageEvent.java @@ -1,54 +1,132 @@ /* - * Copyright (c) Forge Development LLC and contributors + * Copyright (c) NeoForged and contributors * SPDX-License-Identifier: LGPL-2.1-only */ package net.neoforged.neoforge.event.entity.living; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.stream.Collectors; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.LivingEntity; -import net.neoforged.bus.api.ICancellableEvent; import net.neoforged.neoforge.common.CommonHooks; +import net.neoforged.neoforge.common.damagesource.DamageContainer; /** - * LivingDamageEvent is fired just before damage is applied to entity.
- * At this point armor, potion and absorption modifiers have already been applied to damage - this is FINAL value.
- * Also note that appropriate resources (like armor durability and absorption extra hearths) have already been consumed.
- * This event is fired whenever an Entity is damaged in - * {@code LivingEntity#actuallyHurt(DamageSource, float)} and - * {@code Player#actuallyHurt(DamageSource, float)}.
+ * LivingDamageEvent captures an entity's loss of health. At this stage in + * the damage sequence, all reduction effects have been applied. *
- * This event is fired via the {@link CommonHooks#onLivingDamage(LivingEntity, DamageSource, float)}.
+ * {@link Pre} allows for modification of the damage value before it is applied + * to the entity's health. *
- * {@link #source} contains the DamageSource that caused this Entity to be hurt.
- * {@link #amount} contains the final amount of damage that will be dealt to entity.
- *
- * This event is {@link ICancellableEvent}.
- * If this event is canceled, the Entity is not hurt. Used resources WILL NOT be restored.
- *
- * This event does not have a result. {@link HasResult}
+ * {@link Post} contains an immutable representation of the entire damage sequence + * and allows for reference to the values accrued at each step. * - * @see LivingHurtEvent - **/ -public class LivingDamageEvent extends LivingEvent implements ICancellableEvent { - private final DamageSource source; - private float amount; - - public LivingDamageEvent(LivingEntity entity, DamageSource source, float amount) { + * @see DamageContainer for more information on the damage sequence + */ +public abstract class LivingDamageEvent extends LivingEvent { + private LivingDamageEvent(LivingEntity entity) { super(entity); - this.source = source; - this.amount = amount; } - public DamageSource getSource() { - return source; - } + /** + * LivingDamageEvent.Pre is fired when an Entity is set to be hurt.
+ * At this point armor, potion and absorption modifiers have already been applied to the damage value + * and the entity.
+ * This event is fired in {@code LivingEntity#actuallyHurt(DamageSource, float} + *
+ * For custom posting of this event, the event expects to be fired after + * damage reductions have been calculated but before any changes to the entity + * health has been applied. This event expects a mutable {@link DamageContainer}. + *
+ * This event is fired via the {@link CommonHooks#onLivingDamagePre(LivingEntity, DamageContainer)}. + * + * @see DamageContainer for more information on the damage sequence + **/ + public static class Pre extends LivingDamageEvent { + private final DamageContainer container; + + public Pre(LivingEntity entity, DamageContainer container) { + super(entity); + this.container = container; + } - public float getAmount() { - return amount; + public DamageContainer getContainer() { + return container; + } } - public void setAmount(float amount) { - this.amount = amount; + /** + * LivingDamageEvent.Post is fired after health is modified on the entity.
+ * The fields in this event represent the FINAL values of what was applied to the entity. + *
+ * Also note that appropriate resources (like armor durability and absorption extra hearts) have already been consumed.
+ * This event is fired whenever an Entity is damaged in {@code LivingEntity#actuallyHurt(DamageSource, float)} + *
+ * This event is fired via {@link CommonHooks#onLivingDamagePost(LivingEntity, DamageContainer)}. + * + * @see DamageContainer for more information on the damage sequence + **/ + public static class Post extends LivingDamageEvent { + private final float originalDamage; + private final DamageSource source; + private final float newDamage; + private final float blockedDamage; + private final float shieldDamage; + private final int postAttackInvulnerabilityTicks; + private final EnumMap reductions; + + public Post(LivingEntity entity, DamageContainer container) { + super(entity); + this.originalDamage = container.getOriginalDamage(); + this.source = container.getSource(); + this.newDamage = container.getNewDamage(); + this.blockedDamage = container.getBlockedDamage(); + this.shieldDamage = container.getShieldDamage(); + this.postAttackInvulnerabilityTicks = container.getPostAttackInvulnerabilityTicks(); + this.reductions = new EnumMap(Arrays.stream(DamageContainer.Reduction.values()) + .map(type -> new AbstractMap.SimpleEntry<>(type, container.getReduction(type))) + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue))); + } + + /** {@return the original damage when {@link LivingEntity#hurt} was invoked} */ + public float getOriginalDamage() { + return originalDamage; + } + + /** {@return the {@link DamageSource} for this damage sequence} */ + public DamageSource getSource() { + return source; + } + + /** {@return the amount of health this entity lost during this sequence} */ + public float getNewDamage() { + return newDamage; + } + + /** {@return the amount of damage reduced by a blocking action} */ + public float getBlockedDamage() { + return blockedDamage; + } + + /** {@return the amount of shield durability this entity lost if a blocking action was captured and the entity was holding a shield} */ + public float getShieldDamage() { + return shieldDamage; + } + + /** {@return the number of ticks this entity will be invulnerable after this sequence} */ + public int getPostAttackInvulnerabilityTicks() { + return postAttackInvulnerabilityTicks; + } + + /** + * @param reduction the type of reduction to obtain + * @return the amount of damage reduced by this reduction type. + */ + public float getReduction(DamageContainer.Reduction reduction) { + return reductions.getOrDefault(reduction, 0f); + } } } diff --git a/src/main/java/net/neoforged/neoforge/event/entity/living/LivingHurtEvent.java b/src/main/java/net/neoforged/neoforge/event/entity/living/LivingHurtEvent.java deleted file mode 100644 index b337a260e5..0000000000 --- a/src/main/java/net/neoforged/neoforge/event/entity/living/LivingHurtEvent.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.event.entity.living; - -import net.minecraft.world.damagesource.DamageSource; -import net.minecraft.world.entity.LivingEntity; -import net.neoforged.bus.api.ICancellableEvent; -import net.neoforged.neoforge.common.CommonHooks; -import net.neoforged.neoforge.common.NeoForge; - -/** - * LivingHurtEvent is fired when an Entity is set to be hurt.
- * This event is fired whenever an Entity is hurt in - * {@code LivingEntity#actuallyHurt(DamageSource, float)} and - * {@code Player#actuallyHurt(DamageSource, float)}.
- *
- * This event is fired via the {@link CommonHooks#onLivingHurt(LivingEntity, DamageSource, float)}.
- *
- * {@link #source} contains the DamageSource that caused this Entity to be hurt.
- * {@link #amount} contains the amount of damage dealt to the Entity that was hurt.
- *
- * This event is {@link ICancellableEvent}.
- * If this event is canceled, the Entity is not hurt.
- *
- * This event does not have a result. {@link HasResult}
- *
- * This event is fired on the {@link NeoForge#EVENT_BUS}. - * - * @see LivingDamageEvent - **/ -public class LivingHurtEvent extends LivingEvent implements ICancellableEvent { - private final DamageSource source; - private float amount; - - public LivingHurtEvent(LivingEntity entity, DamageSource source, float amount) { - super(entity); - this.source = source; - this.amount = amount; - } - - public DamageSource getSource() { - return source; - } - - public float getAmount() { - return amount; - } - - public void setAmount(float amount) { - this.amount = amount; - } -} diff --git a/src/main/java/net/neoforged/neoforge/event/entity/living/LivingIncomingDamageEvent.java b/src/main/java/net/neoforged/neoforge/event/entity/living/LivingIncomingDamageEvent.java new file mode 100644 index 0000000000..ae30de4bd4 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/event/entity/living/LivingIncomingDamageEvent.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.event.entity.living; + +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.LivingEntity; +import net.neoforged.bus.api.ICancellableEvent; +import net.neoforged.neoforge.common.CommonHooks; +import net.neoforged.neoforge.common.damagesource.DamageContainer; +import net.neoforged.neoforge.common.damagesource.IReductionFunction; + +/** + * LivingIncomingDamageEvent is fired when a LivingEntity is about to receive damage. + *
+ * This event is fired in {@link LivingEntity#hurt(DamageSource, float)} + * after invulnerability checks but before any damage processing/mitigation. + *
+ * For custom posting of this event, the event expects to be fired before any + * damage reductions have been calculated. This event expects a mutable {@link DamageContainer}. + *
+ * This event is fired via the {@link CommonHooks#onEntityIncomingDamage(LivingEntity, DamageContainer)}. + * + * @see DamageContainer for more information on the damage sequence + **/ +public class LivingIncomingDamageEvent extends LivingEvent implements ICancellableEvent { + private final DamageContainer container; + + public LivingIncomingDamageEvent(LivingEntity entity, DamageContainer container) { + super(entity); + this.container = container; + } + + /** {@return the container for this damage sequence} */ + public DamageContainer getContainer() { + return this.container; + } + + /** {@return the {@link DamageSource} for this damage sequence} */ + public DamageSource getSource() { + return this.container.getSource(); + } + + /** {@return the current damage to be applied to the entity} */ + public float getAmount() { + return this.container.getNewDamage(); + } + + /** {@return the damage value passed into the damage sequence before modifications} */ + public float getOriginalAmount() { + return this.container.getOriginalDamage(); + } + + /** + * @param newDamage the damage value to be used in the rest of the damage sequence. + */ + public void setAmount(float newDamage) { + this.container.setNewDamage(newDamage); + } + + /** + * Reduction modifiers alter the vanilla damage reduction before it modifies the damage value. + * Modifiers are executed in sequence. + * + * @param type the reduction type to be modified + * @param reductionFunc the function to apply to the reduction value. + */ + public void addReductionModifier(DamageContainer.Reduction type, IReductionFunction reductionFunc) { + this.container.addModifier(type, reductionFunc); + } + + /** + * When an entity's invulnerable time is fully cooled down, 20 ticks of invulnerability is added + * on the next attack. This method allows for setting a new invulnerability tick count when those + * conditions are met. + *
+ * Note: this value will be ignored if the damage is taken while invulnerability ticks are greater + * than 10 and the damage source does not bypass invulnerability + * + * @param ticks the number of ticks for the entity to remain invulnerable to incoming damage + */ + public void setInvulnerabilityTicks(int ticks) { + this.container.setPostAttackInvulnerabilityTicks(ticks); + } +} diff --git a/src/main/java/net/neoforged/neoforge/event/entity/living/LivingShieldBlockEvent.java b/src/main/java/net/neoforged/neoforge/event/entity/living/LivingShieldBlockEvent.java new file mode 100644 index 0000000000..6207535891 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/event/entity/living/LivingShieldBlockEvent.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.event.entity.living; + +import net.minecraft.util.Mth; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.LivingEntity; +import net.neoforged.bus.api.ICancellableEvent; +import net.neoforged.neoforge.common.damagesource.DamageContainer; + +/** + * LivingShieldBlockEvent is fired when an entity is hurt and vanilla checks if the entity is attempting + * to block with a shield.
+ * Cancelling this event will have the same impact as if the shield was not eligible to block.
+ * The damage blocked cannot be set lower than zero or greater than the original value.
+ *

Note: This event fires whether the player is actively using a shield or not. Vanilla shield + * blocking logic is captured and passed into the event via {@link #getOriginalBlock()}. If this is + * true, The shield item stack "should" be available from {@link LivingEntity#getUseItem()} at least + * for players.

+ * + * @see DamageContainer for more information on the damage sequence + */ +public class LivingShieldBlockEvent extends LivingEvent implements ICancellableEvent { + private final DamageContainer container; + private float dmgBlocked; + private float shieldDamage = -1; + private final boolean originalBlocked; + private boolean newBlocked; + + public LivingShieldBlockEvent(LivingEntity blocker, DamageContainer container, boolean originalBlockedState) { + super(blocker); + this.container = container; + this.dmgBlocked = container.getNewDamage(); + this.originalBlocked = originalBlockedState; + this.newBlocked = originalBlockedState; + this.shieldDamage = container.getNewDamage(); + } + + public DamageContainer getDamageContainer() { + return this.container; + } + + /** + * @return The damage source. + */ + public DamageSource getDamageSource() { + return this.getDamageContainer().getSource(); + } + + /** + * @return The original amount of damage blocked, which is the same as the original + * incoming damage value. + */ + public float getOriginalBlockedDamage() { + return this.getDamageContainer().getNewDamage(); + } + + /** + * @return The current amount of damage blocked, as a result of this event. + */ + public float getBlockedDamage() { + return Math.min(this.dmgBlocked, container.getNewDamage()); + } + + /** + * If the event is {@link #getBlocked()} and the user is holding a shield, the returned amount + * will be taken from the item's durability. + * + * @return The amount of shield durability damage to take. + */ + public float shieldDamage() { + if (newBlocked) + return shieldDamage >= 0 ? shieldDamage : getBlockedDamage(); + return 0; + } + + /** + * Set how much damage is blocked by this action.
+ * Note that initially the blocked amount is the entire attack.
+ */ + public void setBlockedDamage(float blocked) { + this.dmgBlocked = Mth.clamp(blocked, 0, this.getOriginalBlockedDamage()); + } + + /** + * Set how much durability the shield will lose if {@link #getBlocked()} is true. + * + * @param damage the new durability value taken from the shield on successful block + */ + public void setShieldDamage(float damage) { + this.shieldDamage = damage; + } + + /** + * @return whether the damage would have been blocked by vanilla logic + */ + public boolean getOriginalBlock() { + return originalBlocked; + } + + /** + * Used in {@link LivingEntity#hurt(DamageSource, float)} to signify that a blocking + * action has occurred. If returning false, damage to the shield will not occur. + * + * @return true if the entity should be considered "blocking" + */ + public boolean getBlocked() { + return newBlocked; + } + + /** + * Sets the blocking state of the entity. By default, entities raising a shield, + * facing the damage source, and not being hit by a source that bypasses shields + * will be considered blocking. An entity can be considered blocking regardless + * by supplying true to this. + * + * @param isBlocked should the entity be treated as if it is blocking + */ + public void setBlocked(boolean isBlocked) { + this.newBlocked = isBlocked; + } +} diff --git a/src/main/java/net/neoforged/neoforge/event/entity/living/ShieldBlockEvent.java b/src/main/java/net/neoforged/neoforge/event/entity/living/ShieldBlockEvent.java deleted file mode 100644 index 3f5f9275e6..0000000000 --- a/src/main/java/net/neoforged/neoforge/event/entity/living/ShieldBlockEvent.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.event.entity.living; - -import net.minecraft.util.Mth; -import net.minecraft.world.damagesource.DamageSource; -import net.minecraft.world.entity.LivingEntity; -import net.neoforged.bus.api.ICancellableEvent; - -/** - * The ShieldBlockEvent is fired when an entity successfully blocks with a shield.
- * Cancelling this event will have the same impact as if the shield was not eligible to block.
- * The damage blocked cannot be set lower than zero or greater than the original value.
- * Note: The shield item stack "should" be available from {@link LivingEntity#getUseItem()} - * at least for players. - */ -public class ShieldBlockEvent extends LivingEvent implements ICancellableEvent { - private final DamageSource source; - private final float originalBlocked; - private float dmgBlocked; - private boolean shieldTakesDamage = true; - - public ShieldBlockEvent(LivingEntity blocker, DamageSource source, float blocked) { - super(blocker); - this.source = source; - this.originalBlocked = blocked; - this.dmgBlocked = blocked; - } - - /** - * @return The damage source. - */ - public DamageSource getDamageSource() { - return this.source; - } - - /** - * @return The original amount of damage blocked, which is the same as the original - * incoming damage value. - */ - public float getOriginalBlockedDamage() { - return this.originalBlocked; - } - - /** - * @return The current amount of damage blocked, as a result of this event. - */ - public float getBlockedDamage() { - return this.dmgBlocked; - } - - /** - * Controls if {@link LivingEntity#hurtCurrentlyUsedShield} is called. - * - * @return If the shield item will take durability damage or not. - */ - public boolean shieldTakesDamage() { - return this.shieldTakesDamage; - } - - /** - * Set how much damage is blocked by this action.
- * Note that initially the blocked amount is the entire attack.
- */ - public void setBlockedDamage(float blocked) { - this.dmgBlocked = Mth.clamp(blocked, 0, this.originalBlocked); - } - - /** - * Set if the shield will take durability damage or not. - */ - public void setShieldTakesDamage(boolean damage) { - this.shieldTakesDamage = damage; - } -} diff --git a/tests/src/generated/resources/assets/minecraft/models/item/stick.json b/tests/src/generated/resources/assets/minecraft/models/item/stick.json index e3c18581e5..4e9498d758 100644 --- a/tests/src/generated/resources/assets/minecraft/models/item/stick.json +++ b/tests/src/generated/resources/assets/minecraft/models/item/stick.json @@ -1,7 +1,7 @@ { "parent": "minecraft:item/generated", "display": { - "custom_transformtype_test:hanging": { + "neotests:hanging": { "rotation": [ 62, 147, diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/data/DataMapTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/data/DataMapTests.java index 565c32db90..cdcc11f3fa 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/data/DataMapTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/data/DataMapTests.java @@ -276,7 +276,7 @@ protected void gather() { } }); - test.eventListeners().forge().addListener((final LivingDamageEvent event) -> { + test.eventListeners().forge().addListener((final LivingDamageEvent.Post event) -> { final ExperienceGrant grant = event.getSource().typeHolder().getData(xpGrant); if (grant != null && event.getEntity() instanceof Player player) { player.giveExperiencePoints(grant.amount()); diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/entity/EntityEventTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/entity/EntityEventTests.java index b775d16416..02b100e274 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/entity/EntityEventTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/entity/EntityEventTests.java @@ -5,25 +5,32 @@ package net.neoforged.neoforge.debug.entity; +import java.util.Objects; import net.minecraft.core.BlockPos; import net.minecraft.core.registries.Registries; import net.minecraft.gametest.framework.GameTest; +import net.minecraft.network.chat.Component; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.ai.attributes.RangedAttribute; import net.minecraft.world.entity.animal.Cow; import net.minecraft.world.entity.animal.Pig; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.level.GameType; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.phys.Vec3; import net.neoforged.neoforge.event.entity.EntityAttributeModificationEvent; +import net.neoforged.neoforge.event.entity.EntityInvulnerabilityCheckEvent; import net.neoforged.neoforge.event.entity.EntityTeleportEvent; import net.neoforged.neoforge.event.level.ExplosionKnockbackEvent; import net.neoforged.testframework.DynamicTest; import net.neoforged.testframework.annotation.ForEachTest; import net.neoforged.testframework.annotation.TestHolder; import net.neoforged.testframework.gametest.EmptyTemplate; +import net.neoforged.testframework.gametest.GameTestPlayer; import net.neoforged.testframework.registration.RegistrationHelper; @ForEachTest(groups = { EntityTests.GROUP + ".event", "event" }) @@ -75,6 +82,26 @@ static void entityAttributeModificationEvent(final DynamicTest test, final Regis .thenSucceed()); } + @GameTest + @EmptyTemplate + @TestHolder(description = "Tests if EntityInvulnerabilityCheckEvent prevents damage when modified.") + static void entityInvulnerabilityCheckEvent(final DynamicTest test, final RegistrationHelper reg) { + final Component NAME = Component.literal("invulnerable_entity"); + test.eventListeners().forge().addListener((final EntityInvulnerabilityCheckEvent event) -> { + if (event.getEntity() instanceof GameTestPlayer entity && entity.hasCustomName() && Objects.equals(entity.getCustomName(), NAME)) + event.setInvulnerable(false); + }); + + test.onGameTest(helper -> { + DamageSource source = new DamageSource(helper.getLevel().registryAccess().registryOrThrow(Registries.DAMAGE_TYPE).getHolderOrThrow(DamageTypes.MOB_ATTACK)); + helper.startSequence(() -> helper.makeTickingMockServerPlayerInLevel(GameType.SURVIVAL)) + .thenExecute(player -> player.setCustomName(NAME)) + .thenExecute(player -> player.setInvulnerable(true)) + .thenWaitUntil(player -> helper.assertFalse(player.isInvulnerableTo(source), "Player Invulnerability not bypassed.")) + .thenSucceed(); + }); + } + @GameTest @EmptyTemplate(value = "15x5x15", floor = true) @TestHolder(description = "Tests if the pig only gets vertical knockback from explosion knockback event") diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/entity/living/LivingEntityEventTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/entity/living/LivingEntityEventTests.java index 27c6f9158b..647ad33bd2 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/entity/living/LivingEntityEventTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/entity/living/LivingEntityEventTests.java @@ -43,9 +43,9 @@ import net.neoforged.neoforge.event.entity.living.LivingConversionEvent; import net.neoforged.neoforge.event.entity.living.LivingEntityUseItemEvent; import net.neoforged.neoforge.event.entity.living.LivingGetProjectileEvent; +import net.neoforged.neoforge.event.entity.living.LivingShieldBlockEvent; import net.neoforged.neoforge.event.entity.living.LivingSwapItemsEvent; import net.neoforged.neoforge.event.entity.living.MobSplitEvent; -import net.neoforged.neoforge.event.entity.living.ShieldBlockEvent; import net.neoforged.testframework.DynamicTest; import net.neoforged.testframework.annotation.ForEachTest; import net.neoforged.testframework.annotation.TestHolder; @@ -188,8 +188,8 @@ static void setAttackTargetEvent(final DynamicTest test, final RegistrationHelpe @EmptyTemplate(floor = true) @TestHolder(description = "Tests if the ShieldBlockEvent is fired") static void shieldBlockEvent(final DynamicTest test) { - test.eventListeners().forge().addListener((final ShieldBlockEvent event) -> { - if (event.getDamageSource().getDirectEntity() instanceof AbstractArrow arrow && event.getEntity() instanceof Zombie zombie && Objects.equals(zombie.getName(), Component.literal("shieldblock"))) { + test.eventListeners().forge().addListener((final LivingShieldBlockEvent event) -> { + if (event.getBlocked() && event.getDamageSource().getDirectEntity() instanceof AbstractArrow arrow && event.getEntity() instanceof Zombie zombie && Objects.equals(zombie.getName(), Component.literal("shieldblock"))) { zombie.setItemSlot(EquipmentSlot.OFFHAND, new ItemStack(Items.STONE)); event.setBlockedDamage(event.getOriginalBlockedDamage() / 2); arrow.discard(); diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/entity/player/PlayerEventTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/entity/player/PlayerEventTests.java index e2a055d4c9..a0b17ec16c 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/entity/player/PlayerEventTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/entity/player/PlayerEventTests.java @@ -9,6 +9,7 @@ import net.minecraft.commands.Commands; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; import net.minecraft.gametest.framework.GameTest; import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.game.ServerboundInteractPacket; @@ -17,9 +18,13 @@ import net.minecraft.stats.Stats; import net.minecraft.world.InteractionHand; import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; @@ -31,6 +36,7 @@ import net.minecraft.world.level.portal.DimensionTransition; import net.neoforged.bus.api.EventPriority; import net.neoforged.neoforge.event.StatAwardEvent; +import net.neoforged.neoforge.event.entity.living.ArmorHurtEvent; import net.neoforged.neoforge.event.entity.player.ItemEntityPickupEvent; import net.neoforged.neoforge.event.entity.player.PermissionsChangedEvent; import net.neoforged.neoforge.event.entity.player.PlayerEvent; @@ -232,6 +238,28 @@ static void changeStatAward(final DynamicTest test, final RegistrationHelper reg }); } + @GameTest + @EmptyTemplate + @TestHolder(description = "Tests if ArmorHurtEvent fires and prevents armor damage.") + static void armorHurtEvent(final DynamicTest test) { + test.eventListeners().forge().addListener((final ArmorHurtEvent event) -> { + if (event.getEntity() instanceof Player player && player.getItemBySlot(EquipmentSlot.CHEST).getItem().equals(Items.DIAMOND_CHESTPLATE)) + event.setNewDamage(EquipmentSlot.CHEST, 5); + }); + + test.onGameTest(helper -> { + DamageSource source = new DamageSource(helper.getLevel().registryAccess().registryOrThrow(Registries.DAMAGE_TYPE).getHolderOrThrow(DamageTypes.MOB_ATTACK)); + helper.startSequence(() -> helper.makeMockPlayer(GameType.SURVIVAL)) + .thenExecute(player -> player.invulnerableTime = 0) + .thenExecute(player -> player.setItemSlot(EquipmentSlot.CHEST, new ItemStack(Items.DIAMOND_CHESTPLATE))) + .thenExecute(player -> player.hurt(source, 10F)) + .thenWaitUntil(player -> helper.assertTrue(player.getItemBySlot(EquipmentSlot.CHEST).getItem().equals(Items.DIAMOND_CHESTPLATE) + && player.getItemBySlot(EquipmentSlot.CHEST).getDamageValue() == 5, + "Armor hurt not applied. %s actual but expected 5f".formatted(player.getItemBySlot(EquipmentSlot.CHEST).getDamageValue()))) + .thenSucceed(); + }); + } + @GameTest @EmptyTemplate @TestHolder(description = "Tests if the PlayerRespawnPositionEvent fires correctly and can change where the player respawns") diff --git a/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/CustomItemDisplayContextTest.java b/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/CustomItemDisplayContextTest.java index 12302809fb..9c6ce94624 100644 --- a/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/CustomItemDisplayContextTest.java +++ b/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/CustomItemDisplayContextTest.java @@ -237,10 +237,8 @@ public void onDataPacket(Connection net, ClientboundBlockEntityDataPacket pkt, H @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider holderLookup) { super.saveAdditional(tag, holderLookup); - var c = new CompoundTag(); if (heldItem != null) { - heldItem.save(holderLookup, c); - tag.put("item", c); + tag.put("item", heldItem.save(holderLookup, new CompoundTag())); } }