From 01391e906d1e6a1f4deb016b8ad7a55f1f9ffbbc Mon Sep 17 00:00:00 2001 From: IridescentVoid <265486018+IridescentVoid@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:09:14 -0700 Subject: [PATCH] Add additional checks to Place Block to detect obstructions and other likely causes of failure --- .../casting/actions/spells/OpPlaceBlock.kt | 82 ++++++++++++++++++- .../mixin/accessor/AccessorBlockItem.java | 13 +++ .../hexcasting/lang/en_us.flatten.json5 | 1 + Common/src/main/resources/hexplat.mixins.json | 1 + 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 Common/src/main/java/at/petrak/hexcasting/mixin/accessor/AccessorBlockItem.java diff --git a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPlaceBlock.kt b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPlaceBlock.kt index 7730e71c77..b3a9b03435 100644 --- a/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPlaceBlock.kt +++ b/Common/src/main/java/at/petrak/hexcasting/common/casting/actions/spells/OpPlaceBlock.kt @@ -7,20 +7,25 @@ import at.petrak.hexcasting.api.casting.eval.CastingEnvironment import at.petrak.hexcasting.api.casting.getBlockPos import at.petrak.hexcasting.api.casting.iota.Iota import at.petrak.hexcasting.api.casting.mishaps.MishapBadBlock +import at.petrak.hexcasting.api.casting.mishaps.MishapBadLocation import at.petrak.hexcasting.api.casting.mishaps.MishapLackingHotbarItem import at.petrak.hexcasting.api.misc.MediaConstants +import at.petrak.hexcasting.mixin.accessor.AccessorBlockItem import at.petrak.hexcasting.xplat.IXplatAbstractions import net.minecraft.core.BlockPos import net.minecraft.core.Direction import net.minecraft.core.particles.BlockParticleOption import net.minecraft.core.particles.ParticleTypes +import net.minecraft.core.registries.Registries import net.minecraft.server.level.ServerPlayer import net.minecraft.sounds.SoundSource import net.minecraft.world.InteractionResult +import net.minecraft.world.entity.player.Player import net.minecraft.world.item.BlockItem import net.minecraft.world.item.ItemStack import net.minecraft.world.item.context.BlockPlaceContext import net.minecraft.world.item.context.UseOnContext +import net.minecraft.world.level.block.state.pattern.BlockInWorld import net.minecraft.world.phys.BlockHitResult import net.minecraft.world.phys.Vec3 @@ -40,13 +45,11 @@ object OpPlaceBlock : SpellAction { ) val itemUseCtx = env .queryForMatchingStack { it.item is BlockItem } - ?.let { UseOnContext(env.world, env.castingEntity as? ServerPlayer, env.castingHand, it, blockHit) } + ?.let { UseOnContext(env.world, env.castingEntity as? ServerPlayer, env.otherHand, it, blockHit) } ?: throw MishapLackingHotbarItem.of("placeable") val placeContext = BlockPlaceContext(itemUseCtx) - val worldState = env.world.getBlockState(pos) - if (!worldState.canBeReplaced(placeContext)) - throw MishapBadBlock.of(pos, "replaceable") + assertCanPlaceAt(env, pos, placeContext) return SpellAction.Result( Spell(pos), @@ -55,6 +58,77 @@ object OpPlaceBlock : SpellAction { ) } + fun assertCanPlaceAt(env: CastingEnvironment, pos: BlockPos, placeContext: BlockPlaceContext) { + // stepping through all the checks that the spell performs + val casterPlayer = env.castingEntity as? Player + val stack = placeContext.itemInHand + + // XXX: this might have side effects from other mods, is it safe to call twice/call it here? + if (!IXplatAbstractions.INSTANCE.isPlacingAllowed(env.world, pos, stack, casterPlayer)) + throw MishapBadLocation(Vec3.atCenterOf(pos), "forbidden") + + val worldState = env.world.getBlockState(pos) + if (!worldState.canBeReplaced(placeContext)) + throw MishapBadBlock.of(pos, "replaceable") + + if (!env.withdrawItem({ItemStack.isSameItemSameTags(it, stack)}, 1, false)) { + throw MishapLackingHotbarItem.of("placable") + } + + // Begin checks from ItemStack.useOn() + + if ( + casterPlayer != null + && !casterPlayer.abilities.mayBuild + && !stack.hasAdventureModePlaceTagForBlock( + env.world.registryAccess().registryOrThrow(Registries.BLOCK), + BlockInWorld(env.world, pos, false) + ) + ) { + // Adventure mode check, assertPosInRangeForEditing should already catch this but just in case + throw MishapBadLocation(Vec3.atCenterOf(pos), "forbidden") + } + + val item = stack.item as BlockItem + // Checks in BlockItem.useOn -> BlockItem.place + if (!item.block.isEnabled(env.world.enabledFeatures())) { + // XXX: ideally we never select this for placement at all + throw MishapLackingHotbarItem.of("placable") + } + + if (!placeContext.canPlace()) { + throw MishapBadBlock.of(pos, "replacable") + } + + val newPlacementContext = item.updatePlacementContext(placeContext) + ?: throw MishapBadLocation(Vec3.atCenterOf(pos), "obstructed") + // in vanilla, this only happens for wall-likes that are obstructed by entities + // or attempting to place op-only blocks. assume the latter doesn't happen, because + // we don't have a way to really differentiate + + val hasPlacementState = item.block.getStateForPlacement(newPlacementContext) + ?.let { (item as AccessorBlockItem).`hex$canPlace`(newPlacementContext, it) } + ?: false + if (!hasPlacementState) { + // likely has an entity blocking placement + throw MishapBadLocation(Vec3.atCenterOf(pos), "obstructed") + } + + // Checks in BlockItem.placeBlock -> ServerLevel.setBlock + if (env.world.isOutsideBuildHeight(pos)) { + // probably redundant but better safe than sorry + throw MishapBadLocation(Vec3.atCenterOf(pos), "out_of_world") + } + + if (env.world.isDebug) { + // debug world type cannot have blocks edited + throw MishapBadLocation(Vec3.atCenterOf(pos), "forbidden") + } + + // Checks in LevelChunk.setBlockState shouldn't trip for OpPlaceBlock + // checks: chunk fully air, blockstate identical to current + } + private data class Spell(val pos: BlockPos) : RenderedSpell { override fun cast(env: CastingEnvironment) { val caster = env.castingEntity diff --git a/Common/src/main/java/at/petrak/hexcasting/mixin/accessor/AccessorBlockItem.java b/Common/src/main/java/at/petrak/hexcasting/mixin/accessor/AccessorBlockItem.java new file mode 100644 index 0000000000..38137e2308 --- /dev/null +++ b/Common/src/main/java/at/petrak/hexcasting/mixin/accessor/AccessorBlockItem.java @@ -0,0 +1,13 @@ +package at.petrak.hexcasting.mixin.accessor; + +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(BlockItem.class) +public interface AccessorBlockItem { + @Invoker("canPlace") + boolean hex$canPlace(BlockPlaceContext blockPlaceContext, BlockState blockState); +} diff --git a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 index 9e070e27c6..9f53f267c8 100644 --- a/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 +++ b/Common/src/main/resources/assets/hexcasting/lang/en_us.flatten.json5 @@ -1147,6 +1147,7 @@ too_close_to_out: "%s is too close to the boundary of the world", forbidden: "%s is forbidden to you", bad_dimension: "This dimension forbids that action", + obstructed: "%s is obstructed", }, bad_item: { diff --git a/Common/src/main/resources/hexplat.mixins.json b/Common/src/main/resources/hexplat.mixins.json index a70b99261b..a16effcb7d 100644 --- a/Common/src/main/resources/hexplat.mixins.json +++ b/Common/src/main/resources/hexplat.mixins.json @@ -12,6 +12,7 @@ "MixinWanderingTrader", "MixinWitch", "accessor.AccessorAbstractArrow", + "accessor.AccessorBlockItem", "accessor.AccessorEntity", "accessor.AccessorLivingEntity", "accessor.AccessorLootTable",