Skip to content

Instantly share code, notes, and snippets.

@FireInstall
Created July 7, 2024 05:15

Revisions

  1. FireInstall created this gist Jul 7, 2024.
    348 changes: 348 additions & 0 deletions FrameTurtleExample.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,348 @@
    package me.fireinstallations;

    import de.greensurivors.greenbook.utils.Utils;
    import io.papermc.paper.math.BlockPosition;
    import io.papermc.paper.math.Position;
    import org.bukkit.Material;
    import org.bukkit.World;
    import org.bukkit.block.Block;
    import org.bukkit.block.BlockFace;
    import org.bukkit.block.Sign;
    import org.bukkit.block.data.BlockData;
    import org.bukkit.block.data.Directional;
    import org.bukkit.block.data.Rotatable;
    import org.bukkit.block.data.type.WallSign;
    import org.bukkit.util.Vector;
    import org.jetbrains.annotations.NotNull;
    import org.jetbrains.annotations.Nullable;
    import org.jetbrains.annotations.Range;

    @SuppressWarnings("UnstableApiUsage")
    public class FrameTurtleExample {

    /**
    * Retrieves the Frame associated with the given Sign, if any.
    *
    * @param sign the Sign to retrieve the Frame from
    * @return a Frame object representing the frame associated with the Sign, or null if no frame is found
    */
    private @Nullable Frame getFrame(@NotNull Sign sign) {
    final World world = sign.getWorld();

    switch (sign.getBlockData()) {
    case WallSign wallSign -> {// only wall signs can be attached to the frame
    final BlockPosition frameBlockPos = Position.block(sign.getBlock().getRelative(wallSign.getFacing().getOppositeFace()).getLocation());
    final BlockFace facing = wallSign.getFacing();
    final BlockFace opposite = facing.getOppositeFace();

    for (BlockFace toCheck : BlockFace.values()) {
    if (toCheck.isCartesian() && toCheck != facing && toCheck != opposite) {
    Frame frame = new FrameTurtle(world, frameBlockPos, opposite, toCheck).run();

    if (frame != null) {
    return frame;
    }
    }
    }
    // no frame
    }
    case Rotatable rotatable -> {
    if (Utils.isCardinal(rotatable)) {
    final Block block = sign.getBlock();
    final BlockFace facing = rotatable.getRotation();

    BlockPosition frameBlockPos = Position.block(block.getRelative(BlockFace.UP).getLocation());
    Frame frame = new FrameTurtle(world, frameBlockPos, facing, BlockFace.UP).run();

    if (frame == null) {
    frameBlockPos = Position.block(block.getRelative(BlockFace.DOWN).getLocation());
    frame = new FrameTurtle(world, frameBlockPos, facing, BlockFace.DOWN).run();
    }

    return frame; // may be null
    } // can't work with wrong rotations
    }
    // this matches some hanging signs.
    // Note that wall signs ARE Directional too, but since the order of cases matters in pattern matching switches,
    // having a directional behind wall sign should be fine.
    case Directional directional -> {
    final Block block = sign.getBlock();
    final BlockFace facing = directional.getFacing();

    BlockPosition frameBlockPos = Position.block(block.getRelative(BlockFace.UP).getLocation());
    Frame frame = new FrameTurtle(world, frameBlockPos, facing, BlockFace.UP).run();

    if (frame == null) {
    frameBlockPos = Position.block(block.getRelative(BlockFace.DOWN).getLocation());
    frame = new FrameTurtle(world, frameBlockPos, facing, BlockFace.DOWN).run();
    }

    return frame; // may be null
    }
    default -> plugin.getComponentLogger().warn("Got a sign that isn't a Rotatable nor a Directional. Seems like the plugin version was not written for this Minecraft version.");
    } // end switch

    return null;
    }


    private class FrameTurtle { // todo I'm sure you can still clean this class up; java docs; cache everything we fetch from the config since a change from another thread will result in not defined behavior
    private final @NotNull World world;
    private final @NotNull Vector rotationAxis;
    private final @NotNull Vector lengthRun = new Vector(0, 0, 0);
    private final @NotNull Vector workVector;
    private @NotNull Vector direction;
    private @NotNull Vector orthogonalDirection;
    private @Nullable FrameTurtleExample.Frame result = null;
    private @Range(from = 0, to = 4) int cornersFound = 0;
    private int lengthSinceLastCorner = 0;
    private @NotNull TurtleState state = TurtleState.STARTING;

    /**
    * @param world
    * @param startPositionOnFrame
    * @param rotationAxisFace is looking in the direction the turtle will never turn
    * @param orthogonalFace is looking away from the start block, in the direction the turtle will turn next
    */
    public FrameTurtle(final @NotNull World world, @NotNull BlockPosition startPositionOnFrame, final @NotNull BlockFace rotationAxisFace, final @NotNull BlockFace orthogonalFace) { // todo
    this.world = world;
    this.workVector = startPositionOnFrame.toVector();

    this.rotationAxis = rotationAxisFace.getDirection();
    this.orthogonalDirection = orthogonalFace.getDirection();
    this.direction = rotateCorrect90DEGAroundAxis(rotationAxisFace.getDirection(), orthogonalDirection);

    // check first gate block since we will look ahead of the frame blocks later
    final @NotNull Block gateBlock = world.getBlockAt(
    workVector.getBlockX() + orthogonalDirection.getBlockX(),
    workVector.getBlockY() + orthogonalDirection.getBlockY(),
    workVector.getBlockZ() + orthogonalDirection.getBlockZ());

    if (!isValidGate(gateBlock.getBlockData())) {
    // sad, died before it lived
    state = TurtleState.DEAD;
    }
    }

    public @Nullable FrameTurtleExample.Frame run() {
    while (state != TurtleState.DEAD && state != TurtleState.DONE) {
    step();
    }

    if (state == TurtleState.DONE) {
    return result;
    } else {
    return null;
    }
    }

    private boolean isValidGate(@NotNull BlockData data) { // todo make water configurable
    return data.getMaterial().isAir() || data.getMaterial() == Material.WATER || getConfig().isAllowedBlock(data);
    }

    // we don't validate the inside nor the bit left between the startPositionOnFrame and the end position since we won't enforce a perfect frame
    // and the players are free to change the frame after construction. For us do only the two Blockpostions in Frame matter, the rest is mostly
    // fluff to teach players how a gate will be expected to be.
    private @NotNull TurtleState step() {
    if (state == TurtleState.DONE || state == TurtleState.DEAD) {
    // in the life of every turtle, they can only turtle once.
    return state;
    }

    final @NotNull Block frameBlock = world.getBlockAt(workVector.getBlockX(), workVector.getBlockY(), workVector.getBlockZ());

    if (!isValidGate(frameBlock.getBlockData())) {
    // gate block is one block ahead of frame, so we can find corners
    final @NotNull Block gateBlock = world.getBlockAt(
    workVector.getBlockX() + orthogonalDirection.getBlockX() + direction.getBlockX(),
    workVector.getBlockY() + orthogonalDirection.getBlockY() + direction.getBlockY(),
    workVector.getBlockZ() + orthogonalDirection.getBlockZ() + direction.getBlockZ());

    if (isValidGate(gateBlock.getBlockData())) {
    boolean validArea;
    switch (cornersFound) {
    case 0 ->
    validArea = lengthSinceLastCorner <= getConfig().getMaxArea(); // estimate just part of dimension
    case 1 ->
    validArea = lengthSinceLastCorner * (int) Math.abs(lengthRun.getX() + lengthRun.getY() + lengthRun.getZ()) <= getConfig().getMaxArea(); // estimate with one dimension and a part of
    case 2 ->
    validArea = lengthSinceLastCorner * (int) Math.abs(lengthRun.getX() + lengthRun.getY() + lengthRun.getZ()) <= getConfig().getMaxArea(); // both dimensions
    case 3 -> { // area size was already checked and did fit. Now just check we stay in the area
    // get the recorded length of the opposite direction; sign does not matter because we will take the absolute value
    Vector temp = lengthRun.clone().multiply(direction);

    // +1 for the look ahead
    validArea = lengthSinceLastCorner + 1 <= Math.abs(temp.getX() + temp.getY() + temp.getZ());
    }
    default -> { // should never happen
    state = TurtleState.DEAD;
    return state;
    }
    }

    if (validArea) {
    lengthSinceLastCorner++;

    // prepare next step in same direction
    workVector.add(direction);

    //check world height
    int newHeight = workVector.getBlockY() + orthogonalDirection.getBlockY() + direction.getBlockY();

    state = (world.getMaxHeight() - 2) > newHeight && world.getMinHeight() < newHeight ? TurtleState.RUNNING : TurtleState.DEAD;
    } else { // area too big

    state = TurtleState.DEAD;
    }

    return state;
    } else {// reached corner
    cornersFound++;

    // force a step forward to get the frame around the corner
    workVector.add(direction);

    switch (cornersFound) {
    case 1 -> {
    // gone one step back to set the result corner - we are already in the frame
    result = new Frame(Position.block(gateBlock.getLocation().add(direction.clone().multiply(-1))));

    // this is just part of the length in this direction and is set for a rough estimation about the max area
    lengthRun.add(direction.clone().multiply(lengthSinceLastCorner));
    lengthSinceLastCorner = 1;

    direction = rotateCorrect90DEGAroundAxis(direction, rotationAxis);
    orthogonalDirection = rotateCorrect90DEGAroundAxis(orthogonalDirection, rotationAxis);

    // another step in the new direction
    workVector.add(direction);

    state = TurtleState.RUNNING;
    return state;
    }
    case 2 -> {
    // remove the part between start and fist corner
    lengthRun.multiply(orthogonalDirection);
    lengthRun.add(direction.clone().multiply(lengthSinceLastCorner));
    lengthSinceLastCorner = 1;

    direction = rotateCorrect90DEGAroundAxis(direction, rotationAxis);
    orthogonalDirection = rotateCorrect90DEGAroundAxis(orthogonalDirection, rotationAxis);

    // another step in the new direction
    workVector.add(direction);

    state = TurtleState.RUNNING;
    return state;
    }
    case 3 -> {
    // replace estimation with actual length
    if (direction.getX() != 0) {
    lengthRun.setX(lengthSinceLastCorner);
    } else if (direction.getY() != 0) {
    lengthRun.setY(lengthSinceLastCorner);
    } else {
    lengthRun.setZ(lengthSinceLastCorner);
    }

    lengthSinceLastCorner = 1;
    // go one step back to set the result
    result.setCorner2(Position.block(gateBlock.getLocation().add(direction.clone().multiply(-1))));

    direction = rotateCorrect90DEGAroundAxis(direction, rotationAxis);
    orthogonalDirection = rotateCorrect90DEGAroundAxis(orthogonalDirection, rotationAxis);

    // another step in the new direction
    workVector.add(direction);

    state = TurtleState.RUNNING;
    return state;
    }
    case 4 -> {
    state = TurtleState.DONE;
    return state;
    }
    default -> { // should never happen
    // if we ever get here please push the next local cow of your area over.
    state = TurtleState.DEAD;
    return state;
    }
    } // end switch
    } // end reached corner
    } else { // invalid frame
    state = TurtleState.DEAD;
    return state;
    }
    }

    /**
    * {@link Vector#rotateAroundAxis(Vector, double)} is really useless for our case of blackface precision (1 or 0 not more, not less) since it is really sloppy with its precision
    * I removed the sin/cos approximation since we always want to rotate around 90° ala pi/2 anyways.
    *
    * @param toRotate
    * @param axis
    * @return
    */
    public static Vector rotateCorrect90DEGAroundAxis(@NotNull Vector toRotate, @NotNull Vector axis) {
    double x = toRotate.getX(), y = toRotate.getY(), z = toRotate.getZ();
    double x2 = axis.getX(), y2 = axis.getY(), z2 = axis.getZ();

    double dotProduct = toRotate.dot(axis); // x * x2 + y * y2 + z * z2;

    double xPrime = x2 * dotProduct + (-z2 * y + y2 * z);
    double yPrime = y2 * dotProduct + (z2 * x - x2 * z);
    double zPrime = z2 * dotProduct + (-y2 * x + x2 * y);

    return toRotate.setX(xPrime).setY(yPrime).setZ(zPrime);
    }

    private enum TurtleState {
    STARTING,
    RUNNING,
    DONE,
    DEAD
    }
    }

    private static class Frame {
    private final @NotNull BlockPosition corner1;
    private @Nullable BlockPosition corner2;

    private Frame(@NotNull BlockPosition corner1, @NotNull BlockPosition corner2) {
    this.corner1 = corner1;
    this.corner2 = corner2;
    }

    private Frame(@NotNull BlockPosition corner1) {
    this.corner1 = corner1;
    }

    protected void setCorner2(@NotNull BlockPosition corner2) {
    this.corner2 = corner2;
    }

    public @NotNull BlockPosition corner1() {
    return corner1;
    }

    public @Nullable BlockPosition corner2() {
    return corner2;
    }

    /**
    * Calculates the area of the frame, spanned by the two corner points.
    *
    * @return the area of the rectangle, or -1 if corner2 is null.
    */
    public int getArea () {
    if (corner2 == null) {
    return -1;
    } else {
    return
    (corner1.blockX() - corner2.blockX()) *
    (corner1.blockY() - corner2.blockY()) *
    (corner1.blockZ() - corner2.blockZ());
    }
    }
    }
    }