Repository: cabaletta/baritone (language: java)

Issue:
Pathfinding can fail with `IllegalStateException` when sneaking off a block
## Some information
Operating system: Linux
Java version: 21
Minecraft version: 1.20.6
Baritone version: fd8af9ba6722096b80d6e993854293cd22864486
Other mods (if used): Litematica 1.18.1 + Malilib 1.19.0

(i.e. the workspace I used to test #4527)

## Exception, error or logs
```
[16:31:28] [Render thread/INFO]: [CHAT] [Baritone] > path
[16:31:28] [Render thread/INFO]: [CHAT] [Baritone] Now pathing
[16:31:28] [pool-6-thread-3/INFO]: [STDERR]: java.lang.IllegalStateException: Path doubles back on itself, making a loop
[16:31:28] [pool-6-thread-3/INFO]: [STDERR]:    at baritone.api.pathing.calc.IPath.sanityCheck(IPath.java:174)
[16:31:28] [pool-6-thread-3/INFO]: [STDERR]:    at baritone.pathing.calc.Path.postProcess(Path.java:156)
[16:31:28] [pool-6-thread-3/INFO]: [STDERR]:    at java.base/java.util.Optional.map(Optional.java:260)
[16:31:28] [pool-6-thread-3/INFO]: [STDERR]:    at baritone.pathing.calc.AbstractNodeCostSearch.calculate(AbstractNodeCostSearch.java:106)
[16:31:28] [pool-6-thread-3/INFO]: [STDERR]:    at baritone.behavior.PathingBehavior.lambda$findPathInNewThread$2(PathingBehavior.java:506)
[16:31:28] [pool-6-thread-3/INFO]: [STDERR]:    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
[16:31:28] [pool-6-thread-3/INFO]: [STDERR]:    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
[16:31:28] [pool-6-thread-3/INFO]: [STDERR]:    at java.base/java.lang.Thread.run(Thread.java:1583)
```

## How to reproduce
Sneak off of an edge and set a goal such that the shortest path from the supporting block to the goal leads through your current position, then start the pathfinder.
E.g. like this: 
![2024-10-20_16 31 28](https://github.com/user-attachments/assets/78b97282-2cb9-4757-aec0-fc4ba388e2b5)


## Modified settings
None

## Final checklist
- [x] I know how to properly use check boxes 
- [x] I have included the version of Minecraft I'm running, baritone's version and forge mods (if used).
- [x] I have included logs, exceptions and / or steps to reproduce the issue.
- [x] I have not inserted any OwO's or UwU's into this issue.



Discussion / hints:

Fix patch:
diff --git a/build.gradle b/build.gradle
index 6cf42af69..45531e2cb 100755
--- a/build.gradle
+++ b/build.gradle
@@ -78,11 +78,9 @@ allprojects {
     }
 
     dependencies {
-        compileOnly "org.spongepowered:mixin:0.8"
-        compileOnly "org.ow2.asm:asm:9.7"
-        // The following line declares the yarn mappings you may select this one as well.
-        // mappings "net.fabricmc:yarn:1.17.1+build.32:v2"
-        //launchImplementation('dev.babbaj:nether-pathfinder:1.3.0')
+        compileOnly "org.spongepowered:mixin:${project.mixin_version}"
+        compileOnly "org.ow2.asm:asm:${project.asm_version}"
+
         implementation "dev.babbaj:nether-pathfinder:${project.nether_pathfinder_version}"
 
         implementation 'com.google.code.findbugs:jsr305:3.0.2'
diff --git a/gradle.properties b/gradle.properties
index dbfe4efb0..aa200b0ca 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,3 +17,8 @@ neoforge_version=20-beta
 fabric_version=0.15.11
 
 nether_pathfinder_version=1.4.1
+
+// These dependencies are used for common and tweaker
+// while mod loaders usually ship their own version
+mixin_version=0.8.5
+asm_version=9.7
diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java
index 1812fe486..d9cb501ef 100644
--- a/src/api/java/baritone/api/Settings.java
+++ b/src/api/java/baritone/api/Settings.java
@@ -29,6 +29,9 @@
 import net.minecraft.world.item.Item;
 import net.minecraft.world.level.block.Block;
 import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.Mirror;
+import net.minecraft.world.level.block.Rotation;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -1089,6 +1092,28 @@ public final class Settings {
      */
     public final Setting<Boolean> schematicOrientationZ = new Setting<>(false);
 
+    /**
+     * Rotates the schematic before building it.
+     * Possible values are
+     * <ul>
+     *  <li> NONE - No rotation </li>
+     *  <li> CLOCKWISE_90 - Rotate 90° clockwise </li>
+     *  <li> CLOCKWISE_180 - Rotate 180° clockwise </li>
+     *  <li> COUNTERCLOCKWISE_90 - Rotate 270° clockwise </li>
+     * </ul>
+     */
+    public final Setting<Rotation> buildSchematicRotation = new Setting<>(Rotation.NONE);
+
+    /**
+     * Mirrors the schematic before building it.
+     * Possible values are
+     * <ul>
+     *  <li> FRONT_BACK - mirror the schematic along its local x axis </li>
+     *  <li> LEFT_RIGHT - mirror the schematic along its local z axis </li>
+     * </ul>
+     */
+    public final Setting<Mirror> buildSchematicMirror = new Setting<>(Mirror.NONE);
+
     /**
      * The fallback used by the build command when no extension is specified. This may be useful if schematics of a
      * particular format are used often, and the user does not wish to have to specify the extension with every usage.
diff --git a/src/api/java/baritone/api/schematic/MirroredSchematic.java b/src/api/java/baritone/api/schematic/MirroredSchematic.java
new file mode 100644
index 000000000..b9e10808c
--- /dev/null
+++ b/src/api/java/baritone/api/schematic/MirroredSchematic.java
@@ -0,0 +1,114 @@
+/*
+ * This file is part of Baritone.
+ *
+ * Baritone is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Baritone is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Baritone.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package baritone.api.schematic;
+
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.block.Mirror;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class MirroredSchematic implements ISchematic {
+
+    private final ISchematic schematic;
+    private final Mirror mirror;
+
+    public MirroredSchematic(ISchematic schematic, Mirror mirror) {
+        this.schematic = schematic;
+        this.mirror = mirror;
+    }
+
+    @Override
+    public boolean inSchematic(int x, int y, int z, BlockState currentState) {
+        return schematic.inSchematic(
+                mirrorX(x, widthX(), mirror),
+                y,
+                mirrorZ(z, lengthZ(), mirror),
+                mirror(currentState, mirror)
+        );
+    }
+
+    @Override
+    public BlockState desiredState(int x, int y, int z, BlockState current, List<BlockState> approxPlaceable) {
+        return mirror(schematic.desiredState(
+                mirrorX(x, widthX(), mirror),
+                y,
+                mirrorZ(z, lengthZ(), mirror),
+                mirror(current, mirror),
+                mirror(approxPlaceable, mirror)
+        ), mirror);
+    }
+
+    @Override
+    public void reset() {
+        schematic.reset();
+    }
+
+    @Override
+    public int widthX() {
+        return schematic.widthX();
+    }
+
+    @Override
+    public int heightY() {
+        return schematic.heightY();
+    }
+
+    @Override
+    public int lengthZ() {
+        return schematic.lengthZ();
+    }
+
+    private static int mirrorX(int x, int sizeX, Mirror mirror) {
+        switch (mirror) {
+            case NONE:
+            case LEFT_RIGHT:
+                return x;
+            case FRONT_BACK:
+                return sizeX - x - 1;
+        }
+        throw new IllegalArgumentException("Unknown mirror");
+    }
+
+    private static int mirrorZ(int z, int sizeZ, Mirror mirror) {
+        switch (mirror) {
+            case NONE:
+            case FRONT_BACK:
+                return z;
+            case LEFT_RIGHT:
+                return sizeZ - z - 1;
+        }
+        throw new IllegalArgumentException("Unknown mirror");
+    }
+
+    private static BlockState mirror(BlockState state, Mirror mirror) {
+        if (state == null) {
+            return null;
+        }
+        return state.mirror(mirror);
+    }
+
+    private static List<BlockState> mirror(List<BlockState> states, Mirror mirror) {
+        if (states == null) {
+            return null;
+        }
+        return states.stream()
+                .map(s -> mirror(s, mirror))
+                .collect(Collectors.toList());
+    }
+}
diff --git a/src/api/java/baritone/api/schematic/RotatedSchematic.java b/src/api/java/baritone/api/schematic/RotatedSchematic.java
new file mode 100644
index 000000000..e3c9ed7aa
--- /dev/null
+++ b/src/api/java/baritone/api/schematic/RotatedSchematic.java
@@ -0,0 +1,136 @@
+/*
+ * This file is part of Baritone.
+ *
+ * Baritone is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Baritone is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Baritone.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package baritone.api.schematic;
+
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.block.Rotation;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class RotatedSchematic implements ISchematic {
+
+    private final ISchematic schematic;
+    private final Rotation rotation;
+    private final Rotation inverseRotation;
+
+    public RotatedSchematic(ISchematic schematic, Rotation rotation) {
+        this.schematic = schematic;
+        this.rotation = rotation;
+        // I don't think a 14 line switch would improve readability
+        this.inverseRotation = rotation.getRotated(rotation).getRotated(rotation);
+    }
+
+    @Override
+    public boolean inSchematic(int x, int y, int z, BlockState currentState) {
+        return schematic.inSchematic(
+                rotateX(x, z, widthX(), lengthZ(), inverseRotation),
+                y,
+                rotateZ(x, z, widthX(), lengthZ(), inverseRotation),
+                rotate(currentState, inverseRotation)
+        );
+    }
+
+    @Override
+    public BlockState desiredState(int x, int y, int z, BlockState current, List<BlockState> approxPlaceable) {
+        return rotate(schematic.desiredState(
+                rotateX(x, z, widthX(), lengthZ(), inverseRotation),
+                y,
+                rotateZ(x, z, widthX(), lengthZ(), inverseRotation),
+                rotate(current, inverseRotation),
+                rotate(approxPlaceable, inverseRotation)
+        ), rotation);
+    }
+
+    @Override
+    public void reset() {
+        schematic.reset();
+    }
+
+    @Override
+    public int widthX() {
+        return flipsCoordinates(rotation) ? schematic.lengthZ() : schematic.widthX();
+    }
+
+    @Override
+    public int heightY() {
+        return schematic.heightY();
+    }
+
+    @Override
+    public int lengthZ() {
+        return flipsCoordinates(rotation) ? schematic.widthX() : schematic.lengthZ();
+    }
+
+    /**
+     * Wether {@code rotation} swaps the x and z components
+     */
+    private static boolean flipsCoordinates(Rotation rotation) {
+        return rotation == Rotation.CLOCKWISE_90 || rotation == Rotation.COUNTERCLOCKWISE_90;
+    }
+
+    /**
+     * The x component of x,y after applying the rotation
+     */
+    private static int rotateX(int x, int z, int sizeX, int sizeZ, Rotation rotation) {
+        switch (rotation) {
+            case NONE:
+                return x;
+            case CLOCKWISE_90:
+                return sizeZ - z - 1;
+            case CLOCKWISE_180:
+                return sizeX - x - 1;
+            case COUNTERCLOCKWISE_90:
+                return z;
+        }
+        throw new IllegalArgumentException("Unknown rotation");
+    }
+
+    /**
+     * The z component of x,y after applying the rotation
+     */
+    private static int rotateZ(int x, int z, int sizeX, int sizeZ, Rotation rotation) {
+        switch (rotation) {
+            case NONE:
+                return z;
+            case CLOCKWISE_90:
+                return x;
+            case CLOCKWISE_180:
+                return sizeZ - z - 1;
+            case COUNTERCLOCKWISE_90:
+                return sizeX - x - 1;
+        }
+        throw new IllegalArgumentException("Unknown rotation");
+    }
+
+    private static BlockState rotate(BlockState state, Rotation rotation) {
+        if (state == null) {
+            return null;
+        }
+        return state.rotate(rotation);
+    }
+
+    private static List<BlockState> rotate(List<BlockState> states, Rotation rotation) {
+        if (states == null) {
+            return null;
+        }
+        return states.stream()
+                .map(s -> rotate(s, rotation))
+                .collect(Collectors.toList());
+    }
+}
diff --git a/src/api/java/baritone/api/utils/SettingsUtil.java b/src/api/java/baritone/api/utils/SettingsUtil.java
index d3eb8d831..23ffd7b03 100644
--- a/src/api/java/baritone/api/utils/SettingsUtil.java
+++ b/src/api/java/baritone/api/utils/SettingsUtil.java
@@ -27,6 +27,9 @@
 import net.minecraft.resources.ResourceLocation;
 import net.minecraft.world.item.Item;
 import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.Mirror;
+import net.minecraft.world.level.block.Rotation;
+
 import java.awt.*;
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
@@ -220,7 +223,8 @@ private enum Parser implements ISettingParser {
         FLOAT(Float.class, Float::parseFloat),
         LONG(Long.class, Long::parseLong),
         STRING(String.class, String::new),
-        DIRECTION(Direction.class, Direction::byName),
+        MIRROR(Mirror.class, Mirror::valueOf, Mirror::name),
+        ROTATION(Rotation.class, Rotation::valueOf, Rotation::name),
         COLOR(
                 Color.class,
                 str -> new Color(Integer.parseInt(str.split(",")[0]), Integer.parseInt(str.split(",")[1]), Integer.parseInt(str.split(",")[2])),
diff --git a/src/main/java/baritone/behavior/PathingBehavior.java b/src/main/java/baritone/behavior/PathingBehavior.java
index b7c035598..9e62765a2 100644
--- a/src/main/java/baritone/behavior/PathingBehavior.java
+++ b/src/main/java/baritone/behavior/PathingBehavior.java
@@ -265,7 +265,7 @@ public boolean secretInternalSetGoalAndPath(PathingCommand command) {
         if (goal == null) {
             return false;
         }
-        if (goal.isInGoal(ctx.playerFeet()) || goal.isInGoal(expectedSegmentStart)) {
+        if (goal.isInGoal(ctx.playerFeet())) {
             return false;
         }
         synchronized (pathPlanLock) {
@@ -553,7 +553,7 @@ private void findPathInNewThread(final BlockPos start, final boolean talkAboutIt
         });
     }
 
-    private static AbstractNodeCostSearch createPathfinder(BlockPos start, Goal goal, IPath previous, CalculationContext context) {
+    private AbstractNodeCostSearch createPathfinder(BlockPos start, Goal goal, IPath previous, CalculationContext context) {
         Goal transformed = goal;
         if (Baritone.settings().simplifyUnloadedYCoord.value && goal instanceof IGoalRenderPos) {
             BlockPos pos = ((IGoalRenderPos) goal).getGoalPos();
@@ -562,7 +562,14 @@ private static AbstractNodeCostSearch createPathfinder(BlockPos start, Goal goal
             }
         }
         Favoring favoring = new Favoring(context.getBaritone().getPlayerContext(), previous, context);
-        return new AStarPathFinder(start.getX(), start.getY(), start.getZ(), transformed, favoring, context);
+        BetterBlockPos feet = ctx.playerFeet();
+        var realStart = new BetterBlockPos(start);
+        var sub = feet.subtract(realStart);
+        if (feet.getY() == realStart.getY() && Math.abs(sub.getX()) <= 1 && Math.abs(sub.getZ()) <= 1) {
+            realStart = feet;
+        }
+        return new AStarPathFinder(realStart, start.getX(), start.getY(), start.getZ(), transformed, favoring, context);
+
     }
 
     @Override
diff --git a/src/main/java/baritone/command/defaults/LitematicaCommand.java b/src/main/java/baritone/command/defaults/LitematicaCommand.java
index bfe0079b3..2e251b224 100644
--- a/src/main/java/baritone/command/defaults/LitematicaCommand.java
+++ b/src/main/java/baritone/command/defaults/LitematicaCommand.java
@@ -34,18 +34,9 @@ public LitematicaCommand(IBaritone baritone) {
 
     @Override
     public void execute(String label, IArgConsumer args) throws CommandException {
-        int schematic = 0;
-        if (args.hasAny()) {
-            args.requireMax(1);
-            if (args.is(Integer.class)) {
-                schematic = args.getAs(Integer.class) - 1;
-            }
-        }
-        try {
-            baritone.getBuilderProcess().buildOpenLitematic(schematic);
-        } catch (IndexOutOfBoundsException e) {
-            logDirect("Pleas provide a valid index.");
-        }
+        args.requireMax(1);
+        int schematic = args.hasAny() ? args.getAs(Integer.class) - 1 : 0;
+        baritone.getBuilderProcess().buildOpenLitematic(schematic);
     }
 
     @Override
diff --git a/src/main/java/baritone/command/defaults/SelCommand.java b/src/main/java/baritone/command/defaults/SelCommand.java
index 233f118f2..18b6db8d1 100644
--- a/src/main/java/baritone/command/defaults/SelCommand.java
+++ b/src/main/java/baritone/command/defaults/SelCommand.java
@@ -228,12 +228,7 @@ public void execute(String label, IArgConsumer args) throws CommandException {
                         }
                     }
                 }
-                ISchematic schematic = new StaticSchematic() {{
-                    states = blockstates;
-                    x = size.getX();
-                    y = size.getY();
-                    z = size.getZ();
-                }};
+                ISchematic schematic = new StaticSchematic(blockstates);
                 composite.put(schematic, min.x - origin.x, min.y - origin.y, min.z - origin.z);
             }
             clipboard = composite;
diff --git a/src/main/java/baritone/pathing/calc/AStarPathFinder.java b/src/main/java/baritone/pathing/calc/AStarPathFinder.java
index b37b598ff..0537eac5f 100644
--- a/src/main/java/baritone/pathing/calc/AStarPathFinder.java
+++ b/src/main/java/baritone/pathing/calc/AStarPathFinder.java
@@ -41,8 +41,8 @@ public final class AStarPathFinder extends AbstractNodeCostSearch {
     private final Favoring favoring;
     private final CalculationContext calcContext;
 
-    public AStarPathFinder(int startX, int startY, int startZ, Goal goal, Favoring favoring, CalculationContext context) {
-        super(startX, startY, startZ, goal, context);
+    public AStarPathFinder(BetterBlockPos realStart, int startX, int startY, int startZ, Goal goal, Favoring favoring, CalculationContext context) {
+        super(realStart, startX, startY, startZ, goal, context);
         this.favoring = favoring;
         this.calcContext = context;
     }
@@ -96,7 +96,7 @@ protected Optional<IPath> calculate0(long primaryTimeout, long failureTimeout) {
             numNodes++;
             if (goal.isInGoal(currentNode.x, currentNode.y, currentNode.z)) {
                 logDebug("Took " + (System.currentTimeMillis() - startTime) + "ms, " + numMovementsConsidered + " movements considered");
-                return Optional.of(new Path(startNode, currentNode, numNodes, goal, calcContext));
+                return Optional.of(new Path(realStart, startNode, currentNode, numNodes, goal, calcContext));
             }
             for (Moves moves : allMoves) {
                 int newX = currentNode.x + moves.xOffset;
diff --git a/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java b/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java
index d20b519dc..0bfb6ac3f 100644
--- a/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java
+++ b/src/main/java/baritone/pathing/calc/AbstractNodeCostSearch.java
@@ -36,6 +36,7 @@
  */
 public abstract class AbstractNodeCostSearch implements IPathFinder, Helper {
 
+    protected final BetterBlockPos realStart;
     protected final int startX;
     protected final int startY;
     protected final int startZ;
@@ -81,7 +82,8 @@ public abstract class AbstractNodeCostSearch implements IPathFinder, Helper {
      */
     protected static final double MIN_IMPROVEMENT = 0.01;
 
-    AbstractNodeCostSearch(int startX, int startY, int startZ, Goal goal, CalculationContext context) {
+    AbstractNodeCostSearch(BetterBlockPos realStart, int startX, int startY, int startZ, Goal goal, CalculationContext context) {
+        this.realStart = realStart;
         this.startX = startX;
         this.startY = startY;
         this.startZ = startZ;
@@ -177,7 +179,7 @@ protected PathNode getNodeAtPosition(int x, int y, int z, long hashCode) {
 
     @Override
     public Optional<IPath> pathToMostRecentNodeConsidered() {
-        return Optional.ofNullable(mostRecentConsidered).map(node -> new Path(startNode, node, 0, goal, context));
+        return Optional.ofNullable(mostRecentConsidered).map(node -> new Path(realStart, startNode, node, 0, goal, context));
     }
 
     @Override
@@ -208,7 +210,7 @@ protected Optional<IPath> bestSoFar(boolean logInfo, int numNodes) {
                     System.out.println("Path goes for " + Math.sqrt(dist) + " blocks");
                     logDebug("A* cost coefficient " + COEFFICIENTS[i]);
                 }
-                return Optional.of(new Path(startNode, bestSoFar[i], numNodes, goal, context));
+                return Optional.of(new Path(realStart, startNode, bestSoFar[i], numNodes, goal, context));
             }
         }
         // instead of returning bestSoFar[0], be less misleading
diff --git a/src/main/java/baritone/pathing/calc/Path.java b/src/main/java/baritone/pathing/calc/Path.java
index f7bfbaa24..8f5dd52c2 100644
--- a/src/main/java/baritone/pathing/calc/Path.java
+++ b/src/main/java/baritone/pathing/calc/Path.java
@@ -27,6 +27,7 @@
 import baritone.pathing.movement.Moves;
 import baritone.pathing.path.CutoffPath;
 import baritone.utils.pathing.PathBase;
+import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -68,28 +69,40 @@ class Path extends PathBase {
 
     private volatile boolean verified;
 
-    Path(PathNode start, PathNode end, int numNodes, Goal goal, CalculationContext context) {
-        this.start = new BetterBlockPos(start.x, start.y, start.z);
+    Path(BetterBlockPos realStart, PathNode start, PathNode end, int numNodes, Goal goal, CalculationContext context) {
         this.end = new BetterBlockPos(end.x, end.y, end.z);
         this.numNodes = numNodes;
         this.movements = new ArrayList<>();
         this.goal = goal;
         this.context = context;
+
         PathNode current = end;
-        LinkedList<BetterBlockPos> tempPath = new LinkedList<>();
-        LinkedList<PathNode> tempNodes = new LinkedList<>();
-        // Repeatedly inserting to the beginning of an arraylist is O(n^2)
-        // Instead, do it into a linked list, then convert at the end
+        List<BetterBlockPos> tempPath = new ArrayList<>();
+        List<PathNode> tempNodes = new ArrayList<>();
         while (current != null) {
-            tempNodes.addFirst(current);
-            tempPath.addFirst(new BetterBlockPos(current.x, current.y, current.z));
+            tempNodes.add(current);
+            tempPath.add(new BetterBlockPos(current.x, current.y, current.z));
             current = current.previous;
         }
-        // Can't directly convert from the PathNode pseudo linked list to an array because we don't know how long it is
-        // inserting into a LinkedList<E> keeps track of length, then when we addall (which calls .toArray) it's able
-        // to performantly do that conversion since it knows the length.
-        this.path = new ArrayList<>(tempPath);
-        this.nodes = new ArrayList<>(tempNodes);
+
+        // If the position the player is at is different from the position we told A* to start from,
+        // and A* gave us no movements, then add a fake node that will allow a movement to be created
+        // that gets us to the single position in the path.
+        // See PathingBehavior#createPathfinder and https://github.com/cabaletta/baritone/pull/4519
+        var startNodePos = new BetterBlockPos(start.x, start.y, start.z);
+        if (!realStart.equals(startNodePos) && start.equals(end)) {
+            this.start = realStart;
+            PathNode fakeNode = new PathNode(realStart.x, realStart.y, realStart.z, goal);
+            fakeNode.cost = 0;
+            tempNodes.add(fakeNode);
+            tempPath.add(realStart);
+        } else {
+            this.start = startNodePos;
+        }
+
+        // Nodes are traversed last to first so we need to reverse the list
+        this.path = Lists.reverse(tempPath);
+        this.nodes = Lists.reverse(tempNodes);
     }
 
     @Override
diff --git a/src/main/java/baritone/pathing/movement/MovementHelper.java b/src/main/java/baritone/pathing/movement/MovementHelper.java
index 66d497d9e..63ef62420 100644
--- a/src/main/java/baritone/pathing/movement/MovementHelper.java
+++ b/src/main/java/baritone/pathing/movement/MovementHelper.java
@@ -421,7 +421,7 @@ static Ternary canWalkOnBlockState(BlockState state) {
         if (block == Blocks.LADDER || (block == Blocks.VINE && Baritone.settings().allowVines.value)) { // TODO reconsider this
             return YES;
         }
-        if (block == Blocks.FARMLAND || block == Blocks.DIRT_PATH) {
+        if (block == Blocks.FARMLAND || block == Blocks.DIRT_PATH || block == Blocks.SOUL_SAND) {
             return YES;
         }
         if (block == Blocks.ENDER_CHEST || block == Blocks.CHEST || block == Blocks.TRAPPED_CHEST) {
diff --git a/src/main/java/baritone/process/BuilderProcess.java b/src/main/java/baritone/process/BuilderProcess.java
index 301019275..0265a49ce 100644
--- a/src/main/java/baritone/process/BuilderProcess.java
+++ b/src/main/java/baritone/process/BuilderProcess.java
@@ -30,6 +30,8 @@
 import baritone.api.schematic.IStaticSchematic;
 import baritone.api.schematic.MaskSchematic;
 import baritone.api.schematic.SubstituteSchematic;
+import baritone.api.schematic.RotatedSchematic;
+import baritone.api.schematic.MirroredSchematic;
 import baritone.api.schematic.format.ISchematicFormat;
 import baritone.api.utils.*;
 import baritone.api.utils.input.Input;
@@ -42,17 +44,13 @@
 import baritone.utils.schematic.MapArtSchematic;
 import baritone.utils.schematic.SelectionSchematic;
 import baritone.utils.schematic.SchematicSystem;
-import baritone.utils.schematic.format.defaults.LitematicaSchematic;
 import baritone.utils.schematic.litematica.LitematicaHelper;
 import baritone.utils.schematic.schematica.SchematicaHelper;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
 import net.minecraft.core.BlockPos;
 import net.minecraft.core.Direction;
 import net.minecraft.core.Vec3i;
-import net.minecraft.nbt.NbtAccounter;
-import net.minecraft.nbt.NbtIo;
 import net.minecraft.util.Tuple;
 import net.minecraft.world.InteractionHand;
 import net.minecraft.world.item.BlockItem;
@@ -77,7 +75,6 @@
 
 import java.io.File;
 import java.io.FileInputStream;
-import java.nio.file.Files;
 import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -120,6 +117,12 @@ public void build(String name, ISchematic schematic, Vec3i origin) {
         if (!Baritone.settings().buildSubstitutes.value.isEmpty()) {
             this.schematic = new SubstituteSchematic(this.schematic, Baritone.settings().buildSubstitutes.value);
         }
+        if (Baritone.settings().buildSchematicMirror.value != net.minecraft.world.level.block.Mirror.NONE) {
+            this.schematic = new MirroredSchematic(this.schematic, Baritone.settings().buildSchematicMirror.value);
+        }
+        if (Baritone.settings().buildSchematicRotation.value != net.minecraft.world.level.block.Rotation.NONE) {
+            this.schematic = new RotatedSchematic(this.schematic, Baritone.settings().buildSchematicRotation.value);
+        }
         // TODO this preserves the old behavior, but maybe we should bake the setting value right here
         this.schematic = new MaskSchematic(this.schematic) {
             @Override
@@ -230,19 +233,13 @@ public void buildOpenSchematic() {
     public void buildOpenLitematic(int i) {
         if (LitematicaHelper.isLitematicaPresent()) {
             //if java.lang.NoSuchMethodError is thrown see comment in SchematicPlacementManager
-            if (LitematicaHelper.hasLoadedSchematic()) {
-                String name = LitematicaHelper.getName(i);
-                try {
-                    LitematicaSchematic schematic1 = new LitematicaSchematic(NbtIo.readCompressed(Files.newInputStream(LitematicaHelper.getSchematicFile(i).toPath()), NbtAccounter.unlimitedHeap()), false);
-                    Vec3i correctedOrigin = LitematicaHelper.getCorrectedOrigin(schematic1, i);
-                    ISchematic schematic2 = LitematicaHelper.blackMagicFuckery(schematic1, i);
-                    schematic2 = applyMapArtAndSelection(origin, (IStaticSchematic) schematic2);
-                    build(name, schematic2, correctedOrigin);
-                } catch (Exception e) {
-                    logDirect("Schematic File could not be loaded.");
-                }
+            if (LitematicaHelper.hasLoadedSchematic(i)) {
+                Tuple<IStaticSchematic, Vec3i> schematic = LitematicaHelper.getSchematic(i);
+                Vec3i correctedOrigin = schematic.getB();
+                ISchematic schematic2 = applyMapArtAndSelection(correctedOrigin, schematic.getA());
+                build(schematic.getA().toString(), schematic2, correctedOrigin);
             } else {
-                logDirect("No schematic currently loaded");
+                logDirect(String.format("List of placements has no entry %s", i + 1));
             }
         } else {
             logDirect("Litematica is not present");
@@ -373,7 +370,11 @@ private Optional<Placement> possibleToPlace(BlockState toPlace, int x, int y, in
             if (!placementPlausible(new BetterBlockPos(x, y, z), toPlace)) {
                 continue;
             }
-            AABB aabb = placeAgainstState.getShape(ctx.world(), placeAgainstPos).bounds();
+            VoxelShape shape = placeAgainstState.getShape(ctx.world(), placeAgainstPos);
+            if (shape.isEmpty()) {
+                continue;
+            }
+            AABB aabb = shape.bounds();
             for (Vec3 placementMultiplier : aabbSideMultipliers(against)) {
                 double placeX = placeAgainstPos.x + aabb.minX * placementMultiplier.x + aabb.maxX * (1 - placementMultiplier.x);
                 double placeY = placeAgainstPos.y + aabb.minY * placementMultiplier.y + aabb.maxY * (1 - placementMultiplier.y);
@@ -717,13 +718,16 @@ private Goal assemble(BuilderCalculationContext bcc, List<BlockState> approxPlac
         List<BetterBlockPos> sourceLiquids = new ArrayList<>();
         List<BetterBlockPos> flowingLiquids = new ArrayList<>();
         Map<BlockState, Integer> missing = new HashMap<>();
+        List<BetterBlockPos> outOfBounds = new ArrayList<>();
         incorrectPositions.forEach(pos -> {
             BlockState state = bcc.bsi.get0(pos);
             if (state.getBlock() instanceof AirBlock) {
-                if (containsBlockState(approxPlaceable, bcc.getSchematic(pos.x, pos.y, pos.z, state))) {
+                BlockState desired = bcc.getSchematic(pos.x, pos.y, pos.z, state);
+                if (desired == null) {
+                    outOfBounds.add(pos);
+                } else if (containsBlockState(approxPlaceable, desired)) {
                     placeable.add(pos);
                 } else {
-                    BlockState desired = bcc.getSchematic(pos.x, pos.y, pos.z, state);
                     missing.put(desired, 1 + missing.getOrDefault(desired, 0));
                 }
             } else {
@@ -741,6 +745,7 @@ private Goal assemble(BuilderCalculationContext bcc, List<BlockState> approxPlac
                 }
             }
         });
+        incorrectPositions.removeAll(outOfBounds);
         List<Goal> toBreak = new ArrayList<>();
         breakable.forEach(pos -> toBreak.add(breakGoal(pos, bcc)));
         List<Goal> toPlace = new ArrayList<>();
diff --git a/src/main/java/baritone/selection/Selection.java b/src/main/java/baritone/selection/Selection.java
index 69c77d668..116e048c5 100644
--- a/src/main/java/baritone/selection/Selection.java
+++ b/src/main/java/baritone/selection/Selection.java
@@ -37,7 +37,7 @@ public Selection(BetterBlockPos pos1, BetterBlockPos pos2) {
                 max.z - min.z + 1
         );
 
-        this.aabb = new AABB(this.min);
+        this.aabb = new AABB(min.x, min.y, min.z, max.x + 1, max.y + 1, max.z + 1);
     }
 
     @Override
diff --git a/src/main/java/baritone/utils/schematic/StaticSchematic.java b/src/main/java/baritone/utils/schematic/StaticSchematic.java
index 5a110281b..0c875c724 100644
--- a/src/main/java/baritone/utils/schematic/StaticSchematic.java
+++ b/src/main/java/baritone/utils/schematic/StaticSchematic.java
@@ -32,6 +32,16 @@ public class StaticSchematic extends AbstractSchematic implements IStaticSchemat
 
     protected BlockState[][][] states;
 
+    public StaticSchematic() {}
+
+    public StaticSchematic(BlockState[][][] states) {
+        this.states = states;
+        boolean empty = states.length == 0 || states[0].length == 0 || states[0][0].length == 0;
+        this.x = empty ? 0 : states.length;
+        this.z = empty ? 0 : states[0].length;
+        this.y = empty ? 0 : states[0][0].length;
+    }
+
     @Override
     public BlockState desiredState(int x, int y, int z, BlockState current, List<BlockState> approxPlaceable) {
         return this.states[x][z][y];
diff --git a/src/main/java/baritone/utils/schematic/format/DefaultSchematicFormats.java b/src/main/java/baritone/utils/schematic/format/DefaultSchematicFormats.java
index d30867ab8..f51eb371d 100644
--- a/src/main/java/baritone/utils/schematic/format/DefaultSchematicFormats.java
+++ b/src/main/java/baritone/utils/schematic/format/DefaultSchematicFormats.java
@@ -84,7 +84,7 @@ public IStaticSchematic parse(InputStream input) throws IOException {
                 case 6: //1.18-1.20
                     throw new UnsupportedOperationException("This litematic Version is too old.");
                 case 7: //1.21+
-                    return new LitematicaSchematic(nbt, false);
+                    return new LitematicaSchematic(nbt);
                 default:
                     throw new UnsupportedOperationException("Unsuported Version of a Litematica Schematic");
             }
diff --git a/src/main/java/baritone/utils/schematic/format/defaults/LitematicaSchematic.java b/src/main/java/baritone/utils/schematic/format/defaults/LitematicaSchematic.java
index 92e8a39df..d43cce65d 100644
--- a/src/main/java/baritone/utils/schematic/format/defaults/LitematicaSchematic.java
+++ b/src/main/java/baritone/utils/schematic/format/defaults/LitematicaSchematic.java
@@ -17,8 +17,9 @@
 
 package baritone.utils.schematic.format.defaults;
 
+import baritone.api.schematic.CompositeSchematic;
+import baritone.api.schematic.IStaticSchematic;
 import baritone.utils.schematic.StaticSchematic;
-import net.minecraft.core.Registry;
 import net.minecraft.core.Vec3i;
 import net.minecraft.core.registries.BuiltInRegistries;
 import net.minecraft.nbt.CompoundTag;
@@ -30,6 +31,7 @@
 import org.apache.commons.lang3.Validate;
 
 import javax.annotation.Nullable;
+import java.util.Collections;
 import java.util.Optional;
 
 /**
@@ -39,35 +41,24 @@
  * @author rycbar
  * @since 22.09.2022
  */
-public final class LitematicaSchematic extends StaticSchematic {
-    private final Vec3i offsetMinCorner;
-    private final CompoundTag nbt;
+public final class LitematicaSchematic extends CompositeSchematic implements IStaticSchematic {
 
     /**
      * @param nbtTagCompound a decompressed file stream aka nbt data.
      * @param rotated        if the schematic is rotated by 90°.
      */
-    public LitematicaSchematic(CompoundTag nbtTagCompound, boolean rotated) {
-        this.nbt = nbtTagCompound;
-        this.offsetMinCorner = new Vec3i(getMinOfSchematic("x"), getMinOfSchematic("y"), getMinOfSchematic("z"));
-        this.y = Math.abs(nbt.getCompound("Metadata").getCompound("EnclosingSize").getInt("y"));
-
-        if (rotated) {
-            this.x = Math.abs(nbt.getCompound("Metadata").getCompound("EnclosingSize").getInt("z"));
-            this.z = Math.abs(nbt.getCompound("Metadata").getCompound("EnclosingSize").getInt("x"));
-        } else {
-            this.x = Math.abs(nbt.getCompound("Metadata").getCompound("EnclosingSize").getInt("x"));
-            this.z = Math.abs(nbt.getCompound("Metadata").getCompound("EnclosingSize").getInt("z"));
-        }
-        this.states = new BlockState[this.x][this.z][this.y];
-        fillInSchematic();
+    public LitematicaSchematic(CompoundTag nbt) {
+        super(0, 0, 0);
+        fillInSchematic(nbt);
     }
 
     /**
-     * @return Array of subregion names.
+     * @return Array of subregion tags.
      */
-    private static String[] getRegions(CompoundTag nbt) {
-        return nbt.getCompound("Regions").getAllKeys().toArray(new String[0]);
+    private static CompoundTag[] getRegions(CompoundTag nbt) {
+        return nbt.getCompound("Regions").getAllKeys().stream()
+                .map(nbt.getCompound("Regions")::getCompound)
+                .toArray(CompoundTag[]::new);
     }
 
     /**
@@ -76,14 +67,10 @@ private static String[] getRegions(CompoundTag nbt) {
      * @param s axis that should be read.
      * @return the lower coord of the requested axis.
      */
-    private static int getMinOfSubregion(CompoundTag nbt, String subReg, String s) {
-        int a = nbt.getCompound("Regions").getCompound(subReg).getCompound("Position").getInt(s);
-        int b = nbt.getCompound("Regions").getCompound(subReg).getCompound("Size").getInt(s);
-        if (b < 0) {
-            b++;
-        }
-        return Math.min(a, a + b);
-
+    private static int getMinOfSubregion(CompoundTag subReg, String s) {
+        int a = subReg.getCompound("Position").getInt(s);
+        int b = subReg.getCompound("Size").getInt(s);
+        return Math.min(a, a + b + 1);
     }
 
     /**
@@ -110,7 +97,7 @@ private static BlockState[] getBlockList(ListTag blockStatePalette) {
     private static BlockState getBlockState(Block block, CompoundTag properties) {
         BlockState blockState = block.defaultBlockState();
 
-        for (Object key : properties.getAllKeys().toArray()) {
+        for (Object key : properties.getAllKeys()) {
             Property<?> property = block.getStateDefinition().getProperty((String) key);
             String propertyValue = properties.getString((String) key);
             if (property != null) {
@@ -146,43 +133,19 @@ private static int getBitsPerBlock(int amountOfBlockTypes) {
      *
      * @return the volume of the subregion.
      */
-    private static long getVolume(CompoundTag nbt, String subReg) {
-        return Math.abs(
-                nbt.getCompound("Regions").getCompound(subReg).getCompound("Size").getInt("x") *
-                        nbt.getCompound("Regions").getCompound(subReg).getCompound("Size").getInt("y") *
-                        nbt.getCompound("Regions").getCompound(subReg).getCompound("Size").getInt("z"));
-    }
-
-    /**
-     * @return array of Long values.
-     */
-    private static long[] getBlockStates(CompoundTag nbt, String subReg) {
-        return nbt.getCompound("Regions").getCompound(subReg).getLongArray("BlockStates");
-    }
-
-    /**
-     * Subregion don't have to be the same size as the enclosing size of the schematic. If they are smaller we check here if the current block is part of the subregion.
-     *
-     * @param x coord of the block relative to the minimum corner.
-     * @param y coord of the block relative to the minimum corner.
-     * @param z coord of the block relative to the minimum corner.
-     * @return if the current block is part of the subregion.
-     */
-    private static boolean inSubregion(CompoundTag nbt, String subReg, int x, int y, int z) {
-        return x >= 0 && y >= 0 && z >= 0 &&
-                x < Math.abs(nbt.getCompound("Regions").getCompound(subReg).getCompound("Size").getInt("x")) &&
-                y < Math.abs(nbt.getCompound("Regions").getCompound(subReg).getCompound("Size").getInt("y")) &&
-                z < Math.abs(nbt.getCompound("Regions").getCompound(subReg).getCompound("Size").getInt("z"));
+    private static long getVolume(CompoundTag subReg) {
+        CompoundTag size = subReg.getCompound("Size");
+        return Math.abs(size.getInt("x") * size.getInt("y") * size.getInt("z"));
     }
 
     /**
      * @param s axis.
      * @return the lowest coordinate of that axis of the schematic.
      */
-    private int getMinOfSchematic(String s) {
+    private static int getMinOfSchematic(CompoundTag nbt, String s) {
         int n = Integer.MAX_VALUE;
-        for (String subReg : getRegions(nbt)) {
-            n = Math.min(n, getMinOfSubregion(nbt, subReg, s));
+        for (CompoundTag subReg : getRegions(nbt)) {
+            n = Math.min(n, getMinOfSubregion(subReg, s));
         }
         return n;
     }
@@ -190,18 +153,18 @@ private int getMinOfSchematic(String s) {
     /**
      * reads the file data.
      */
-    private void fillInSchematic() {
-        for (String subReg : getRegions(nbt)) {
-            ListTag usedBlockTypes = nbt.getCompound("Regions").getCompound(subReg).getList("BlockStatePalette", 10);
+    private void fillInSchematic(CompoundTag nbt) {
+        Vec3i offsetMinCorner = new Vec3i(getMinOfSchematic(nbt, "x"), getMinOfSchematic(nbt, "y"), getMinOfSchematic(nbt, "z"));
+        for (CompoundTag subReg : getRegions(nbt)) {
+            ListTag usedBlockTypes = subReg.getList("BlockStatePalette", 10);
             BlockState[] blockList = getBlockList(usedBlockTypes);
 
             int bitsPerBlock = getBitsPerBlock(usedBlockTypes.size());
-            long regionVolume = getVolume(nbt, subReg);
-            long[] blockStateArray = getBlockStates(nbt, subReg);
+            long regionVolume = getVolume(subReg);
+            long[] blockStateArray = subReg.getLongArray("BlockStates");
 
             LitematicaBitArray bitArray = new LitematicaBitArray(bitsPerBlock, regionVolume, blockStateArray);
-
-            writeSubregionIntoSchematic(nbt, subReg, blockList, bitArray);
+            writeSubregionIntoSchematic(subReg, offsetMinCorner, blockList, bitArray);
         }
     }
 
@@ -211,65 +174,30 @@ private void fillInSchematic() {
      * @param blockList list with the different block types used in the schematic.
      * @param bitArray  bit array that holds the placement pattern.
      */
-    private void writeSubregionIntoSchematic(CompoundTag nbt, String subReg, BlockState[] blockList, LitematicaBitArray bitArray) {
-        Vec3i offsetSubregion = new Vec3i(getMinOfSubregion(nbt, subReg, "x"), getMinOfSubregion(nbt, subReg, "y"), getMinOfSubregion(nbt, subReg, "z"));
+    private void writeSubregionIntoSchematic(CompoundTag subReg, Vec3i offsetMinCorner, BlockState[] blockList, LitematicaBitArray bitArray) {
+        int offsetX = getMinOfSubregion(subReg, "x") - offsetMinCorner.getX();
+        int offsetY = getMinOfSubregion(subReg, "y") - offsetMinCorner.getY();
+        int offsetZ = getMinOfSubregion(subReg, "z") - offsetMinCorner.getZ();
+        CompoundTag size = subReg.getCompound("Size");
+        int sizeX = Math.abs(size.getInt("x"));
+        int sizeY = Math.abs(size.getInt("y"));
+        int sizeZ = Math.abs(size.getInt("z"));
+        BlockState[][][] states = new BlockState[sizeX][sizeZ][sizeY];
         int index = 0;
-        for (int y = 0; y < this.y; y++) {
-            for (int z = 0; z < this.z; z++) {
-                for (int x = 0; x < this.x; x++) {
-                    if (inSubregion(nbt, subReg, x, y, z)) {
-                        this.states[x - (offsetMinCorner.getX() - offsetSubregion.getX())][z - (offsetMinCorner.getZ() - offsetSubregion.getZ())][y - (offsetMinCorner.getY() - offsetSubregion.getY())] = blockList[bitArray.getAt(index)];
-                        index++;
-                    }
+        for (int y = 0; y < sizeY; y++) {
+            for (int z = 0; z < sizeZ; z++) {
+                for (int x = 0; x < sizeX; x++) {
+                    states[x][z][y] = blockList[bitArray.getAt(index)];
+                    index++;
                 }
             }
         }
+        this.put(new StaticSchematic(states), offsetX, offsetY, offsetZ);
     }
 
-    /**
-     * @return offset from the schematic origin to the minimum Corner as a Vec3i.
-     */
-    public Vec3i getOffsetMinCorner() {
-        return offsetMinCorner;
-    }
-
-    /**
-     * @return x size of the schematic.
-     */
-    public int getX() {
-        return this.x;
-    }
-
-    /**
-     * @return y size of the schematic.
-     */
-    public int getY() {
-        return this.y;
-    }
-
-    /**
-     * @return z size of the schematic.
-     */
-    public int getZ() {
-        return this.z;
-    }
-
-    /**
-     * @param x          position relative to the minimum corner of the schematic.
-     * @param y          position relative to the minimum corner of the schematic.
-     * @param z          position relative to the minimum corner of the schematic.
-     * @param blockState new blockstate of the block at this position.
-     */
-    public void setDirect(int x, int y, int z, BlockState blockState) {
-        this.states[x][z][y] = blockState;
-    }
-
-    /**
-     * @param rotated if the schematic is rotated by 90°.
-     * @return a copy of the schematic.
-     */
-    public LitematicaSchematic getCopy(boolean rotated) {
-        return new LitematicaSchematic(nbt, rotated);
+    @Override
+    public BlockState getDirect(int x, int y, int z) {
+        return desiredState(x, y, z, null, Collections.emptyList());
     }
 
     /**
diff --git a/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java b/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java
index 3b05c23ef..081d32cc8 100644
--- a/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java
+++ b/src/main/java/baritone/utils/schematic/litematica/LitematicaHelper.java
@@ -17,15 +17,25 @@
 
 package baritone.utils.schematic.litematica;
 
-import baritone.utils.schematic.format.defaults.LitematicaSchematic;
+import baritone.api.schematic.CompositeSchematic;
+import baritone.api.schematic.IStaticSchematic;
+import baritone.utils.schematic.StaticSchematic;
 import fi.dy.masa.litematica.Litematica;
 import fi.dy.masa.litematica.data.DataManager;
+import fi.dy.masa.litematica.schematic.placement.SchematicPlacement;
+import fi.dy.masa.litematica.schematic.placement.SubRegionPlacement;
+import fi.dy.masa.litematica.world.SchematicWorldHandler;
+import fi.dy.masa.litematica.world.WorldSchematic;
+import net.minecraft.core.BlockPos;
 import net.minecraft.core.Vec3i;
+import net.minecraft.util.Tuple;
 import net.minecraft.world.level.block.Mirror;
 import net.minecraft.world.level.block.Rotation;
 import net.minecraft.world.level.block.state.BlockState;
 
-import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * Helper class that provides access or processes data related to Litmatica schematics.
@@ -48,168 +58,98 @@ public static boolean isLitematicaPresent() {
     }
 
     /**
-     * @return if there are loaded schematics.
+     * @return if {@code i} is a valid placement index
      */
-    public static boolean hasLoadedSchematic() {
-        return DataManager.getSchematicPlacementManager().getAllSchematicsPlacements().size() > 0;
+    public static boolean hasLoadedSchematic(int i) {
+        return 0 <= i && i < DataManager.getSchematicPlacementManager().getAllSchematicsPlacements().size();
     }
 
-    /**
-     * @param i index of the Schematic in the schematic placement list.
-     * @return the name of the requested schematic.
-     */
-    public static String getName(int i) {
-        return DataManager.getSchematicPlacementManager().getAllSchematicsPlacements().get(i).getName();
-    }
-
-    /**
-     * @param i index of the Schematic in the schematic placement list.
-     * @return the world coordinates of the schematic origin. This can but does not have to be the minimum corner.
-     */
-    public static Vec3i getOrigin(int i) {
-        return DataManager.getSchematicPlacementManager().getAllSchematicsPlacements().get(i).getOrigin();
+    private static SchematicPlacement getPlacement(int i) {
+        return DataManager.getSchematicPlacementManager().getAllSchematicsPlacements().get(i);
     }
 
-    /**
-     * @param i index of the Schematic in the schematic placement list.
-     * @return Filepath of the schematic file.
-     */
-    public static File getSchematicFile(int i) {
-        return DataManager.getSchematicPlacementManager().getAllSchematicsPlacements().get(i).getSchematicFile();
-    }
-
-    /**
-     * @param i index of the Schematic in the schematic placement list.
-     * @return rotation of the schematic placement.
-     */
-    public static Rotation getRotation(int i) {
-        return DataManager.getSchematicPlacementManager().getAllSchematicsPlacements().get(i).getRotation();
-    }
-
-    /**
-     * @param i index of the Schematic in the schematic placement list.
-     * @return the mirroring of the schematic placement.
-     */
-    public static Mirror getMirror(int i) {
-        return DataManager.getSchematicPlacementManager().getAllSchematicsPlacements().get(i).getMirror();
-    }
-
-    /**
-     * @param schematic original schematic.
-     * @param i         index of the Schematic in the schematic placement list.
-     * @return the minimum corner coordinates of the schematic, after the original schematic got rotated and mirrored.
-     */
-    public static Vec3i getCorrectedOrigin(LitematicaSchematic schematic, int i) {
-        int x = LitematicaHelper.getOrigin(i).getX();
-        int y = LitematicaHelper.getOrigin(i).getY();
-        int z = LitematicaHelper.getOrigin(i).getZ();
-        int mx = schematic.getOffsetMinCorner().getX();
-        int my = schematic.getOffsetMinCorner().getY();
-        int mz = schematic.getOffsetMinCorner().getZ();
-        int sx = (schematic.getX() - 1) * -1;
-        int sz = (schematic.getZ() - 1) * -1;
-
-        Vec3i correctedOrigin;
-        Mirror mirror = LitematicaHelper.getMirror(i);
-        Rotation rotation = LitematicaHelper.getRotation(i);
-
-        //todo there has to be a better way to do this but i cant finde it atm
-        switch (mirror) {
-            case FRONT_BACK:
-            case LEFT_RIGHT:
-                switch ((mirror.ordinal() * 2 + rotation.ordinal()) % 4) {
-                    case 1:
-                        correctedOrigin = new Vec3i(x + (sz - mz), y + my, z + (sx - mx));
-                        break;
-                    case 2:
-                        correctedOrigin = new Vec3i(x + mx, y + my, z + (sz - mz));
-                        break;
-                    case 3:
-                        correctedOrigin = new Vec3i(x + mz, y + my, z + mx);
-                        break;
-                    default:
-                        correctedOrigin = new Vec3i(x + (sx - mx), y + my, z + mz);
-                        break;
-                }
-                break;
+    private static Vec3i transform(Vec3i in, Mirror mirror, Rotation rotation) {
+        int x = in.getX();
+        int z = in.getZ();
+        if (mirror == Mirror.LEFT_RIGHT) {
+            z = -z;
+        } else if (mirror == Mirror.FRONT_BACK) {
+            x = -x;
+        }
+        switch (rotation) {
+            case CLOCKWISE_90:
+                return new Vec3i(-z, in.getY(), x);
+            case CLOCKWISE_180:
+                return new Vec3i(-x, in.getY(), -z);
+            case COUNTERCLOCKWISE_90:
+                return new Vec3i(z, in.getY(), -x);
             default:
-                switch (rotation) {
-                    case CLOCKWISE_90:
-                        correctedOrigin = new Vec3i(x + (sz - mz), y + my, z + mx);
-                        break;
-                    case CLOCKWISE_180:
-                        correctedOrigin = new Vec3i(x + (sx - mx), y + my, z + (sz - mz));
-                        break;
-                    case COUNTERCLOCKWISE_90:
-                        correctedOrigin = new Vec3i(x + mz, y + my, z + (sx - mx));
-                        break;
-                    default:
-                        correctedOrigin = new Vec3i(x + mx, y + my, z + mz);
-                        break;
-                }
+                return new Vec3i(x, in.getY(), z);
         }
-        return correctedOrigin;
     }
 
     /**
-     * @param in     the xyz offsets of the block relative to the schematic minimum corner.
-     * @param sizeX  size of the schematic in the x-axis direction.
-     * @param sizeZ  size of the schematic in the z-axis direction.
-     * @param mirror the mirroring of the schematic placement.
-     * @return the corresponding xyz coordinates after mirroring them according to the given mirroring.
+     * @param i   index of the Schematic in the schematic placement list.
+     * @return    The transformed schematic and the position of its minimum corner
      */
-    public static Vec3i doMirroring(Vec3i in, int sizeX, int sizeZ, Mirror mirror) {
-        int xOut = in.getX();
-        int zOut = in.getZ();
-        if (mirror == Mirror.LEFT_RIGHT) {
-            zOut = sizeZ - in.getZ();
-        } else if (mirror == Mirror.FRONT_BACK) {
-            xOut = sizeX - in.getX();
+    public static Tuple<IStaticSchematic, Vec3i> getSchematic(int i) {
+        SchematicPlacement placement = getPlacement(i);
+        int minX = Integer.MAX_VALUE;
+        int minY = Integer.MAX_VALUE;
+        int minZ = Integer.MAX_VALUE;
+        HashMap<Vec3i, StaticSchematic> subRegions = new HashMap<>();
+        WorldSchematic schematicWorld = SchematicWorldHandler.getSchematicWorld();
+        for (Map.Entry<String, SubRegionPlacement> entry : placement.getEnabledRelativeSubRegionPlacements().entrySet()) {
+            SubRegionPlacement subPlacement = entry.getValue();
+            Vec3i pos = transform(subPlacement.getPos(), placement.getMirror(), placement.getRotation());
+            Vec3i size = placement.getSchematic().getAreaSize(entry.getKey());
+            size = transform(size, placement.getMirror(), placement.getRotation());
+            size = transform(size, subPlacement.getMirror(), subPlacement.getRotation());
+            int mx = Math.min(size.getX() + 1, 0);
+            int my = Math.min(size.getY() + 1, 0);
+            int mz = Math.min(size.getZ() + 1, 0);
+            minX = Math.min(minX, pos.getX() + mx);
+            minY = Math.min(minY, pos.getY() + my);
+            minZ = Math.min(minZ, pos.getZ() + mz);
+            BlockPos origin = placement.getOrigin().offset(pos).offset(mx, my, mz);
+            BlockState[][][] states = new BlockState[Math.abs(size.getX())][Math.abs(size.getZ())][Math.abs(size.getY())];
+            for (int x = 0; x < states.length; x++) {
+                for (int z = 0; z < states[x].length; z++) {
+                    for (int y = 0; y < states[x][z].length; y++) {
+                        states[x][z][y] = schematicWorld.getBlockState(origin.offset(x, y, z));
+                    }
+                }
+            }
+            StaticSchematic schematic = new StaticSchematic(states);
+            subRegions.put(pos.offset(mx, my, mz), schematic);
         }
-        return new Vec3i(xOut, in.getY(), zOut);
+        LitematicaPlacementSchematic composite = new LitematicaPlacementSchematic(placement.getName());
+        for (Map.Entry<Vec3i, StaticSchematic> entry : subRegions.entrySet()) {
+            Vec3i pos = entry.getKey().offset(-minX, -minY, -minZ);
+            composite.put(entry.getValue(), pos.getX(), pos.getY(), pos.getZ());
+        }
+        return new Tuple<>(composite, placement.getOrigin().offset(minX, minY, minZ));
     }
 
-    /**
-     * @param in    the xyz offsets of the block relative to the schematic minimum corner.
-     * @param sizeX size of the schematic in the x-axis direction.
-     * @param sizeZ size of the schematic in the z-axis direction.
-     * @return the corresponding xyz coordinates after rotation them 90° clockwise.
-     */
-    public static Vec3i rotate(Vec3i in, int sizeX, int sizeZ) {
-        return new Vec3i(sizeX - (sizeX - sizeZ) - in.getZ(), in.getY(), in.getX());
-    }
+    private static class LitematicaPlacementSchematic extends CompositeSchematic implements IStaticSchematic {
+        private final String name;
 
-    /**
-     * IDFK this just grew and it somehow works. If you understand how, pls tell me.
-     *
-     * @param schemIn give in the original schematic.
-     * @param i       index of the Schematic in the schematic placement list.
-     * @return get it out rotated and mirrored.
-     */
-    public static LitematicaSchematic blackMagicFuckery(LitematicaSchematic schemIn, int i) {
-        LitematicaSchematic tempSchem = schemIn.getCopy(LitematicaHelper.getRotation(i).ordinal() % 2 == 1);
-        for (int yCounter = 0; yCounter < schemIn.getY(); yCounter++) {
-            for (int zCounter = 0; zCounter < schemIn.getZ(); zCounter++) {
-                for (int xCounter = 0; xCounter < schemIn.getX(); xCounter++) {
-                    Vec3i xyzHolder = new Vec3i(xCounter, yCounter, zCounter);
-                    xyzHolder = LitematicaHelper.doMirroring(xyzHolder, schemIn.getX() - 1, schemIn.getZ() - 1, LitematicaHelper.getMirror(i));
-                    for (int turns = 0; turns < LitematicaHelper.getRotation(i).ordinal(); turns++) {
-                        if ((turns % 2) == 0) {
-                            xyzHolder = LitematicaHelper.rotate(xyzHolder, schemIn.getX() - 1, schemIn.getZ() - 1);
-                        } else {
-                            xyzHolder = LitematicaHelper.rotate(xyzHolder, schemIn.getZ() - 1, schemIn.getX() - 1);
-                        }
-                    }
-                    BlockState state = schemIn.getDirect(xCounter, yCounter, zCounter);
-                    try {
-                        state = state.mirror(LitematicaHelper.getMirror(i)).rotate(LitematicaHelper.getRotation(i));
-                    } catch (NullPointerException e) {
-                        //nothing to worry about it's just a hole in the schematic.
-                    }
-                    tempSchem.setDirect(xyzHolder.getX(), xyzHolder.getY(), xyzHolder.getZ(), state);
-                }
+        public LitematicaPlacementSchematic(String name) {
+            super(0, 0, 0);
+            this.name = name;
+        }
+
+        @Override
+        public BlockState getDirect(int x, int y, int z) {
+            if (inSchematic(x, y, z, null)) {
+                return desiredState(x, y, z, null, Collections.emptyList());
             }
+            return null;
+        }
+
+        @Override
+        public String toString() {
+            return name;
         }
-        return tempSchem;
     }
 }
\ No newline at end of file
diff --git a/src/schematica_api/java/fi/dy/masa/litematica/data/DataManager.java b/src/schematica_api/java/fi/dy/masa/litematica/data/DataManager.java
index 4941e7cf3..3f5c6c98f 100644
--- a/src/schematica_api/java/fi/dy/masa/litematica/data/DataManager.java
+++ b/src/schematica_api/java/fi/dy/masa/litematica/data/DataManager.java
@@ -20,14 +20,8 @@
 import fi.dy.masa.litematica.schematic.placement.SchematicPlacementManager;
 
 public class DataManager {
-    public static final DataManager INSTANCE = new DataManager();
-    private final SchematicPlacementManager schematicPlacementManager = new SchematicPlacementManager();
-
-    private static DataManager getInstance() {
-        return INSTANCE;
-    }
 
     public static SchematicPlacementManager getSchematicPlacementManager() {
-        return getInstance().schematicPlacementManager;
+        throw new LinkageError();
     }
 }
\ No newline at end of file
diff --git a/src/schematica_api/java/fi/dy/masa/litematica/schematic/LitematicaSchematic.java b/src/schematica_api/java/fi/dy/masa/litematica/schematic/LitematicaSchematic.java
new file mode 100644
index 000000000..fc8ef5c6e
--- /dev/null
+++ b/src/schematica_api/java/fi/dy/masa/litematica/schematic/LitematicaSchematic.java
@@ -0,0 +1,27 @@
+/*
+ * This file is part of Baritone.
+ *
+ * Baritone is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Baritone is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Baritone.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package fi.dy.masa.litematica.schematic;
+
+import net.minecraft.core.BlockPos;
+
+public class LitematicaSchematic {
+
+    public BlockPos getAreaSize(String name) {
+        throw new LinkageError();
+    }
+}
\ No newline at end of file
diff --git a/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacement.java b/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacement.java
index a267553a9..77f8b629e 100644
--- a/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacement.java
+++ b/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacement.java
@@ -17,19 +17,35 @@
 
 package fi.dy.masa.litematica.schematic.placement;
 
+import com.google.common.collect.ImmutableMap;
+import fi.dy.masa.litematica.schematic.LitematicaSchematic;
+import net.minecraft.core.BlockPos;
 import net.minecraft.world.level.block.Mirror;
 import net.minecraft.world.level.block.Rotation;
 
-public class SchematicPlacement extends SchematicPlacementUnloaded {
-    private Rotation rotation;
-    private Mirror mirror;
+public class SchematicPlacement {
+
+    public String getName() {
+        throw new LinkageError();
+    }
+
+    public BlockPos getOrigin() {
+        throw new LinkageError();
+    }
 
     public Rotation getRotation() {
-        return this.rotation;
+        throw new LinkageError();
     }
 
     public Mirror getMirror() {
-        return this.mirror;
+        throw new LinkageError();
     }
 
+    public ImmutableMap<String, SubRegionPlacement> getEnabledRelativeSubRegionPlacements() {
+        throw new LinkageError();
+    }
+
+    public LitematicaSchematic getSchematic() {
+        throw new LinkageError();
+    }
 }
\ No newline at end of file
diff --git a/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacementManager.java b/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacementManager.java
index ab60e27fa..e7f810a5e 100644
--- a/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacementManager.java
+++ b/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacementManager.java
@@ -17,15 +17,13 @@
 
 package fi.dy.masa.litematica.schematic.placement;
 
-import java.util.ArrayList;
 import java.util.List;
 
 public class SchematicPlacementManager {
-    private final List<SchematicPlacement> schematicPlacements = new ArrayList<>();
 
     //in case of a java.lang.NoSuchMethodError try change the name of this method to getAllSchematicPlacements()
     //there are inconsistencies in the litematica mod about the naming of this method
     public List<SchematicPlacement> getAllSchematicsPlacements() {
-        return schematicPlacements;
+        throw new LinkageError();
     }
 }
\ No newline at end of file
diff --git a/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacementUnloaded.java b/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SubRegionPlacement.java
similarity index 65%
rename from src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacementUnloaded.java
rename to src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SubRegionPlacement.java
index 9e705dc7b..ec28ee95d 100644
--- a/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SchematicPlacementUnloaded.java
+++ b/src/schematica_api/java/fi/dy/masa/litematica/schematic/placement/SubRegionPlacement.java
@@ -18,26 +18,20 @@
 package fi.dy.masa.litematica.schematic.placement;
 
 import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.block.Mirror;
+import net.minecraft.world.level.block.Rotation;
 
-import javax.annotation.Nullable;
-import java.io.File;
+public class SubRegionPlacement {
 
-public class SchematicPlacementUnloaded {
-    protected String name = "?";
-    @Nullable
-    protected File schematicFile;
-    protected BlockPos origin = BlockPos.ZERO;
-
-    public String getName() {
-        return this.name;
+    public BlockPos getPos() {
+        throw new LinkageError();
     }
 
-    @Nullable
-    public File getSchematicFile() {
-        return this.schematicFile;
+    public Rotation getRotation() {
+        throw new LinkageError();
     }
 
-    public BlockPos getOrigin() {
-        return this.origin;
+    public Mirror getMirror() {
+        throw new LinkageError();
     }
 }
\ No newline at end of file
diff --git a/src/schematica_api/java/fi/dy/masa/litematica/world/SchematicWorldHandler.java b/src/schematica_api/java/fi/dy/masa/litematica/world/SchematicWorldHandler.java
new file mode 100644
index 000000000..dfe6e9807
--- /dev/null
+++ b/src/schematica_api/java/fi/dy/masa/litematica/world/SchematicWorldHandler.java
@@ -0,0 +1,25 @@
+/*
+ * This file is part of Baritone.
+ *
+ * Baritone is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Baritone is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Baritone.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package fi.dy.masa.litematica.world;
+
+public class SchematicWorldHandler {
+
+    public static WorldSchematic getSchematicWorld() {
+        throw new LinkageError();
+    }
+}
diff --git a/src/schematica_api/java/fi/dy/masa/litematica/world/WorldSchematic.java b/src/schematica_api/java/fi/dy/masa/litematica/world/WorldSchematic.java
new file mode 100644
index 000000000..58f28b3a6
--- /dev/null
+++ b/src/schematica_api/java/fi/dy/masa/litematica/world/WorldSchematic.java
@@ -0,0 +1,27 @@
+/*
+ * This file is part of Baritone.
+ *
+ * Baritone is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Baritone is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Baritone.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package fi.dy.masa.litematica.world;
+
+import net.minecraft.world.level.Level;
+
+public abstract class WorldSchematic extends Level {
+    private WorldSchematic() {
+        super(null, null, null, null, null, false, false, 0, 0);
+        throw new LinkageError();
+    }
+}
diff --git a/tweaker/build.gradle b/tweaker/build.gradle
index feb9dd039..7bad8747f 100644
--- a/tweaker/build.gradle
+++ b/tweaker/build.gradle
@@ -38,14 +38,14 @@ configurations {
 }
 
 dependencies {
-    implementation "org.spongepowered:mixin:0.8.5"
+    implementation "org.spongepowered:mixin:${project.mixin_version}"
 
     // for some reason mixin isn't including these...
-    implementation "org.ow2.asm:asm:9.3"
-    implementation "org.ow2.asm:asm-tree:9.3"
-    implementation "org.ow2.asm:asm-commons:9.3"
-    implementation "org.ow2.asm:asm-util:9.3"
-    implementation "org.ow2.asm:asm-analysis:9.3"
+    implementation "org.ow2.asm:asm:${project.asm_version}"
+    implementation "org.ow2.asm:asm-tree:${project.asm_version}"
+    implementation "org.ow2.asm:asm-commons:${project.asm_version}"
+    implementation "org.ow2.asm:asm-util:${project.asm_version}"
+    implementation "org.ow2.asm:asm-analysis:${project.asm_version}"
 
     implementation 'com.github.ImpactDevelopment:SimpleTweaker:1.2'
     implementation('net.minecraft:launchwrapper:of-2.3') {


Test patch: