From 2f9157328406a6c23e7f3960583e89020cedda5e Mon Sep 17 00:00:00 2001 From: Electroblob77 <35599699+Electroblob77@users.noreply.github.com> Date: Wed, 23 Sep 2020 00:57:55 +0100 Subject: [PATCH] Block protection compat overhaul - Unifies all block protection checks into helper methods in BlockUtils which trigger all the necessary forge events and vanilla methods - Implements these checks in all the appropriate places and removes calls to EntityUtils#canDamageBlocks(...) where they are no longer needed - Changes the mine spell to respect the config settings in line with the rest of the spells - Fixes earthquake not respecting the settings at all - Adds a separate config option for dispenser block damage and updates the description for the existing settings to reflect the changes --- .../java/electroblob/wizardry/Settings.java | 11 +- .../wizardry/block/BlockStatue.java | 52 +++---- .../entity/construct/EntityBlackHole.java | 2 +- .../entity/construct/EntityBoulder.java | 4 +- .../construct/EntityCombustionRune.java | 2 +- .../entity/construct/EntityEarthquake.java | 7 +- .../entity/projectile/EntityIceball.java | 11 +- .../projectile/EntityLargeMagicFireball.java | 4 +- .../projectile/EntityMagicFireball.java | 13 +- .../wizardry/spell/FireBreath.java | 2 +- .../electroblob/wizardry/spell/Firestorm.java | 3 +- .../electroblob/wizardry/spell/Freeze.java | 2 +- .../electroblob/wizardry/spell/IceAge.java | 5 +- .../electroblob/wizardry/spell/IceStatue.java | 2 +- .../electroblob/wizardry/spell/Ignite.java | 5 +- .../java/electroblob/wizardry/spell/Mine.java | 26 ++-- .../electroblob/wizardry/spell/Petrify.java | 2 +- .../wizardry/spell/WallOfFrost.java | 6 +- .../electroblob/wizardry/util/BlockUtils.java | 138 ++++++++++++++++++ .../wizardry/util/EntityUtils.java | 19 ++- .../assets/ebwizardry/lang/en_gb.lang | 4 +- .../assets/ebwizardry/lang/en_us.lang | 4 +- 22 files changed, 238 insertions(+), 86 deletions(-) diff --git a/src/main/java/electroblob/wizardry/Settings.java b/src/main/java/electroblob/wizardry/Settings.java index 7adefb39..f5882ed7 100644 --- a/src/main/java/electroblob/wizardry/Settings.java +++ b/src/main/java/electroblob/wizardry/Settings.java @@ -188,6 +188,8 @@ public final class Settings { public boolean playersMoveEachOther = true; /** [Server-only] Whether spells cast by players can destroy blocks in the world. */ public boolean playerBlockDamage = true; + /** [Server-only] Whether spells cast by dispensers can destroy blocks in the world. */ + public boolean dispenserBlockDamage = true; /** [Server-only] Whether to revert to the old wand upgrade system, which only requires tomes of arcana. */ public boolean legacyWandLevelling = false; /** @@ -555,7 +557,14 @@ public final class Settings { config.addCustomCategoryComment(GAMEPLAY_CATEGORY, "Global settings that affect game mechanics. In multiplayer, the server/LAN host settings will apply. Please note that changing some of these settings may make the mod very difficult to play."); property = config.get(GAMEPLAY_CATEGORY, "playerBlockDamage", true, - "Whether spells cast by players can destroy blocks in the world. Set to false to prevent griefing. To prevent non-players from destroying blocks with magic, use the mobGriefing gamerule."); + "Whether spells cast by players can destroy blocks in the world. Wizardry makes every attempt to respect protection mods and plugins, but cannot guarantee it will work in all cases for every mod. If you need absolutely watertight anti-grief, disable this setting. (N.B. This setting only affects players. To prevent mobs from destroying blocks with magic, use the mobGriefing gamerule.)"); + property.setLanguageKey("config." + Wizardry.MODID + ".player_block_damage"); + Wizardry.proxy.setToNamedBooleanEntry(property); + playerBlockDamage = property.getBoolean(); + propOrder.add(property.getName()); + + property = config.get(GAMEPLAY_CATEGORY, "dispenserBlockDamage", true, + "Whether spells cast by dispensers can destroy blocks in the world. Wizardry makes every attempt to respect protection mods and plugins, but cannot guarantee it will work in all cases for every mod. If you need absolutely watertight anti-grief, disable this setting."); property.setLanguageKey("config." + Wizardry.MODID + ".player_block_damage"); Wizardry.proxy.setToNamedBooleanEntry(property); playerBlockDamage = property.getBoolean(); diff --git a/src/main/java/electroblob/wizardry/block/BlockStatue.java b/src/main/java/electroblob/wizardry/block/BlockStatue.java index 3733715a..b8d2ee40 100644 --- a/src/main/java/electroblob/wizardry/block/BlockStatue.java +++ b/src/main/java/electroblob/wizardry/block/BlockStatue.java @@ -8,6 +8,7 @@ import net.minecraft.block.SoundType; import net.minecraft.block.material.Material; import net.minecraft.block.state.IBlockState; import net.minecraft.entity.EntityLiving; +import net.minecraft.entity.EntityLivingBase; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.BlockRenderLayer; import net.minecraft.util.EnumBlockRenderType; @@ -19,6 +20,7 @@ import net.minecraft.world.World; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; +import javax.annotation.Nullable; import java.util.Random; public class BlockStatue extends Block implements ITileEntityProvider { @@ -180,77 +182,77 @@ public class BlockStatue extends Block implements ITileEntityProvider { /** * Turns the given entity into a statue. The type of statue depends on the block instance this method was invoked on. - * @param entity The entity to turn into a statue. + * @param target The entity to turn into a statue. + * @param caster The entity that caused it, or null if it was not caused by an entity * @param duration The time for which the entity should remain a statue. For petrified creatures, this is the minimum * time it can stay as a statue. * @return True if the entity was successfully turned into a statue, false if not (i.e. something was in the way). */ // Making this an instance method means it works equally well for both types of statue - public boolean convertToStatue(EntityLiving entity, int duration){ + public boolean convertToStatue(EntityLiving target, @Nullable EntityLivingBase caster, int duration){ - if(entity.deathTime > 0) return false; + if(target.deathTime > 0) return false; - BlockPos pos = new BlockPos(entity); - World world = entity.world; + BlockPos pos = new BlockPos(target); + World world = target.world; - entity.hurtTime = 0; // Stops the entity looking red while frozen and the resulting z-fighting - entity.extinguish(); + target.hurtTime = 0; // Stops the entity looking red while frozen and the resulting z-fighting + target.extinguish(); // Short mobs such as spiders and pigs - if((entity.height < 1.2 || entity.isChild()) && BlockUtils.canBlockBeReplaced(world, pos)){ + if((target.height < 1.2 || target.isChild()) && BlockUtils.canBlockBeReplaced(world, pos) && BlockUtils.canPlaceBlock(caster, world, pos)){ world.setBlockState(pos, this.getDefaultState()); if(world.getTileEntity(pos) instanceof TileEntityStatue){ - ((TileEntityStatue)world.getTileEntity(pos)).setCreatureAndPart(entity, 1, 1); + ((TileEntityStatue)world.getTileEntity(pos)).setCreatureAndPart(target, 1, 1); ((TileEntityStatue)world.getTileEntity(pos)).setLifetime(duration); } - entity.getEntityData().setBoolean(this.isIce ? FROZEN_NBT_KEY : PETRIFIED_NBT_KEY, true); - entity.setDead(); + target.getEntityData().setBoolean(this.isIce ? FROZEN_NBT_KEY : PETRIFIED_NBT_KEY, true); + target.setDead(); return true; } // Normal sized mobs like zombies and skeletons - else if(entity.height < 2.5 && BlockUtils.canBlockBeReplaced(world, pos) - && BlockUtils.canBlockBeReplaced(world, pos.up())){ + else if(target.height < 2.5 && BlockUtils.canBlockBeReplaced(world, pos) && BlockUtils.canBlockBeReplaced(world, pos.up()) + && BlockUtils.canPlaceBlock(caster, world, pos) && BlockUtils.canPlaceBlock(caster, world, pos.up())){ world.setBlockState(pos, this.getDefaultState()); if(world.getTileEntity(pos) instanceof TileEntityStatue){ - ((TileEntityStatue)world.getTileEntity(pos)).setCreatureAndPart(entity, 1, 2); + ((TileEntityStatue)world.getTileEntity(pos)).setCreatureAndPart(target, 1, 2); ((TileEntityStatue)world.getTileEntity(pos)).setLifetime(duration); } world.setBlockState(pos.up(), this.getDefaultState()); if(world.getTileEntity(pos.up()) instanceof TileEntityStatue){ - ((TileEntityStatue)world.getTileEntity(pos.up())).setCreatureAndPart(entity, 2, 2); + ((TileEntityStatue)world.getTileEntity(pos.up())).setCreatureAndPart(target, 2, 2); } - entity.getEntityData().setBoolean(this.isIce ? FROZEN_NBT_KEY : PETRIFIED_NBT_KEY, true); - entity.setDead(); + target.getEntityData().setBoolean(this.isIce ? FROZEN_NBT_KEY : PETRIFIED_NBT_KEY, true); + target.setDead(); return true; } // Tall mobs like endermen - else if(BlockUtils.canBlockBeReplaced(world, pos) - && BlockUtils.canBlockBeReplaced(world, pos.up()) - && BlockUtils.canBlockBeReplaced(world, pos.up(2))){ + else if(BlockUtils.canBlockBeReplaced(world, pos) && BlockUtils.canBlockBeReplaced(world, pos.up()) && BlockUtils.canBlockBeReplaced(world, pos.up(2)) + && BlockUtils.canPlaceBlock(caster, world, pos) && BlockUtils.canPlaceBlock(caster, world, pos.up()) && BlockUtils.canPlaceBlock(caster, world, pos.up(2))){ world.setBlockState(pos, this.getDefaultState()); if(world.getTileEntity(pos) instanceof TileEntityStatue){ - ((TileEntityStatue)world.getTileEntity(pos)).setCreatureAndPart(entity, 1, 3); + ((TileEntityStatue)world.getTileEntity(pos)).setCreatureAndPart(target, 1, 3); ((TileEntityStatue)world.getTileEntity(pos)).setLifetime(duration); } world.setBlockState(pos.up(), this.getDefaultState()); if(world.getTileEntity(pos.up()) instanceof TileEntityStatue){ - ((TileEntityStatue)world.getTileEntity(pos.up())).setCreatureAndPart(entity, 2, 3); + ((TileEntityStatue)world.getTileEntity(pos.up())).setCreatureAndPart(target, 2, 3); } world.setBlockState(pos.up(2), this.getDefaultState()); if(world.getTileEntity(pos.up(2)) instanceof TileEntityStatue){ - ((TileEntityStatue)world.getTileEntity(pos.up(2))).setCreatureAndPart(entity, 3, 3); + ((TileEntityStatue)world.getTileEntity(pos.up(2))).setCreatureAndPart(target, 3, 3); } - entity.getEntityData().setBoolean(this.isIce ? FROZEN_NBT_KEY : PETRIFIED_NBT_KEY, true); - entity.setDead(); + target.getEntityData().setBoolean(this.isIce ? FROZEN_NBT_KEY : PETRIFIED_NBT_KEY, true); + target.setDead(); return true; } diff --git a/src/main/java/electroblob/wizardry/entity/construct/EntityBlackHole.java b/src/main/java/electroblob/wizardry/entity/construct/EntityBlackHole.java index 27938bd4..b38445e9 100644 --- a/src/main/java/electroblob/wizardry/entity/construct/EntityBlackHole.java +++ b/src/main/java/electroblob/wizardry/entity/construct/EntityBlackHole.java @@ -109,7 +109,7 @@ public class EntityBlackHole extends EntityScaledConstruct { if(rand.nextInt(Math.max(1, (int)this.getDistanceSq(pos) * 3)) == 0){ if(!BlockUtils.isBlockUnbreakable(world, pos) && !world.isAirBlock(pos) - && world.isBlockNormalCube(pos, false)){ + && world.isBlockNormalCube(pos, false) && BlockUtils.canBreakBlock(getCaster(), world, pos)){ // Checks that the block above is not solid, since this causes the falling block to vanish. // && !world.isBlockNormalCube(pos.up(), false)){ diff --git a/src/main/java/electroblob/wizardry/entity/construct/EntityBoulder.java b/src/main/java/electroblob/wizardry/entity/construct/EntityBoulder.java index 72a4b9de..4a1d65cd 100644 --- a/src/main/java/electroblob/wizardry/entity/construct/EntityBoulder.java +++ b/src/main/java/electroblob/wizardry/entity/construct/EntityBoulder.java @@ -6,6 +6,7 @@ import electroblob.wizardry.registry.Spells; import electroblob.wizardry.registry.WizardrySounds; import electroblob.wizardry.spell.Boulder; import electroblob.wizardry.spell.Spell; +import electroblob.wizardry.util.BlockUtils; import electroblob.wizardry.util.EntityUtils; import electroblob.wizardry.util.MagicDamage; import electroblob.wizardry.util.MagicDamage.DamageType; @@ -119,7 +120,8 @@ public class EntityBoulder extends EntityScaledConstruct { */ private boolean smashBlocks(List blocks, boolean breakIfTooHard){ - if(blocks.removeIf(p -> world.getBlockState(p).getBlock().getExplosionResistance(world, p, this, null) > 3)){ + if(blocks.removeIf(p -> world.getBlockState(p).getBlock().getExplosionResistance(world, p, this, null) > 3 + || (!world.isRemote && !BlockUtils.canBreakBlock(getCaster(), world, p)))){ // If any of the blocks were not breakable, the boulder is smashed if(breakIfTooHard){ this.despawn(); diff --git a/src/main/java/electroblob/wizardry/entity/construct/EntityCombustionRune.java b/src/main/java/electroblob/wizardry/entity/construct/EntityCombustionRune.java index 5930b2dd..155dd248 100644 --- a/src/main/java/electroblob/wizardry/entity/construct/EntityCombustionRune.java +++ b/src/main/java/electroblob/wizardry/entity/construct/EntityCombustionRune.java @@ -43,7 +43,7 @@ public class EntityCombustionRune extends EntityScaledConstruct { float strength = Spells.combustion_rune.getProperty(Spell.BLAST_RADIUS).floatValue() * sizeMultiplier; world.newExplosion(this.getCaster(), this.posX, this.posY, this.posZ, strength, true, - getCaster() != null && EntityUtils.canDamageBlocks(getCaster(), world)); + EntityUtils.canDamageBlocks(getCaster(), world)); // The trap is destroyed once triggered. this.setDead(); diff --git a/src/main/java/electroblob/wizardry/entity/construct/EntityEarthquake.java b/src/main/java/electroblob/wizardry/entity/construct/EntityEarthquake.java index d97c191b..8fc6c454 100644 --- a/src/main/java/electroblob/wizardry/entity/construct/EntityEarthquake.java +++ b/src/main/java/electroblob/wizardry/entity/construct/EntityEarthquake.java @@ -31,7 +31,7 @@ public class EntityEarthquake extends EntityMagicConstruct { // NOT a scaled con double speed = Spells.earthquake.getProperty(Earthquake.SPREAD_SPEED).doubleValue(); - if(!world.isRemote){ + if(!world.isRemote && EntityUtils.canDamageBlocks(getCaster(), world)){ // The further the earthquake is going to spread, the finer the angle increments. for(float angle = 0; angle < 2 * Math.PI; angle += Math.PI / (lifetime * 1.5)){ @@ -46,10 +46,9 @@ public class EntityEarthquake extends EntityMagicConstruct { // NOT a scaled con BlockPos pos = new BlockPos(x, y, z); - if(!BlockUtils.isBlockUnbreakable(world, pos) && !world.isAirBlock(pos) - && world.isBlockNormalCube(pos, false) + if(!BlockUtils.isBlockUnbreakable(world, pos) && !world.isAirBlock(pos) && world.isBlockNormalCube(pos, false) // Checks that the block above is not solid, since this causes the falling sand to vanish. - && !world.isBlockNormalCube(pos.up(), false)){ + && !world.isBlockNormalCube(pos.up(), false) && BlockUtils.canBreakBlock(getCaster(), world, pos)){ // Falling blocks do the setting block to air themselves. EntityFallingBlock fallingblock = new EntityFallingBlock(world, x + 0.5, y + 0.5, z + 0.5, diff --git a/src/main/java/electroblob/wizardry/entity/projectile/EntityIceball.java b/src/main/java/electroblob/wizardry/entity/projectile/EntityIceball.java index 254ca6b0..88c77dc8 100644 --- a/src/main/java/electroblob/wizardry/entity/projectile/EntityIceball.java +++ b/src/main/java/electroblob/wizardry/entity/projectile/EntityIceball.java @@ -45,14 +45,11 @@ public class EntityIceball extends EntityMagicProjectile { }else{ - if(this.getThrower() == null || EntityUtils.canDamageBlocks(this.getThrower(), world)){ + BlockPos pos = rayTrace.getBlockPos(); - BlockPos pos = rayTrace.getBlockPos(); - - if(rayTrace.sideHit == EnumFacing.UP && !world.isRemote && world.isSideSolid(pos, EnumFacing.UP) - && BlockUtils.canBlockBeReplaced(world, pos.up())){ - world.setBlockState(pos.up(), Blocks.SNOW_LAYER.getDefaultState()); - } + if(rayTrace.sideHit == EnumFacing.UP && !world.isRemote && world.isSideSolid(pos, EnumFacing.UP) + && BlockUtils.canBlockBeReplaced(world, pos.up()) && BlockUtils.canPlaceBlock(thrower, world, pos)){ + world.setBlockState(pos.up(), Blocks.SNOW_LAYER.getDefaultState()); } } diff --git a/src/main/java/electroblob/wizardry/entity/projectile/EntityLargeMagicFireball.java b/src/main/java/electroblob/wizardry/entity/projectile/EntityLargeMagicFireball.java index 26e27918..f34b6d57 100644 --- a/src/main/java/electroblob/wizardry/entity/projectile/EntityLargeMagicFireball.java +++ b/src/main/java/electroblob/wizardry/entity/projectile/EntityLargeMagicFireball.java @@ -51,8 +51,8 @@ public class EntityLargeMagicFireball extends EntityMagicFireball { protected void onImpact(RayTraceResult rayTrace){ if(!world.isRemote){ - boolean flag = this.getThrower() == null || EntityUtils.canDamageBlocks(this.getThrower(), world); - this.world.newExplosion(null, this.posX, this.posY, this.posZ, getExplosionPower() * blastMultiplier, flag, flag); + boolean terrainDamage = EntityUtils.canDamageBlocks(this.getThrower(), world); + this.world.newExplosion(null, this.posX, this.posY, this.posZ, getExplosionPower() * blastMultiplier, terrainDamage, terrainDamage); } super.onImpact(rayTrace); diff --git a/src/main/java/electroblob/wizardry/entity/projectile/EntityMagicFireball.java b/src/main/java/electroblob/wizardry/entity/projectile/EntityMagicFireball.java index 31e2da8c..28356618 100644 --- a/src/main/java/electroblob/wizardry/entity/projectile/EntityMagicFireball.java +++ b/src/main/java/electroblob/wizardry/entity/projectile/EntityMagicFireball.java @@ -3,7 +3,7 @@ package electroblob.wizardry.entity.projectile; import electroblob.wizardry.Wizardry; import electroblob.wizardry.registry.Spells; import electroblob.wizardry.spell.Spell; -import electroblob.wizardry.util.EntityUtils; +import electroblob.wizardry.util.BlockUtils; import electroblob.wizardry.util.MagicDamage; import electroblob.wizardry.util.MagicDamage.DamageType; import electroblob.wizardry.util.ParticleBuilder; @@ -84,13 +84,12 @@ public class EntityMagicFireball extends EntityMagicProjectile { }else{ - if(this.getThrower() == null || EntityUtils.canDamageBlocks(this.getThrower(), world)){ + BlockPos pos = rayTrace.getBlockPos().offset(rayTrace.sideHit); - BlockPos blockpos = rayTrace.getBlockPos().offset(rayTrace.sideHit); - - if(this.world.isAirBlock(blockpos)){ - this.world.setBlockState(blockpos, Blocks.FIRE.getDefaultState()); - } + // Remember that canPlaceBlock should ALWAYS be the last thing that gets checked, or it risks other mods + // thinking the block was placed even when a later condition prevents it, which may have side-effects + if(this.world.isAirBlock(pos) && BlockUtils.canPlaceBlock(thrower, world, pos)){ + this.world.setBlockState(pos, Blocks.FIRE.getDefaultState()); } } diff --git a/src/main/java/electroblob/wizardry/spell/FireBreath.java b/src/main/java/electroblob/wizardry/spell/FireBreath.java index 64e4e4fe..28301d94 100644 --- a/src/main/java/electroblob/wizardry/spell/FireBreath.java +++ b/src/main/java/electroblob/wizardry/spell/FireBreath.java @@ -72,7 +72,7 @@ public class FireBreath extends SpellRay { pos = pos.offset(side); if(world.isAirBlock(pos)){ - if(!world.isRemote) world.setBlockState(pos, Blocks.FIRE.getDefaultState()); + if(!world.isRemote && BlockUtils.canPlaceBlock(caster, world, pos)) world.setBlockState(pos, Blocks.FIRE.getDefaultState()); return true; } diff --git a/src/main/java/electroblob/wizardry/spell/Firestorm.java b/src/main/java/electroblob/wizardry/spell/Firestorm.java index 769405e7..691bd87f 100644 --- a/src/main/java/electroblob/wizardry/spell/Firestorm.java +++ b/src/main/java/electroblob/wizardry/spell/Firestorm.java @@ -97,7 +97,8 @@ public class Firestorm extends SpellAreaEffect { double dist = origin.distanceTo(new Vec3d(origin.x + i, y, origin.z + j)); // Randomised with weighting so that the nearer the block the more likely it is to be set alight. - if(y != -1 && world.rand.nextInt((int)(dist * 2) + 1) < radius && dist < radius && dist > 1.5){ + if(y != -1 && world.rand.nextInt((int)(dist * 2) + 1) < radius && dist < radius && dist > 1.5 + && BlockUtils.canPlaceBlock(caster, world, pos)){ world.setBlockState(pos, Blocks.FIRE.getDefaultState()); } } diff --git a/src/main/java/electroblob/wizardry/spell/Freeze.java b/src/main/java/electroblob/wizardry/spell/Freeze.java index f87bc9de..af9154fd 100644 --- a/src/main/java/electroblob/wizardry/spell/Freeze.java +++ b/src/main/java/electroblob/wizardry/spell/Freeze.java @@ -58,7 +58,7 @@ public class Freeze extends SpellRay { @Override protected boolean onBlockHit(World world, BlockPos pos, EnumFacing side, Vec3d hit, EntityLivingBase caster, Vec3d origin, int ticksInUse, SpellModifiers modifiers){ - if(!world.isRemote && EntityUtils.canDamageBlocks(caster, world)){ + if(!world.isRemote && BlockUtils.canPlaceBlock(caster, world, pos)){ BlockUtils.freeze(world, pos, true); } diff --git a/src/main/java/electroblob/wizardry/spell/IceAge.java b/src/main/java/electroblob/wizardry/spell/IceAge.java index 29dc43d9..e88c32e1 100644 --- a/src/main/java/electroblob/wizardry/spell/IceAge.java +++ b/src/main/java/electroblob/wizardry/spell/IceAge.java @@ -54,7 +54,7 @@ public class IceAge extends SpellAreaEffect { if(target instanceof EntityLiving){ if(((BlockStatue)WizardryBlocks.ice_statue).convertToStatue((EntityLiving)target, - (int)(getProperty(FREEZE_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)))){ + caster, (int)(getProperty(FREEZE_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)))){ target.playSound(WizardrySounds.MISC_FREEZE, 1.0F, world.rand.nextFloat() * 0.4F + 0.8F); } }else if(target instanceof EntityPlayer){ @@ -113,7 +113,8 @@ public class IceAge extends SpellAreaEffect { double dist = origin.distanceTo(new Vec3d(origin.x + i, y, origin.z + j)); // Randomised with weighting so that the nearer the block the more likely it is to be snowed. - if(y != -1 && world.rand.nextInt((int)(dist * 2) + 1) < radius && dist < radius){ + if(y != -1 && world.rand.nextInt((int)(dist * 2) + 1) < radius && dist < radius + && BlockUtils.canPlaceBlock(caster, world, pos)){ BlockUtils.freeze(world, pos.down(), true); } } diff --git a/src/main/java/electroblob/wizardry/spell/IceStatue.java b/src/main/java/electroblob/wizardry/spell/IceStatue.java index 67aaf45b..37ba24ad 100644 --- a/src/main/java/electroblob/wizardry/spell/IceStatue.java +++ b/src/main/java/electroblob/wizardry/spell/IceStatue.java @@ -35,7 +35,7 @@ public class IceStatue extends SpellRay { if(target instanceof EntityLiving && !world.isRemote){ // Unchecked cast is fine because the block is a static final field if(((BlockStatue)WizardryBlocks.ice_statue).convertToStatue((EntityLiving)target, - (int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)))){ + caster, (int)(getProperty(EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)))){ //target.playSound(WizardrySounds.SPELL_FREEZE, 1.0F, world.rand.nextFloat() * 0.4F + 0.8F); } diff --git a/src/main/java/electroblob/wizardry/spell/Ignite.java b/src/main/java/electroblob/wizardry/spell/Ignite.java index b4dc0125..2c3d3756 100644 --- a/src/main/java/electroblob/wizardry/spell/Ignite.java +++ b/src/main/java/electroblob/wizardry/spell/Ignite.java @@ -2,6 +2,7 @@ package electroblob.wizardry.spell; import electroblob.wizardry.item.SpellActions; import electroblob.wizardry.registry.WizardryItems; +import electroblob.wizardry.util.BlockUtils; import electroblob.wizardry.util.EntityUtils; import electroblob.wizardry.util.MagicDamage; import electroblob.wizardry.util.MagicDamage.DamageType; @@ -46,13 +47,11 @@ public class Ignite extends SpellRay { @Override protected boolean onBlockHit(World world, BlockPos pos, EnumFacing side, Vec3d hit, EntityLivingBase caster, Vec3d origin, int ticksInUse, SpellModifiers modifiers){ - if(!EntityUtils.canDamageBlocks(caster, world)) return false; - pos = pos.offset(side); if(world.isAirBlock(pos)){ - if(!world.isRemote){ + if(!world.isRemote && BlockUtils.canPlaceBlock(caster, world, pos)){ world.setBlockState(pos, Blocks.FIRE.getDefaultState()); } diff --git a/src/main/java/electroblob/wizardry/spell/Mine.java b/src/main/java/electroblob/wizardry/spell/Mine.java index 93c1de18..9da7a527 100644 --- a/src/main/java/electroblob/wizardry/spell/Mine.java +++ b/src/main/java/electroblob/wizardry/spell/Mine.java @@ -6,8 +6,11 @@ import electroblob.wizardry.item.ISpellCastingItem; import electroblob.wizardry.item.ItemArtefact; import electroblob.wizardry.item.SpellActions; import electroblob.wizardry.registry.WizardryItems; -import electroblob.wizardry.util.*; +import electroblob.wizardry.util.BlockUtils; +import electroblob.wizardry.util.EntityUtils; +import electroblob.wizardry.util.ParticleBuilder; import electroblob.wizardry.util.ParticleBuilder.Type; +import electroblob.wizardry.util.SpellModifiers; import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; import net.minecraft.entity.Entity; @@ -20,8 +23,6 @@ import net.minecraft.util.EnumHand; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; -import net.minecraftforge.common.ForgeHooks; -import net.minecraftforge.event.ForgeEventFactory; import net.minecraftforge.fml.common.ObfuscationReflectionHelper; import java.lang.reflect.InvocationTargetException; @@ -62,13 +63,8 @@ public class Mine extends SpellRay { if(!world.isRemote){ if(BlockUtils.isBlockUnbreakable(world, pos)) return false; - // The mine spell ignores the block damage setting for players, since that's the entire point of the spell - // Instead, it triggers block break events at the appropriate points, which protection mods should be able to - // pick up and allow/disallow accordingly - // For the time being, dispensers respect the mobGriefing gamerule - if(!(caster instanceof EntityPlayer) && !EntityUtils.canDamageBlocks(caster, world)) return false; - // Can't mine arcane-locked blocks - if(world.getTileEntity(pos) != null && world.getTileEntity(pos).getTileData().hasUniqueId(ArcaneLock.NBT_KEY)) return false; + // Reworked to respect the rules, but since we might break multiple blocks this is left as an optimisation + if(!EntityUtils.canDamageBlocks(caster, world)) return false; IBlockState state = world.getBlockState(pos); // The maximum harvest level as determined by the potency multiplier. The + 0.5f is so that @@ -105,13 +101,9 @@ public class Mine extends SpellRay { boolean silkTouch = state1.getBlock().canSilkHarvest(world, pos1, state1, (EntityPlayer)caster) && ItemArtefact.isArtefactActive((EntityPlayer)caster, WizardryItems.charm_silk_touch); - // Some protection mods seem to use this event instead so let's trigger it to check - if(ForgeEventFactory.getBreakSpeed((EntityPlayer)caster, state1, 1, pos1) <= 0) continue; + int xp = BlockUtils.checkBlockBreakXP(caster, world, pos); - int xp = ForgeHooks.onBlockBreakEvent(world, - ((EntityPlayerMP)caster).interactionManager.getGameType(), (EntityPlayerMP)caster, pos1); - - if(xp == -1) continue; // Event was cancelled + if(xp < 0) continue; // Not allowed to break the block if(silkTouch){ flag = world.destroyBlock(pos1, false); @@ -124,7 +116,7 @@ public class Mine extends SpellRay { if(flag) state1.getBlock().dropXpOnBlockBreak(world, pos1, xp); } - }else{ + }else if(BlockUtils.canBreakBlock(caster, world, pos)){ // NPCs can dig the block under the target's feet flag = world.destroyBlock(pos1, true) || flag; } diff --git a/src/main/java/electroblob/wizardry/spell/Petrify.java b/src/main/java/electroblob/wizardry/spell/Petrify.java index 09daf675..6638cfb0 100644 --- a/src/main/java/electroblob/wizardry/spell/Petrify.java +++ b/src/main/java/electroblob/wizardry/spell/Petrify.java @@ -32,7 +32,7 @@ public class Petrify extends SpellRay { if(target instanceof EntityLiving && !world.isRemote){ // Unchecked cast is fine because the block is a static final field if(((BlockStatue)WizardryBlocks.petrified_stone).convertToStatue((EntityLiving)target, - (int)(getProperty(MINIMUM_EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)))){ + caster, (int)(getProperty(MINIMUM_EFFECT_DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)))){ } } diff --git a/src/main/java/electroblob/wizardry/spell/WallOfFrost.java b/src/main/java/electroblob/wizardry/spell/WallOfFrost.java index ed8d1150..a07362dc 100644 --- a/src/main/java/electroblob/wizardry/spell/WallOfFrost.java +++ b/src/main/java/electroblob/wizardry/spell/WallOfFrost.java @@ -49,7 +49,7 @@ public class WallOfFrost extends SpellRay { if(target instanceof EntityLiving && !world.isRemote){ // Unchecked cast is fine because the block is a static final field if(((BlockStatue)WizardryBlocks.ice_statue).convertToStatue((EntityLiving)target, - (int)(getProperty(DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)))){ + caster, (int)(getProperty(DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)))){ target.playSound(WizardrySounds.MISC_FREEZE, 1.0F, world.rand.nextFloat() * 0.4F + 0.8F); } @@ -77,7 +77,7 @@ public class WallOfFrost extends SpellRay { int duration = (int)(getProperty(DURATION).floatValue() * modifiers.get(WizardryItems.duration_upgrade)); - if(BlockUtils.canBlockBeReplaced(world, pos)){ + if(BlockUtils.canBlockBeReplaced(world, pos) && BlockUtils.canPlaceBlock(caster, world, pos)){ world.setBlockState(pos, WizardryBlocks.dry_frosted_ice.getDefaultState()); world.scheduleUpdate(pos.toImmutable(), WizardryBlocks.dry_frosted_ice, duration); } @@ -86,7 +86,7 @@ public class WallOfFrost extends SpellRay { if(side == EnumFacing.UP){ pos = pos.offset(side); - if(BlockUtils.canBlockBeReplaced(world, pos)){ + if(BlockUtils.canBlockBeReplaced(world, pos) && BlockUtils.canPlaceBlock(caster, world, pos)){ world.setBlockState(pos, WizardryBlocks.dry_frosted_ice.getDefaultState()); world.scheduleUpdate(pos.toImmutable(), WizardryBlocks.dry_frosted_ice, duration); } diff --git a/src/main/java/electroblob/wizardry/util/BlockUtils.java b/src/main/java/electroblob/wizardry/util/BlockUtils.java index ba31eb8a..db7325f9 100644 --- a/src/main/java/electroblob/wizardry/util/BlockUtils.java +++ b/src/main/java/electroblob/wizardry/util/BlockUtils.java @@ -7,9 +7,14 @@ import net.minecraft.block.material.Material; import net.minecraft.block.properties.IProperty; import net.minecraft.block.state.IBlockState; import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLiving; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.init.Biomes; import net.minecraft.init.Blocks; import net.minecraft.util.EnumFacing; +import net.minecraft.util.EnumHand; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.RayTraceResult; @@ -17,6 +22,9 @@ import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import net.minecraft.world.biome.Biome; import net.minecraftforge.common.BiomeDictionary; +import net.minecraftforge.common.ForgeHooks; +import net.minecraftforge.common.util.BlockSnapshot; +import net.minecraftforge.event.ForgeEventFactory; import javax.annotation.Nullable; import java.util.ArrayList; @@ -167,6 +175,136 @@ public final class BlockUtils { return sphere; } + // Region protection compatibility + // =============================================================================================================== + + // This is the best I can do to play nicely with world protection mods/plugins like WorldGuard. As before, the + // playerBlockDamage config option is the ONLY anti-grief feature that is guaranteed to be 100% watertight. + + // For Forge mods, it's not too bad, we can just make sure we trigger the appropriate Forge events (or call other + // methods that subsequently trigger them). For breaking, there are four events that claim mods might hook into: + // - PlayerInteractEvent.LeftClickBlock (I actually use this one myself for arcane lock, it's completely seamless) + // - PlayerEvent.BreakSpeed (not really what it's for, but it is possible to prevent it this way. Can show the first + // frame of the breaking animation but otherwise seamless. Not fired in creative!) + // - LivingDestroyBlockEvent (useful for wizards, probably not intended for players) + // - BlockEvent.BreakEvent (the most obvious one but also the last to be fired. Shows the entire animation before + // cancelling, looks super ugly) + // As long as we trigger the last three, it should be fine! Also note that PlayerInteractEvent.LeftClickBlock is fired + // every tick during breaking (only once in creative). + + // For Bukkit plugins, it's exceedingly difficult! We have to make sure we call the VANILLA methods that will then + // trigger Bukkit's hooks for plugins to respond to. These are likely to also trigger the relevant Forge events, but + // to be safe we should trigger everything we possibly can that won't have too many side-effects. + // Building Gadgets' implementation is a useful resource which I'm pretty sure works with WorldGuard: + // https://github.com/Direwolf20-MC/BuildingGadgets/blob/1.12.x/src/main/java/com/direwolf20/buildinggadgets/common/items/gadgets/GadgetBuilding.java + // See also: https://worldguard.enginehub.org/en/latest/regions/scope/#mod-and-plugin-support + + /** + * Tests whether the given entity may place the given block at the given location. Does not actually place the block + * (this gives callers more flexibility in how they use it, for example it allows different block placement methods + * in {@link World} to be used depending on the situation). This should only be called server-side. + *

