From 770cdec7f99060789aaf7a3959422bb571a8de00 Mon Sep 17 00:00:00 2001 From: Layue13 Date: Thu, 12 Nov 2026 23:52:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A5=87=E7=82=B9=E5=90=9E=E6=96=B9?= =?UTF-8?q?=E5=9D=97=E5=89=8D=E9=97=AE=E4=BF=9D=E6=8A=A4=E5=B1=82=EF=BC=88?= =?UTF-8?q?BreakEvent=EF=BC=89=EF=BC=9B=E6=AF=81=E7=81=AD=E7=88=86?= =?UTF-8?q?=E7=82=B8=E9=81=B5=E5=AE=88=20mobGriefing=EF=BC=9B=E6=9A=B4?= =?UTF-8?q?=E9=9C=B2=20getOwnerEntity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../additions/entity/EntitySingularity.java | 17 +++++- .../additions/misc/TerritoryGuard.java | 57 +++++++++++++++++++ .../singularity_territory_guard_regression.py | 28 +++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/pengu/thaumcraft/additions/misc/TerritoryGuard.java create mode 100644 tests/regression/singularity_territory_guard_regression.py diff --git a/src/main/java/com/pengu/thaumcraft/additions/entity/EntitySingularity.java b/src/main/java/com/pengu/thaumcraft/additions/entity/EntitySingularity.java index 010f566..f85d9a0 100644 --- a/src/main/java/com/pengu/thaumcraft/additions/entity/EntitySingularity.java +++ b/src/main/java/com/pengu/thaumcraft/additions/entity/EntitySingularity.java @@ -1,11 +1,13 @@ package com.pengu.thaumcraft.additions.entity; import com.pengu.thaumcraft.additions.api.integration.ThaumicAdditionsDamage; +import com.pengu.thaumcraft.additions.misc.TerritoryGuard; import java.util.List; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.boss.EntityDragon; import net.minecraft.entity.boss.EntityWither; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.item.EntityItem; import net.minecraft.init.Blocks; import net.minecraft.item.ItemStack; @@ -58,7 +60,7 @@ public class EntitySingularity extends EntityAlumentum { ++this.ticksExisted; if(this.ticksExisted >= 2000 || this.suckedItems >= 2000.0F) { for(p = 0; p < 5; ++p) { - this.worldObj.createExplosion(this, this.posX, this.posY, this.posZ, this.suckedItems / 200.0F, true); + this.worldObj.createExplosion(this, this.posX, this.posY, this.posZ, this.suckedItems / 200.0F, this.worldObj.getGameRules().getGameRuleBooleanValue("mobGriefing")); } this.setDead(); @@ -137,7 +139,7 @@ public class EntitySingularity extends EntityAlumentum { i = (int)this.posX + this.worldObj.rand.nextInt(this.radius) - this.worldObj.rand.nextInt(this.radius); var22 = (int)this.posY + this.worldObj.rand.nextInt(this.radius) - this.worldObj.rand.nextInt(this.radius); var23 = (int)this.posZ + this.worldObj.rand.nextInt(this.radius) - this.worldObj.rand.nextInt(this.radius); - if(this.worldObj.getBlock(i, var22, var23) != Blocks.air && this.worldObj.getBlock(i, var22, var23).getBlockHardness(this.worldObj, i, var22, var23) > -1.0F && this.worldObj.getGameRules().getGameRuleBooleanValue("mobGriefing")) { + if(this.worldObj.getBlock(i, var22, var23) != Blocks.air && this.worldObj.getBlock(i, var22, var23).getBlockHardness(this.worldObj, i, var22, var23) > -1.0F && this.worldObj.getGameRules().getGameRuleBooleanValue("mobGriefing") && this.canDestroy(i, var22, var23)) { this.worldObj.getBlock(i, var22, var23).breakBlock(this.worldObj, i, var22, var23, this.worldObj.getBlock(i, var22, var23), 1); this.worldObj.playSoundEffect((double)i, (double)var22, (double)var23, this.worldObj.getBlock(i, var22, var23).stepSound.getBreakSound(), this.worldObj.getBlock(i, var22, var23).stepSound.getVolume(), this.worldObj.getBlock(i, var22, var23).stepSound.getPitch()); if(!this.worldObj.isRemote) { @@ -188,6 +190,17 @@ public class EntitySingularity extends EntityAlumentum { } + /** The player who threw it, or null (dispenser, merged singularity). Protection plugins read this by name. */ + public EntityPlayer getOwnerEntity() { + EntityLivingBase t = this.getThrower(); + return t instanceof EntityPlayer ? (EntityPlayer)t : null; + } + + /** Island/territory check: a BreakEvent as the thrower, or as a fake player when nobody threw it. */ + private boolean canDestroy(int x, int y, int z) { + return TerritoryGuard.canRemoveBlock(this.worldObj, x, y, z, this.getOwnerEntity()); + } + protected void onImpact(MovingObjectPosition m) { this.worldObj.playSoundEffect(this.posX, this.posY, this.posZ, "thaumicadditions:singularity", 16.0F, 1.0F); this.inGround = true; diff --git a/src/main/java/com/pengu/thaumcraft/additions/misc/TerritoryGuard.java b/src/main/java/com/pengu/thaumcraft/additions/misc/TerritoryGuard.java new file mode 100644 index 0000000..b890eaf --- /dev/null +++ b/src/main/java/com/pengu/thaumcraft/additions/misc/TerritoryGuard.java @@ -0,0 +1,57 @@ +package com.pengu.thaumcraft.additions.misc; + +import java.lang.ref.WeakReference; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; + +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.util.FakePlayer; +import net.minecraftforge.common.util.FakePlayerFactory; +import net.minecraftforge.event.world.BlockEvent; + +/** + * Standard cancellable BlockEvent.BreakEvent gate for block removal that no player performs directly + * (void entity, hungry blocks, ...). Island and territory plugins receive the contract they already handle for player + * mining; the actor is the responsible player when there is one, otherwise a per-world fake player (which the + * protection layer judges as an unattributed machine). With no receiver nothing changes. + */ +public final class TerritoryGuard { + + private static WeakReference fallback = new WeakReference<>(null); + + /** @param actor the player responsible for the removal, or null when there is none */ + public static boolean canRemoveBlock(World world, int x, int y, int z, EntityPlayer actor) { + EntityPlayer who = resolveActor(world, actor); + if (who == null) { + // no world to attribute the event to (client world): nothing to protect + return !(world instanceof WorldServer); + } + BlockEvent.BreakEvent event = new BlockEvent.BreakEvent( + x, + y, + z, + world, + world.getBlock(x, y, z), + world.getBlockMetadata(x, y, z), + who); + return !MinecraftForge.EVENT_BUS.post(event); + } + + private static EntityPlayer resolveActor(World world, EntityPlayer actor) { + if (actor != null) return actor; + if (world instanceof WorldServer) { + EntityPlayer player = fallback.get(); + if (player == null || player.worldObj != world) { + FakePlayer fake = FakePlayerFactory.getMinecraft((WorldServer) world); + player = fake; + fallback = new WeakReference<>(player); + } + return player; + } + return null; + } + + private TerritoryGuard() {} +} diff --git a/tests/regression/singularity_territory_guard_regression.py b/tests/regression/singularity_territory_guard_regression.py new file mode 100644 index 0000000..994efaa --- /dev/null +++ b/tests/regression/singularity_territory_guard_regression.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""The singularity must ask the protection layer before removing a block and respect mobGriefing for its blast.""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +SRC = (ROOT / "src/main/java/com/pengu/thaumcraft/additions/entity/EntitySingularity.java").read_text(encoding="utf-8") +GUARD = (ROOT / "src/main/java/com/pengu/thaumcraft/additions/misc/TerritoryGuard.java").read_text(encoding="utf-8") + + +def main(): + assert "new BlockEvent.BreakEvent(" in GUARD and "EVENT_BUS.post(event)" in GUARD + assert "FakePlayerFactory.getMinecraft" in GUARD + + # the block-eating branch is gated by the guard before any breakBlock / setBlockToAir + gate = SRC.index("this.canDestroy(i, var22, var23)") + assert gate < SRC.index(".breakBlock(this.worldObj, i, var22, var23"), "guard must precede breakBlock" + assert gate < SRC.index("this.worldObj.setBlockToAir(i, var22, var23)") + assert "TerritoryGuard.canRemoveBlock(this.worldObj, x, y, z, this.getOwnerEntity())" in SRC + + # explosion attribution accessor, plus the end-of-life blast no longer ignores mobGriefing + assert "public EntityPlayer getOwnerEntity()" in SRC + assert 'this.suckedItems / 200.0F, true)' not in SRC + print("ok") + + +if __name__ == "__main__": + main()