/**
* @see <a href="https://mcsrc.dev/#1/1.21.11_unobfuscated/net/minecraft/world/level/block/DoorBlock">Minecraft source code</a>
*/
public final class DoorPlacementRule extends BlockPlacementRule {
public static final Key KEY = Key.key("minecraft:doors");
public DoorPlacementRule(@NotNull Block block) {
super(block);
}
@Override
public @Nullable Block blockPlace(@NotNull PlacementState placementState) {
var instance = (Instance) placementState.instance();
var playerPosition = placementState.playerPosition();
var facing = getFacingDirection(playerPosition);
var placePosition = placementState.placePosition();
var dimension = MinecraftServer.getDimensionTypeRegistry().get(instance.getDimensionType());
assert dimension != null;
var baseX = placePosition.blockX();
var baseY = placePosition.blockY();
var baseZ = placePosition.blockZ();
// check if there's space for both door halves within the world bounds
if (baseY <= dimension.minY() || baseY + 1 >= dimension.maxY()) {
return null;
}
var targetBlock = instance.getBlock(baseX, baseY, baseZ);
if (!isReplaceable(targetBlock)) {
return null;
}
var upperBlock = instance.getBlock(baseX, baseY + 1, baseZ);
if (!isReplaceable(upperBlock)) {
return null;
}
var supportBlock = instance.getBlock(baseX, baseY - 1, baseZ);
if (!isSupporting(supportBlock)) {
return null;
}
var hinge = getHinge(instance, facing, placementState, baseX, baseY, baseZ);
var configured = placementState.block()
.withProperty("facing", facing.name().toLowerCase())
.withProperty("open", "false")
.withProperty("hinge", hinge.name().toLowerCase())
.withProperty("powered", "false");
var upperPosition = placePosition.add(0, 1, 0);
instance.setBlock(upperPosition, configured.withProperty("half", "upper"));
return configured.withProperty("half", "lower");
}
@Override
public @NotNull Block blockUpdate(@NotNull UpdateState updateState) {
var currentBlock = updateState.currentBlock();
var half = currentBlock.getProperty("half");
if (half == null) {
return currentBlock;
}
var otherHalfY = "lower".equals(half) ? 1 : -1;
var blockPosition = updateState.blockPosition();
var otherHalfBlock = updateState.instance().getBlock(
blockPosition.blockX(),
blockPosition.blockY() + otherHalfY,
blockPosition.blockZ());
if (!isDoorHalf(otherHalfBlock, getOppositeHalf(half))) {
return Block.AIR;
}
return currentBlock;
}
@Override
public int maxUpdateDistance() {
return 1;
}
private static boolean isDoorHalf(Block block, String expectedHalf) {
if (!Utility.hasTag(block, KEY)) {
return false;
}
var half = block.getProperty("half");
return expectedHalf.equals(half);
}
private static String getOppositeHalf(String half) {
return "lower".equals(half) ? "upper" : "lower";
}
private static Direction getFacingDirection(@Nullable Pos position) {
if (position == null) {
return Direction.NORTH;
}
// convert yaw to horizontal direction
var yaw = (position.yaw() % 360.0F + 360.0F) % 360.0F;
if (yaw < 45.0F || yaw >= 315.0F) {
return Direction.SOUTH;
} else if (yaw < 135.0F) {
return Direction.WEST;
} else if (yaw < 225.0F) {
return Direction.NORTH;
} else {
return Direction.EAST;
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private static boolean isReplaceable(Block block) {
return block.isAir() || block.registry().isReplaceable();
}
private static boolean isSupporting(Block block) {
return !block.isAir() && block.registry().isSolid();
}
private static Hinge getHinge(Instance instance, Direction facing, PlacementState placementState, int baseX, int baseY, int baseZ) {
var leftDirection = rotateCounterClockwise(facing);
var rightDirection = rotateClockwise(facing);
var leftLower = instance.getBlock(baseX + leftDirection.normalX(), baseY, baseZ + leftDirection.normalZ());
var leftUpper = instance.getBlock(baseX + leftDirection.normalX(), baseY + 1, baseZ + leftDirection.normalZ());
var rightLower = instance.getBlock(baseX + rightDirection.normalX(), baseY, baseZ + rightDirection.normalZ());
var rightUpper = instance.getBlock(baseX + rightDirection.normalX(), baseY + 1, baseZ + rightDirection.normalZ());
// calculate solidity score: negative favors left hinge, positive favors right hinge
var solidityScore = (isFullBlock(leftLower) ? -1 : 0)
+ (isFullBlock(leftUpper) ? -1 : 0)
+ (isFullBlock(rightLower) ? 1 : 0)
+ (isFullBlock(rightUpper) ? 1 : 0);
boolean leftDoor = isLowerDoor(leftLower);
boolean rightDoor = isLowerDoor(rightLower);
// determine hinge based on neighboring doors and solidity
if ((!leftDoor || rightDoor) && solidityScore <= 0) {
if ((!rightDoor || leftDoor) && solidityScore == 0) {
// no clear preference, use cursor position to decide
var relativeCursor = getRelativeCursor(placementState);
var stepX = facing.normalX();
var stepZ = facing.normalZ();
var placeLeft = (stepX >= 0 || !(relativeCursor.z() < 0.5D))
&& (stepX <= 0 || !(relativeCursor.z() > 0.5D))
&& (stepZ >= 0 || !(relativeCursor.x() > 0.5D))
&& (stepZ <= 0 || !(relativeCursor.x() < 0.5D));
return placeLeft ? Hinge.LEFT : Hinge.RIGHT;
}
return Hinge.LEFT;
}
return Hinge.RIGHT;
}
private static boolean isFullBlock(Block block) {
return !block.isAir() && block.registry().occludes() && block.registry().isSolid();
}
private static boolean isLowerDoor(Block block) {
if (!Utility.hasTag(block, KEY)) {
return false;
}
return "lower".equals(block.getProperty("half"));
}
private static Direction rotateClockwise(Direction direction) {
return switch (direction) {
case NORTH -> Direction.EAST;
case EAST -> Direction.SOUTH;
case SOUTH -> Direction.WEST;
case WEST -> Direction.NORTH;
default -> direction;
};
}
private static Direction rotateCounterClockwise(Direction direction) {
return switch (direction) {
case NORTH -> Direction.WEST;
case WEST -> Direction.SOUTH;
case SOUTH -> Direction.EAST;
case EAST -> Direction.NORTH;
default -> direction;
};
}
private static Cursor getRelativeCursor(PlacementState placementState) {
var cursorPosition = placementState.cursorPosition();
var localX = 0.5D;
var localZ = 0.5D;
if (cursorPosition != null) {
localX = cursorPosition.x();
localZ = cursorPosition.z();
}
var offsetX = 0;
var offsetZ = 0;
var blockFace = placementState.blockFace();
if (blockFace != null) {
var direction = blockFace.toDirection();
offsetX = direction.normalX();
offsetZ = direction.normalZ();
}
return new Cursor(localX - offsetX, localZ - offsetZ);
}
private record Cursor(double x, double z) {
}
public enum Hinge {
LEFT,
RIGHT
}
}