+ * Be aware that this method triggers a variety of vanilla methods and Forge events, and depending on other + * installed mods these may have side-effects. Use of this method just to query is therefore not advised - if it + * returns true, callers should actually place the block to ensure behaviour is consistent for other mods. + *

+ * Also note that this method does not do blockstate-specific checks (notice it doesn't even take the + * block being placed as a parameter; this is in line with the block place events). It is up to the caller to + * determine whether the position is suitable for placing whatever kind of block is to be placed. + * @param placer The entity doing the placing, or null if the block is not being placed by an entity + * @param world The world in which the block is being placed + * @param pos The position of the block being placed + * @return True if the given entity is allowed to place the block, false otherwise. + */ + public static boolean canPlaceBlock(@Nullable Entity placer, World world, BlockPos pos){ + + if(world.isRemote){ + Wizardry.logger.warn("BlockUtils#canPlaceBlock called from the client side! Blocks should be modified server-side only"); + return true; + } + + if(!EntityUtils.canDamageBlocks(placer, world)) return false; // General block damage prevention comes first + + if(world.isOutsideBuildHeight(pos)) return false; + // This line *should* trigger bukkit plugin hooks + if(placer instanceof EntityPlayer && !world.isBlockModifiable((EntityPlayer)placer, pos)) return false; + + BlockSnapshot snapshot = BlockSnapshot.getBlockSnapshot(world, pos); + // Despite there being a separate event for players, BOTH events seem to be fired for players during normal placement + if(ForgeEventFactory.onBlockPlace(placer, snapshot, EnumFacing.UP).isCanceled()) return false; + + if(placer instanceof EntityPlayer && ForgeEventFactory.onPlayerBlockPlace( + (EntityPlayer)placer, snapshot, EnumFacing.UP, EnumHand.MAIN_HAND).isCanceled()){ + return false; + } + + return true; + } + + /** + * Tests whether the given entity may break the block at the given location. Does not actually break the block (this + * gives callers more flexibility in how they use it, for example it allows different block break methods in + * {@link World} to be used depending on whether particles, sound, drops, etc. are desired). This should only be + * called server-side. + *

+ * This method is a shorthand for {@link BlockUtils#checkBlockBreakXP(Entity, World, BlockPos)} {@code >= 0}. + *

+ * Be aware that this method triggers a variety of vanilla methods and Forge events, and depending on other + * installed mods these may have side-effects. Use of this method just to query is therefore not advised - if it + * returns true, callers should actually break the block to ensure behaviour is consistent for other mods. + * @param breaker The entity doing the breaking, or null if the block is not being broken by an entity + * @param world The world in which the block is being broken + * @param pos The position of the block being broken + * @return True if the given entity is allowed to break the block, false otherwise. + */ + public static boolean canBreakBlock(@Nullable Entity breaker, World world, BlockPos pos){ + return checkBlockBreakXP(breaker, world, pos) >= 0; + } + + /** + * Tests whether the given entity may break the block at the given location and returns the xp it should drop. Does + * not actually break the block or drop the xp (this gives callers more flexibility in how they use it, for example + * it allows different block break methods in {@link World} to be used depending on whether particles, sound, drops, + * etc. are desired). This should only be called server-side. + *

+ * Be aware that this method triggers a variety of vanilla methods and Forge events, and depending on other + * installed mods these may have side-effects. Use of this method just to query is therefore not advised - if it + * returns true, callers should actually break the block to ensure behaviour is consistent for other mods. + * @param breaker The entity doing the breaking, or null if the block is not being broken by an entity + * @param world The world in which the block is being broken + * @param pos The position of the block being broken + * @return The xp to be dropped if the given entity is allowed to break the block, -1 if the block cannot be broken. + */ + public static int checkBlockBreakXP(@Nullable Entity breaker, World world, BlockPos pos){ + + if(world.isRemote){ + Wizardry.logger.warn("BlockUtils#checkBlockBreakXP called from the client side! Blocks should be modified server-side only"); + return 0; + } + + if(!EntityUtils.canDamageBlocks(breaker, world)) return -1; // General block damage prevention comes first + + if(world.isOutsideBuildHeight(pos)) return -1; + // This line *should* trigger bukkit plugin hooks + if(breaker instanceof EntityPlayer && !world.isBlockModifiable((EntityPlayer)breaker, pos)) return -1; + + // I think this is irrelevant in forge because it's only for vanilla entities, but bukkit might use it + IBlockState state = world.getBlockState(pos); + if(!state.getBlock().canEntityDestroy(state, world, pos, breaker)) return -1; + // Although the forge event only needs an EntityLivingBase, it seems it's not supposed to be for players + if(breaker instanceof EntityLiving && ForgeEventFactory.onEntityDestroyBlock((EntityLivingBase)breaker, pos, state)) return -1; + // Need to trigger PlayerEvent.BreakSpeed as some claim mods (e.g. LandManager) use it instead of BlockEvent.BreakEvent + if(breaker instanceof EntityPlayer && ForgeEventFactory.getBreakSpeed((EntityPlayer)breaker, state, 1, pos) < 0) return -1; + + int xp = 0; + + if(breaker instanceof EntityPlayerMP){ + xp = ForgeHooks.onBlockBreakEvent(world, ((EntityPlayerMP)breaker).interactionManager.getGameType(), (EntityPlayerMP)breaker, pos); + } + + return xp; + } + // Specific blocks // =============================================================================================================== diff --git a/src/main/java/electroblob/wizardry/util/EntityUtils.java b/src/main/java/electroblob/wizardry/util/EntityUtils.java index 62e10c4d..0386188c 100644 --- a/src/main/java/electroblob/wizardry/util/EntityUtils.java +++ b/src/main/java/electroblob/wizardry/util/EntityUtils.java @@ -354,11 +354,20 @@ public final class EntityUtils { return server.getPlayerList().getOppedPlayers().getEntry(player.getGameProfile()) != null; } - /** Checks that the given entity is allowed to damage blocks in the given world. If the entity is a player or null, - * this checks the player block damage config setting, otherwise it posts a mob griefing event and returns the result. */ - public static boolean canDamageBlocks(@Nullable EntityLivingBase entity, World world){ - // TODO: Dispenser griefing! - if(entity == null || entity instanceof EntityPlayer) return Wizardry.settings.playerBlockDamage; + /** + * Checks that the given entity is allowed to damage blocks in the given world. If the entity is a player or null, + * this checks the player block damage config setting and (for players only) the {@code PlayerCapabilities} object, + * otherwise it posts a mob griefing event and returns the result. + *

+ * This method only checks whether the entity can damage blocks in general. It can be useful in certain + * situations where breaking of individual blocks is out of the caller's control (e.g. explosions, which have their + * own events anyway) or as an optimisation where multiple blocks are affected. In most cases, however, it is + * advisable to use the location-specific methods in {@link BlockUtils}, as they call this method anyway and allow + * for better compatibility with other mods, particularly region protection mods. + */ + public static boolean canDamageBlocks(@Nullable Entity entity, World world){ + if(entity == null) return Wizardry.settings.dispenserBlockDamage; + else if(entity instanceof EntityPlayer) return ((EntityPlayer)entity).isAllowEdit() && Wizardry.settings.playerBlockDamage; return ForgeEventFactory.getMobGriefingEvent(world, entity); } diff --git a/src/main/resources/assets/ebwizardry/lang/en_gb.lang b/src/main/resources/assets/ebwizardry/lang/en_gb.lang index 02d857cb..24b20d21 100644 --- a/src/main/resources/assets/ebwizardry/lang/en_gb.lang +++ b/src/main/resources/assets/ebwizardry/lang/en_gb.lang @@ -1332,9 +1332,11 @@ config.ebwizardry.players_move_each_other.tooltip=Whether to allow players to mo config.ebwizardry.players_move_each_other.true=Yes - let the games begin! config.ebwizardry.players_move_each_other.false=No - I won't be pushed around config.ebwizardry.player_block_damage=Player Block Damage -config.ebwizardry.player_block_damage.tooltip=Whether spells cast by players can destroy blocks in the world. Disable this to prevent griefing. To prevent non-players from destroying blocks with magic, use the mobGriefing gamerule. +config.ebwizardry.player_block_damage.tooltip=Whether spells cast by players can destroy blocks in the world. Wizardry makes every attempt to respect protection mods and plugins, but cannot guarantee it will work in all cases for every mod. If you need absolutely watertight anti-grief, disable this setting. (N.B. This setting only affects players. To prevent mobs from destroying blocks with magic, use the mobGriefing gamerule.) config.ebwizardry.player_block_damage.true=Yes - kaboom! config.ebwizardry.player_block_damage.false=No - activate anti-grief (TM) +config.ebwizardry.dispenser_block_damage=Dispenser Block Damage +config.ebwizardry.dispenser_block_damage.tooltip=Whether spells cast by dispensers can destroy blocks in the world. Wizardry makes every attempt to respect protection mods and plugins, but cannot guarantee it will work in all cases for every mod. If you need absolutely watertight anti-grief, disable this setting. config.ebwizardry.telekinetic_disarmament=Telekinetic Disarmament config.ebwizardry.telekinetic_disarmament.tooltip=Whether to allow players to disarm other players using the telekinesis spell. Disable to prevent stealing of items. config.ebwizardry.telekinetic_disarmament.true=Yes - let people steal things diff --git a/src/main/resources/assets/ebwizardry/lang/en_us.lang b/src/main/resources/assets/ebwizardry/lang/en_us.lang index 17db89c7..3980aed6 100644 --- a/src/main/resources/assets/ebwizardry/lang/en_us.lang +++ b/src/main/resources/assets/ebwizardry/lang/en_us.lang @@ -1332,9 +1332,11 @@ config.ebwizardry.players_move_each_other.tooltip=Whether to allow players to mo config.ebwizardry.players_move_each_other.true=Yes - let the games begin! config.ebwizardry.players_move_each_other.false=No - I won't be pushed around config.ebwizardry.player_block_damage=Player Block Damage -config.ebwizardry.player_block_damage.tooltip=Whether spells cast by players can destroy blocks in the world. Disable this to prevent griefing. To prevent non-players from destroying blocks with magic, use the mobGriefing gamerule. +config.ebwizardry.player_block_damage.tooltip=Whether spells cast by players can destroy blocks in the world. Wizardry makes every attempt to respect protection mods and plugins, but cannot guarantee it will work in all cases for every mod. If you need absolutely watertight anti-grief, disable this setting. (N.B. This setting only affects players. To prevent mobs from destroying blocks with magic, use the mobGriefing gamerule.) config.ebwizardry.player_block_damage.true=Yes - kaboom! config.ebwizardry.player_block_damage.false=No - activate anti-grief (TM) +config.ebwizardry.dispenser_block_damage=Dispenser Block Damage +config.ebwizardry.dispenser_block_damage.tooltip=Whether spells cast by dispensers can destroy blocks in the world. Wizardry makes every attempt to respect protection mods and plugins, but cannot guarantee it will work in all cases for every mod. If you need absolutely watertight anti-grief, disable this setting. config.ebwizardry.telekinetic_disarmament=Telekinetic Disarmament config.ebwizardry.telekinetic_disarmament.tooltip=Whether to allow players to disarm other players using the telekinesis spell. Disable to prevent stealing of items. config.ebwizardry.telekinetic_disarmament.true=Yes - let people steal things