diff --git a/src/api/java/baritone/api/utils/BetterBlockPos.java b/src/api/java/baritone/api/utils/BetterBlockPos.java index 57cdae37f..ae3ea19b7 100644 --- a/src/api/java/baritone/api/utils/BetterBlockPos.java +++ b/src/api/java/baritone/api/utils/BetterBlockPos.java @@ -17,6 +17,7 @@ package baritone.api.utils; +import it.unimi.dsi.fastutil.HashCommon; import net.minecraft.util.EnumFacing; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; @@ -81,42 +82,55 @@ public final class BetterBlockPos extends BlockPos { private static final int NUM_X_BITS = 26; private static final int NUM_Z_BITS = NUM_X_BITS; - private static final int NUM_Y_BITS = 64 - NUM_X_BITS - NUM_Z_BITS; - private static final int Y_SHIFT = 0 + NUM_Z_BITS; - private static final int X_SHIFT = Y_SHIFT + NUM_Y_BITS; - private static final long X_MASK = (1L << NUM_X_BITS) - 1L; + private static final int NUM_Y_BITS = 9; // note: even though Y goes from 0 to 255, that doesn't mean 8 bits will "just work" because the deserializer assumes signed. i could change it for just Y to assume unsigned and leave X and Z as signed, however, we know that in 1.17 they plan to add negative Y. for that reason, the better approach is to give the extra bits to Y and leave it as signed. + // also, if 1.17 sticks with the current plan which is -64 to +320, we could have 9 bits for Y and a constant offset of -64 to change it to -128 to +256. + // that would result in the packed long representation of any valid coordinate still being a positive integer + // i like that property, so i will keep num_y_bits at 9 and plan for an offset in 1.17 + // it also gives 1 bit of wiggle room in case anything else happens in the future, so we are only using 63 out of 64 bits at the moment + private static final int Z_SHIFT = 0; + private static final int Y_SHIFT = Z_SHIFT + NUM_Z_BITS + 1; // 1 padding bit to make twos complement not overflow + private static final int X_SHIFT = Y_SHIFT + NUM_Y_BITS + 1; // and also here too + private static final long X_MASK = (1L << NUM_X_BITS) - 1L; // X doesn't need padding as the overflow carry bit is just discarded, like a normal long (-1) + (1) = 0 private static final long Y_MASK = (1L << NUM_Y_BITS) - 1L; private static final long Z_MASK = (1L << NUM_Z_BITS) - 1L; + public static final long POST_ADDITION_MASK = toLong(-1, -1, -1); public long toLong() { return toLong(this.x, this.y, this.z); } - public static BetterBlockPos fromLong(long serialized) { - int x = (int) (serialized << (64 - X_SHIFT - NUM_X_BITS) >> (64 - NUM_X_BITS)); - int y = (int) (serialized << (64 - Y_SHIFT - NUM_Y_BITS) >> (64 - NUM_Y_BITS)); - int z = (int) (serialized << (64 - NUM_Z_BITS) >> (64 - NUM_Z_BITS)); - return new BetterBlockPos(x, y, z); + return new BetterBlockPos(XfromLong(serialized), YfromLong(serialized), ZfromLong(serialized)); + } + + public static int XfromLong(long serialized) { + return (int) (serialized << (64 - X_SHIFT - NUM_X_BITS) >> (64 - NUM_X_BITS)); + } + + public static int YfromLong(long serialized) { + return (int) (serialized << (64 - Y_SHIFT - NUM_Y_BITS) >> (64 - NUM_Y_BITS)); + } + + public static int ZfromLong(long serialized) { + return (int) (serialized << (64 - Z_SHIFT - NUM_Z_BITS) >> (64 - NUM_Z_BITS)); } public static long toLong(final int x, final int y, final int z) { - return ((long) x & X_MASK) << X_SHIFT | ((long) y & Y_MASK) << Y_SHIFT | ((long) z & Z_MASK); + return ((long) x & X_MASK) << X_SHIFT | ((long) y & Y_MASK) << Y_SHIFT | ((long) z & Z_MASK) << Z_SHIFT; } private static final long MURMUR_MASK = murmur64(BetterBlockPos.class.hashCode()); public static long longHash(int x, int y, int z) { - return murmur64(MURMUR_MASK ^ toLong(x, y, z)); + return longHash(toLong(x, y, z)); + } + + public static long longHash(long packed) { + return murmur64(MURMUR_MASK ^ packed); } public static long murmur64(long h) { - h ^= h >>> 33; - h *= 0xff51afd7ed558ccdL; - h ^= h >>> 33; - h *= 0xc4ceb9fe1a85ec53L; - h ^= h >>> 33; - return h; + return HashCommon.murmurHash3(h); } @Override diff --git a/src/main/java/baritone/Baritone.java b/src/main/java/baritone/Baritone.java index d03953fd9..b9c1fd606 100755 --- a/src/main/java/baritone/Baritone.java +++ b/src/main/java/baritone/Baritone.java @@ -24,6 +24,7 @@ import baritone.api.event.listener.IEventBus; import baritone.api.utils.Helper; import baritone.api.utils.IPlayerContext; import baritone.behavior.*; +import baritone.builder.Main; import baritone.cache.WorldProvider; import baritone.command.manager.CommandManager; import baritone.event.GameEventHandler; @@ -120,6 +121,12 @@ public class Baritone implements IBaritone { this.worldProvider = new WorldProvider(); this.selectionManager = new SelectionManager(this); this.commandManager = new CommandManager(this); + + try { + Main.main(); + } catch (InterruptedException e) { + e.printStackTrace(); + } } @Override diff --git a/src/main/java/baritone/builder/BlockStateCachedData.java b/src/main/java/baritone/builder/BlockStateCachedData.java new file mode 100644 index 000000000..aaa6c3597 --- /dev/null +++ b/src/main/java/baritone/builder/BlockStateCachedData.java @@ -0,0 +1,102 @@ +/* + * 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 . + */ + +package baritone.builder; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockAir; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Information about an IBlockState + *

+ * There will be exactly one of these per valid IBlockState in the game + */ +public final class BlockStateCachedData { + + public boolean canWalkOn; + public boolean isAir; + + public boolean canPlaceAgainstAtAll; + + private final List options; + + + public BlockStateCachedData(IBlockState state) { + isAir = state.getBlock() instanceof BlockAir; + canPlaceAgainstAtAll = state.getBlock() == Blocks.COBBLESTONE || state.getBlock() == Blocks.DIRT; + canWalkOn = canPlaceAgainstAtAll; + this.options = Collections.unmodifiableList(calcOptions()); + } + + private List calcOptions() { + if (canPlaceAgainstAtAll) { + List ret = new ArrayList<>(); + for (Face face : Face.VALUES) { + if (Main.STRICT_Y && face == Face.UP) { + continue; + } + ret.add(new BlockStatePlacementOption(face)); + } + return ret; + } + return Collections.emptyList(); + } + + + public boolean canBeDoneAgainstMe(BlockStatePlacementOption placement) { + // if i am a bottom slab, and the placement option is Face.DOWN, it's impossible + // if i am a top slab, and the placement option is Face.UP, it's impossible + // if i am a bottom slab, and the placement option is Half.TOP, it's impossible + // if i am a top slab, and the placement option is Half.BOTTOM, it's impossible + + // if i am a stair then in newer versions of minecraft its complicated because voxel shapes and stuff blegh + + if (!canPlaceAgainstAtAll) { + return false; + } + //if (Main.DEBUG) { + return Main.RAND.nextInt(10) < 8; + //} + //throw new UnsupportedOperationException(); + } + + public List placementOptions() { + return options; + } + + private static final BlockStateCachedData[] PER_STATE = new BlockStateCachedData[Block.BLOCK_STATE_IDS.size()]; + public static final BlockStateCachedData SCAFFOLDING; + + static { + Block.BLOCK_STATE_IDS.forEach(state -> PER_STATE[Block.BLOCK_STATE_IDS.get(state)] = new BlockStateCachedData(state)); + SCAFFOLDING = get(Blocks.COBBLESTONE.getDefaultState()); + } + + public static BlockStateCachedData get(int state) { + return PER_STATE[state]; + } + + public static BlockStateCachedData get(IBlockState state) { + return get(Block.BLOCK_STATE_IDS.get(state)); + } +} diff --git a/src/main/java/baritone/builder/BlockStatePlacementOption.java b/src/main/java/baritone/builder/BlockStatePlacementOption.java new file mode 100644 index 000000000..16b18d5d8 --- /dev/null +++ b/src/main/java/baritone/builder/BlockStatePlacementOption.java @@ -0,0 +1,50 @@ +/* + * 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 . + */ + +package baritone.builder; + +/** + * A plane against which this block state can be placed + *

+ * For a normal block, this will be a full face of a block. In that case, this class is no more than an EnumFacing + *

+ * For a block like a slab or a stair, this will contain the information that the placement must be against the top or bottom half of the face + */ +public class BlockStatePlacementOption { + + /** + * e.g. a torch placed down on the ground is placed against the bottom of "the torch bounding box", so this would be DOWN for the torch + */ + public final Face against; + public final Half half; + + public BlockStatePlacementOption(Face against, Half half) { + this.against = against; + this.half = half; + if ((against == Face.DOWN || against == Face.UP) && half != Half.EITHER) { + throw new IllegalArgumentException(); + } + } + + public BlockStatePlacementOption(Face against) { + this(against, Half.EITHER); + } + + enum Half { + TOP, BOTTOM, EITHER + } +} diff --git a/src/main/java/baritone/builder/CuboidBounds.java b/src/main/java/baritone/builder/CuboidBounds.java new file mode 100644 index 000000000..df1741609 --- /dev/null +++ b/src/main/java/baritone/builder/CuboidBounds.java @@ -0,0 +1,138 @@ +/* + * 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 . + */ + +package baritone.builder; + +import baritone.api.utils.BetterBlockPos; + +/** + * Bounding box of a cuboid + *

+ * Basically just a lot of helper util methods lol + */ +public class CuboidBounds { + + public final int sizeX; + public final int sizeY; + public final int sizeZ; + public final int size; + + public CuboidBounds(int sizeX, int sizeY, int sizeZ) { + this.sizeX = sizeX; + this.sizeY = sizeY; + this.sizeZ = sizeZ; + this.size = sizeX * sizeY * sizeZ; + if (Main.DEBUG) { + sanityCheck(); + } + } + + public int toIndex(long pos) { + return toIndex(BetterBlockPos.XfromLong(pos), BetterBlockPos.YfromLong(pos), BetterBlockPos.ZfromLong(pos)); + } + + public int toIndex(int x, int y, int z) { + if (Main.DEBUG && !inRange(x, y, z)) { + throw new IllegalStateException(); + } + return (x * sizeY + y) * sizeZ + z; + } + + public boolean inRange(int x, int y, int z) { + return (x >= 0) & (x < sizeX) & (y >= 0) & (y < sizeY) & (z >= 0) & (z < sizeZ); + } + + public boolean inRangeIndex(int index) { + return (index >= 0) & (index < size); + } + + public boolean inRangePos(long pos) { + return inRange(BetterBlockPos.XfromLong(pos), BetterBlockPos.YfromLong(pos), BetterBlockPos.ZfromLong(pos)); + } + + @FunctionalInterface + public interface CuboidCoordConsumer { + + void consume(int x, int y, int z); + } + + @FunctionalInterface + public interface CuboidIndexConsumer { + + void consume(long index); + } + + @FunctionalInterface + public interface CuboidCoordAndIndexConsumer { + + void consume(int x, int y, int z, long index); + } + + public void forEach(CuboidCoordConsumer consumer) { + for (int x = 0; x < sizeX; x++) { + for (int y = 0; y < sizeY; y++) { + for (int z = 0; z < sizeZ; z++) { + consumer.consume(x, y, z); + } + } + } + } + + public void forEach(CuboidIndexConsumer consumer) { + for (int x = 0; x < sizeX; x++) { + for (int y = 0; y < sizeY; y++) { + for (int z = 0; z < sizeZ; z++) { + consumer.consume(BetterBlockPos.toLong(x, y, z)); + } + } + } + } + + public void forEach(CuboidCoordAndIndexConsumer consumer) { + for (int x = 0; x < sizeX; x++) { + for (int y = 0; y < sizeY; y++) { + for (int z = 0; z < sizeZ; z++) { + consumer.consume(x, y, z, BetterBlockPos.toLong(x, y, z)); + } + } + } + } + + private void sanityCheck() { + if (sizeY > 256) { + throw new IllegalStateException(); + } + long chk = ((long) sizeX) * ((long) sizeY) * ((long) sizeZ); + if (chk != (long) size) { + throw new IllegalStateException(); + } + int index = 0; + for (int x = 0; x < sizeX; x++) { + for (int y = 0; y < sizeY; y++) { + for (int z = 0; z < sizeZ; z++) { + if (toIndex(x, y, z) != index) { + throw new IllegalStateException(); + } + index++; + } + } + } + if (index != size) { + throw new IllegalStateException(); + } + } +} diff --git a/src/main/java/baritone/builder/DependencyGraphAnalyzer.java b/src/main/java/baritone/builder/DependencyGraphAnalyzer.java new file mode 100644 index 000000000..984b2b056 --- /dev/null +++ b/src/main/java/baritone/builder/DependencyGraphAnalyzer.java @@ -0,0 +1,79 @@ +/* + * 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 . + */ + +package baritone.builder; + +import baritone.api.utils.BetterBlockPos; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + +/** + * Some initial checks on the schematic + *

+ * The intent is to provide reasonable error messages, which we can do by catching common cases as early as possible + *

+ * So that it's an actual comprehensible error **that tells you where the problem is** instead of just "pathing failed" + */ +public class DependencyGraphAnalyzer { + + public static void prevalidate(PlaceOrderDependencyGraph graph) { + graph.bounds().forEach(pos -> { + if (graph.airTreatedAsScaffolding(pos)) { + // completely fine to, for example, have an air pocket with non-place-against-able stuff all around it + return; + } + for (Face face : Face.VALUES) { + if (graph.incomingEdge(pos, face)) { + return; + } + } + throw new IllegalStateException(BetterBlockPos.fromLong(pos) + " is unplaceable from any side"); + }); + } + + public static void prevalidateExternalToInteriorSearch(PlaceOrderDependencyGraph graph) { + LongOpenHashSet reachable = new LongOpenHashSet(); + LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); + graph.bounds().forEach(pos -> { + for (Face face : Face.VALUES) { + if (graph.incomingEdgePermitExterior(pos, face) && !graph.incomingEdge(pos, face)) { + // this block is placeable from the exterior of the schematic! + queue.enqueue(pos); // this will intentionally put the top of the schematic at the front + } + } + }); + while (!queue.isEmpty()) { + long pos = queue.dequeueLong(); + if (reachable.add(pos)) { + for (Face face : Face.VALUES) { + if (graph.outgoingEdge(pos, face)) { + queue.enqueueFirst(face.offset(pos)); + } + } + } + } + graph.bounds().forEach(pos -> { + if (graph.airTreatedAsScaffolding(pos)) { + // same as previous validation + return; + } + if (!reachable.contains(pos)) { + throw new IllegalStateException(BetterBlockPos.fromLong(pos) + " is placeable, in theory, but in practice there is no valid path from the exterior to it"); + } + }); + } +} diff --git a/src/main/java/baritone/builder/DependencyGraphScaffoldingOverlay.java b/src/main/java/baritone/builder/DependencyGraphScaffoldingOverlay.java new file mode 100644 index 000000000..672112cb6 --- /dev/null +++ b/src/main/java/baritone/builder/DependencyGraphScaffoldingOverlay.java @@ -0,0 +1,428 @@ +/* + * 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 . + */ + +package baritone.builder; + +import baritone.api.utils.BetterBlockPos; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + +import java.util.*; + +/** + * A mutable addition of scaffolding blocks to a schematic + *

+ * Contains a set of coordinates that are "air" in the schematic, but we are going to put scaffolding throwaway blocks there + *

+ * Maintains and incrementally updates a collapsed dependency graph, which is the block placement dependency graph reduced to directed acyclic graph form by way of collapsing all strongly connected components into single nodes + */ +public class DependencyGraphScaffoldingOverlay { + + private final PlaceOrderDependencyGraph delegate; + private final LongOpenHashSet scaffoldingAdded; + private final CollapsedDependencyGraph collapsedGraph; + + public DependencyGraphScaffoldingOverlay(PlaceOrderDependencyGraph delegate) { + this.delegate = delegate; + this.scaffoldingAdded = new LongOpenHashSet(); + this.collapsedGraph = new CollapsedDependencyGraph(TarjansAlgorithm.run(this)); + //System.out.println("Num components: " + collapsedGraph.components.size()); + } + + public boolean outgoingEdge(long pos, Face face) { + if (overrideOff(pos)) { + return false; + } + if (overrideOff(face.offset(pos))) { + return false; + } + return delegate.outgoingEdge(pos, face); + } + + public boolean incomingEdge(long pos, Face face) { + if (overrideOff(pos)) { + return false; + } + if (overrideOff(face.offset(pos))) { + return false; + } + return delegate.incomingEdge(pos, face); + } + + public CuboidBounds bounds() { + return delegate.bounds(); + } + + private boolean overrideOff(long pos) { + return bounds().inRangePos(pos) && air(pos); + } + + public boolean real(long pos) { + return !air(pos); + } + + public void forEachReal(CuboidBounds.CuboidIndexConsumer consumer) { + bounds().forEach(pos -> { + if (real(pos)) { + consumer.consume(pos); + } + }); + } + + public boolean air(long pos) { + return delegate.airTreatedAsScaffolding(pos) && !scaffoldingAdded.contains(pos); + } + + public void enable(long pos) { + if (!delegate.airTreatedAsScaffolding(pos)) { + throw new IllegalArgumentException(); + } + if (!scaffoldingAdded.add(pos)) { + throw new IllegalArgumentException(); + } + collapsedGraph.incrementalUpdate(pos); + if (Main.DEBUG) { + //System.out.println(collapsedGraph.posToComponent.containsKey(pos) + " " + scaffoldingAdded.contains(pos) + " " + real(pos)); + checkEquality(collapsedGraph, new CollapsedDependencyGraph(TarjansAlgorithm.run(this))); + //System.out.println("Checked equality"); + //System.out.println("Num connected components: " + collapsedGraph.components.size()); + } + } + + public class CollapsedDependencyGraph { + + private final List components; + private final Long2ObjectOpenHashMap posToComponent; + + private CollapsedDependencyGraph(TarjansAlgorithm.TarjansResult partition) { + components = new ArrayList<>(partition.numComponents()); + for (int i = 0; i < partition.numComponents(); i++) { + addComponent(); + } + posToComponent = new Long2ObjectOpenHashMap<>(); + forEachReal(pos -> { + CollapsedDependencyGraphComponent component = components.get(partition.getComponent(pos)); + component.positions.add(pos); + posToComponent.put(pos, component); + }); + forEachReal(pos -> { + for (Face face : Face.VALUES) { + if (outgoingEdge(pos, face)) { + CollapsedDependencyGraphComponent src = posToComponent.get(pos); + CollapsedDependencyGraphComponent dst = posToComponent.get(face.offset(pos)); + if (src != dst) { + src.outgoingEdges.add(dst); + dst.incomingEdges.add(src); + } + } + } + }); + if (Main.DEBUG) { + sanityCheck(); + /*long pos = 976433971301L; + if (!real(pos)) { + return; + } + System.out.println("Tarjan outcome"); + System.out.println("Pos core " + posToComponent.get(pos).index); + for (Face face : Face.VALUES) { + if (outgoingEdge(pos, face)) { + System.out.println("Pos outgoing edge " + face + " goes to " + posToComponent.get(face.offset(pos)).index); + } + if (incomingEdge(pos, face)) { + System.out.println("Pos incoming edge " + face + " comes from " + posToComponent.get(face.offset(pos)).index); + } + }*/ + } + } + + private CollapsedDependencyGraphComponent addComponent() { + CollapsedDependencyGraphComponent component = new CollapsedDependencyGraphComponent(components.size()); + components.add(component); + return component; + } + + private CollapsedDependencyGraphComponent mergeInto(CollapsedDependencyGraphComponent child, CollapsedDependencyGraphComponent parent) { + if (child.deleted() || parent.deleted()) { + throw new IllegalStateException(); + } + if (child == parent) { + throw new IllegalStateException(); + } + if (child.positions.size() > parent.positions.size()) { + return mergeInto(parent, child); + } + if (Main.DEBUG) { + //System.out.println("Merging " + child.index + " into " + parent.index); + } + child.incomingEdges.forEach(intoChild -> { + intoChild.outgoingEdges.remove(child); + if (intoChild == parent) { + return; + } + intoChild.outgoingEdges.add(parent); + parent.incomingEdges.add(intoChild); + }); + child.outgoingEdges.forEach(outOfChild -> { + outOfChild.incomingEdges.remove(child); + if (outOfChild == parent) { + return; + } + outOfChild.incomingEdges.add(parent); + parent.outgoingEdges.add(outOfChild); + }); + parent.positions.addAll(child.positions); + LongIterator it = child.positions.iterator(); + while (it.hasNext()) { + long pos = it.nextLong(); + posToComponent.put(pos, parent); + } + for (int i = child.index + 1; i < components.size(); i++) { + /*if (Main.DEBUG && components.get(i).index != i) { + throw new IllegalStateException("catch this bug a little bit earlier than otherwise..."); + }*/ + components.get(i).index--; + } + components.remove(child.index); + child.index = -1; + //System.out.println("Debug child contains: " + child.positions.contains(963549069314L) + " " + parent.positions.contains(963549069314L)); + return parent; + } + + private void incrementalEdgeAddition(long src, long dst) { + CollapsedDependencyGraphComponent srcComponent = posToComponent.get(src); + CollapsedDependencyGraphComponent dstComponent = posToComponent.get(dst); + if (srcComponent == dstComponent) { // already strongly connected + return; + } + if (srcComponent.outgoingEdges.contains(dstComponent)) { // we already know about this edge + return; + } + List> paths = pathExists(dstComponent, srcComponent); + if (!paths.isEmpty()) { + CollapsedDependencyGraphComponent survivor = srcComponent; + for (List path : paths) { + if (path.get(0) != srcComponent || path.get(path.size() - 1) != dstComponent) { + throw new IllegalStateException(); + } + for (int i = 1; i < path.size(); i++) { + if (path.get(i).deleted() || path.get(i) == survivor) { // two different paths to the same goal, only merge the components once, so skip is already survivor or deleted + continue; + } + survivor = mergeInto(survivor, path.get(i)); + } + } + // can't run sanityCheck after each mergeInto because it could leave a 2-way connection between components as an intermediary state while collapsing + if (Main.DEBUG) { + sanityCheck(); + } + return; + } + srcComponent.outgoingEdges.add(dstComponent); + dstComponent.incomingEdges.add(srcComponent); + } + + private List> pathExists(CollapsedDependencyGraphComponent src, CollapsedDependencyGraphComponent dst) { + if (src == dst) { + return new ArrayList<>(Collections.singletonList(new ArrayList<>(Collections.singletonList(src)))); + } + if (dst.incomingEdges.isEmpty()) { + return Collections.emptyList(); // impossible + } + if (Main.STRICT_Y && src.y() > dst.y()) { + return Collections.emptyList(); // no downward edges in strict_y mode + } + List> ret = new ArrayList<>(); + for (CollapsedDependencyGraphComponent nxt : src.outgoingEdges) { + List> paths = pathExists(nxt, dst); + for (List path : paths) { + path.add(src); + ret.add(path); + } + } + return ret; + } + + private void incrementalUpdate(long pos) { + if (posToComponent.containsKey(pos)) { + throw new IllegalStateException(); + } + CollapsedDependencyGraphComponent component = addComponent(); + component.positions.add(pos); + posToComponent.put(pos, component); + if (Main.DEBUG) { + sanityCheck(); + } + //System.out.println("Incremental " + pos); + //System.out.println("Pos core " + posToComponent.get(pos).index); + for (Face face : Face.VALUES) { + if (outgoingEdge(pos, face)) { + //System.out.println("Pos outgoing edge " + face + " goes to " + posToComponent.get(face.offset(pos)).index); + incrementalEdgeAddition(pos, face.offset(pos)); + } + if (incomingEdge(pos, face)) { + //System.out.println("Pos incoming edge " + face + " comes from " + posToComponent.get(face.offset(pos)).index); + incrementalEdgeAddition(face.offset(pos), pos); + } + } + if (Main.DEBUG) { + sanityCheck(); + } + } + + public class CollapsedDependencyGraphComponent { + + private int index; + private final int hash; + private final LongOpenHashSet positions = new LongOpenHashSet(); + private final Set outgoingEdges = new HashSet<>(); + private final Set incomingEdges = new HashSet<>(); + private int y = -1; + + private CollapsedDependencyGraphComponent(int index) { + this.index = index; + this.hash = System.identityHashCode(this); + } + + @Override + public int hashCode() { + return hash; // no need to enter native code to get a hashCode, that saves a few nanoseconds + } + + private int y() { + if (!Main.STRICT_Y || positions.isEmpty()) { + throw new IllegalStateException(); + } + if (y == -1) { + y = BetterBlockPos.YfromLong(positions.iterator().nextLong()); + if (y == -1) { + throw new IllegalStateException(); + } + } + return y; + } + + public boolean deleted() { + return index < 0; + } + } + + private void sanityCheck() { + LongOpenHashSet inComponents = new LongOpenHashSet(); + int index = 0; + for (CollapsedDependencyGraphComponent component : components) { + if (component.index != index++) { + throw new IllegalStateException(); + } + if (component.incomingEdges.contains(component) || component.outgoingEdges.contains(component)) { + throw new IllegalStateException(component.index + ""); + } + if (component.positions.isEmpty()) { + throw new IllegalStateException(); + } + int y = Main.STRICT_Y ? component.y() : -1; + for (CollapsedDependencyGraphComponent out : component.outgoingEdges) { + if (Main.STRICT_Y && out.y() < y) { + throw new IllegalStateException(); + } + if (!out.incomingEdges.contains(component)) { + throw new IllegalStateException(); + } + if (component.incomingEdges.contains(out)) { + throw new IllegalStateException(out.index + " is both an incoming AND and outgoing of " + component.index); + } + } + for (CollapsedDependencyGraphComponent in : component.incomingEdges) { + if (Main.STRICT_Y && in.y() > y) { + throw new IllegalStateException(); + } + if (!in.outgoingEdges.contains(component)) { + throw new IllegalStateException(in.index + " is an incoming edge of " + component.index + " but it doesn't have that as an outgoing edge"); + } + if (component.outgoingEdges.contains(in)) { + throw new IllegalStateException(); + } + } + LongIterator it = component.positions.iterator(); + while (it.hasNext()) { + long l = it.nextLong(); + if (posToComponent.get(l) != component) { + throw new IllegalStateException(); + } + if (Main.STRICT_Y && BetterBlockPos.YfromLong(l) != y) { + throw new IllegalStateException(); + } + if (!real(l)) { + throw new IllegalStateException(); + } + } + inComponents.addAll(component.positions); + } + if (!inComponents.equals(posToComponent.keySet())) { + for (long l : posToComponent.keySet()) { + if (!inComponents.contains(l)) { + System.out.println(l); + System.out.println(posToComponent.get(l).index); + System.out.println(posToComponent.get(l).positions.contains(l)); + System.out.println(posToComponent.get(l).deleted()); + System.out.println(components.contains(posToComponent.get(l))); + throw new IllegalStateException(l + " is in posToComponent but not actually in any component"); + } + } + throw new IllegalStateException("impossible"); + } + } + } + + private void checkEquality(CollapsedDependencyGraph a, CollapsedDependencyGraph b) { + if (a.components.size() != b.components.size()) { + throw new IllegalStateException(a.components.size() + " " + b.components.size()); + } + if (a.posToComponent.size() != b.posToComponent.size()) { + throw new IllegalStateException(); + } + if (!a.posToComponent.keySet().equals(b.posToComponent.keySet())) { + throw new IllegalStateException(); + } + a.sanityCheck(); + b.sanityCheck(); + int[] aToB = new int[a.components.size()]; + Arrays.setAll(aToB, i -> b.posToComponent.get(a.components.get(i).positions.iterator().nextLong()).index); + for (int i = 0; i < aToB.length; i++) { + int bInd = aToB[i]; + if (!a.components.get(i).positions.equals(b.components.get(bInd).positions)) { + throw new IllegalStateException(); + } + for (List> toCompare : Arrays.asList( + Arrays.asList(a.components.get(i).incomingEdges, b.components.get(bInd).incomingEdges), + Arrays.asList(a.components.get(i).outgoingEdges, b.components.get(bInd).outgoingEdges) + )) { + Set aEdges = toCompare.get(0); + Set bEdges = toCompare.get(1); + if (aEdges.size() != bEdges.size()) { + throw new IllegalStateException(); + } + for (CollapsedDependencyGraph.CollapsedDependencyGraphComponent dst : aEdges) { + if (!bEdges.contains(b.components.get(aToB[dst.index]))) { + throw new IllegalStateException(); + } + } + } + } + } +} diff --git a/src/main/java/baritone/builder/Face.java b/src/main/java/baritone/builder/Face.java new file mode 100644 index 000000000..91bfab6c4 --- /dev/null +++ b/src/main/java/baritone/builder/Face.java @@ -0,0 +1,59 @@ +/* + * 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 . + */ + +package baritone.builder; + +import baritone.api.utils.BetterBlockPos; +import net.minecraft.util.EnumFacing; + +/** + * I hate porting things to new versions of Minecraft + *

+ * So just like BetterBlockPos, we now have Face + */ +public enum Face { + DOWN, UP, NORTH, SOUTH, WEST, EAST; + public final int index = ordinal(); + public final int oppositeIndex = toMC().getOpposite().getIndex(); + public final int x = toMC().getXOffset(); + public final int y = toMC().getYOffset(); + public final int z = toMC().getZOffset(); + public final long offset = BetterBlockPos.toLong(x, y, z); + public static final Face[] VALUES = new Face[6]; + + static { + for (Face face : values()) { + VALUES[face.index] = face; + } + } + + public final EnumFacing toMC() { + return EnumFacing.byIndex(index); + } + + public static Face fromMC(EnumFacing facing) { + return VALUES[facing.getIndex()]; + } + + public final Face opposite() { + return VALUES[oppositeIndex]; + } + + public final long offset(long pos) { + return (pos + offset) & BetterBlockPos.POST_ADDITION_MASK; + } +} diff --git a/src/main/java/baritone/builder/Main.java b/src/main/java/baritone/builder/Main.java new file mode 100644 index 000000000..4b7b4097b --- /dev/null +++ b/src/main/java/baritone/builder/Main.java @@ -0,0 +1,131 @@ +/* + * 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 . + */ + +package baritone.builder; + +import baritone.api.utils.BetterBlockPos; +import net.minecraft.block.Block; +import net.minecraft.init.Blocks; + +import java.util.Random; + +public class Main { + + public static final boolean DEBUG = false; + + /** + * If true, many different parts of the builder switch to a more efficient mode where blocks can only be placed adjacent or upwards, never downwards + */ + public static final boolean STRICT_Y = false; + + public static final Random RAND = new Random(5021); + + public static void main() throws InterruptedException { + for (Face face : Face.VALUES) { + System.out.println(face); + System.out.println(face.x); + System.out.println(face.y); + System.out.println(face.z); + System.out.println(face.index); + System.out.println(face.offset); + System.out.println(face.oppositeIndex); + } + { + System.out.println("Without"); + long start = BetterBlockPos.toLong(5021, 69, 420); + System.out.println(BetterBlockPos.fromLong(start)); + start += Face.UP.offset; + System.out.println(BetterBlockPos.fromLong(start)); + start += Face.DOWN.offset; + System.out.println(BetterBlockPos.fromLong(start)); + start += Face.UP.offset; + System.out.println(BetterBlockPos.fromLong(start)); + start += Face.DOWN.offset; + System.out.println(BetterBlockPos.fromLong(start)); + } + { + System.out.println("With"); + long start = BetterBlockPos.toLong(5021, 69, 420); + System.out.println(BetterBlockPos.fromLong(start)); + start += Face.UP.offset; + start &= BetterBlockPos.POST_ADDITION_MASK; + System.out.println(BetterBlockPos.fromLong(start)); + start += Face.DOWN.offset; + start &= BetterBlockPos.POST_ADDITION_MASK; + System.out.println(BetterBlockPos.fromLong(start)); + start += Face.UP.offset; + start &= BetterBlockPos.POST_ADDITION_MASK; + System.out.println(BetterBlockPos.fromLong(start)); + start += Face.DOWN.offset; + start &= BetterBlockPos.POST_ADDITION_MASK; + System.out.println(BetterBlockPos.fromLong(start)); + } + { + System.out.println(BetterBlockPos.fromLong(BetterBlockPos.toLong(150, 150, 150))); + } + { + int[][][] test = new int[20][20][20]; + int based = Block.BLOCK_STATE_IDS.get(Blocks.DIRT.getDefaultState()); + int numRealBlocks = 0; + for (int x = 0; x < test.length; x++) { + for (int y = 0; y < test[0].length; y++) { + for (int z = 0; z < test[0][0].length; z++) { + //if ((x + y + z) % 2 == 0) { + if (RAND.nextBoolean()) { + test[x][y][z] = based; + numRealBlocks++; + } + } + + } + } + /*for (int[][] arr : test) { + for (int[] arr2 : arr) { + Arrays.fill(arr2, based); + } + }*/ + PackedBlockStateCuboid states = new PackedBlockStateCuboid(test); + PlaceOrderDependencyGraph graph = new PlaceOrderDependencyGraph(states); + /*DependencyGraphAnalyzer.prevalidate(graph); + for (int i = 0; i < 1; i++) { + long a = System.currentTimeMillis(); + DependencyGraphAnalyzer.prevalidateExternalToInteriorSearch(graph); + long b = System.currentTimeMillis(); + System.out.println((b - a) + "ms"); + Thread.sleep(500); + System.gc(); + Thread.sleep(500); + }*/ + DependencyGraphScaffoldingOverlay scaffolding = new DependencyGraphScaffoldingOverlay(graph); + long a = System.currentTimeMillis(); + while (numRealBlocks < scaffolding.bounds().size) { + int x = RAND.nextInt(scaffolding.bounds().sizeX); + int y = RAND.nextInt(scaffolding.bounds().sizeY); + int z = RAND.nextInt(scaffolding.bounds().sizeZ); + long pos = BetterBlockPos.toLong(x, y, z); + if (scaffolding.air(pos)) { + //System.out.println("Setting to scaffolding " + BetterBlockPos.fromLong(pos) + " " + pos); + scaffolding.enable(pos); + numRealBlocks++; + } + } + System.out.println("Done " + (System.currentTimeMillis() - a) + "ms"); + //scaffolding.enable(0); + } + System.exit(0); + } +} diff --git a/src/main/java/baritone/builder/PackedBlockStateCuboid.java b/src/main/java/baritone/builder/PackedBlockStateCuboid.java new file mode 100644 index 000000000..d1c3610bd --- /dev/null +++ b/src/main/java/baritone/builder/PackedBlockStateCuboid.java @@ -0,0 +1,34 @@ +/* + * 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 . + */ + +package baritone.builder; + +public class PackedBlockStateCuboid { + + public final CuboidBounds bounds; + private final int[] states; + + public PackedBlockStateCuboid(int[][][] blockStates) { + this.bounds = new CuboidBounds(blockStates.length, blockStates[0].length, blockStates[0][0].length); + this.states = new int[bounds.size]; + bounds.forEach((x, y, z) -> states[bounds.toIndex(x, y, z)] = blockStates[x][y][z]); + } + + public int get(int index) { + return states[index]; + } +} diff --git a/src/main/java/baritone/builder/PlaceOrderDependencyGraph.java b/src/main/java/baritone/builder/PlaceOrderDependencyGraph.java new file mode 100644 index 000000000..c48be382f --- /dev/null +++ b/src/main/java/baritone/builder/PlaceOrderDependencyGraph.java @@ -0,0 +1,130 @@ +/* + * 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 . + */ + +package baritone.builder; + +import java.util.BitSet; + +/** + * An immutable graph representing block placement dependency order + *

+ * Air blocks are treated as scaffolding! + *

+ * Edge A --> B means that B can be placed against A + */ +public class PlaceOrderDependencyGraph { + + private final PackedBlockStateCuboid states; + private final BitSet edges; + + public PlaceOrderDependencyGraph(PackedBlockStateCuboid states) { + this.states = states; + + this.edges = new BitSet(); + if (Main.DEBUG) { + sanityCheck(); + } + bounds().forEach(this::compute); + } + + private void compute(long pos) { + for (BlockStatePlacementOption option : data(pos).placementOptions()) { + if (Main.STRICT_Y && option.against == Face.UP) { + throw new IllegalStateException(); + } + long againstPos = option.against.offset(pos); + BlockStateCachedData against; + if (inRange(againstPos)) { + against = data(againstPos); + } else { + against = BlockStateCachedData.SCAFFOLDING; + } + if (against.canBeDoneAgainstMe(option)) { + edges.set(bitIndex(pos, option.against)); + } + } + } + + private BlockStateCachedData data(long pos) { + int state = state(pos); + if (treatAsScaffolding(state)) { + return BlockStateCachedData.SCAFFOLDING; + } + return BlockStateCachedData.get(state); + } + + // example: dirt at 0,0,0 torch at 0,1,0. outgoingEdge(0,0,0,UP) returns true, incomingEdge(0,1,0,DOWN) returns true + + public boolean outgoingEdge(long pos, Face face) { + if (!inRange(pos)) { + return false; + } + return incomingEdge(face.offset(pos), face.opposite()); + } + + public boolean incomingEdge(long pos, Face face) { + if (!inRange(face.offset(pos))) { + return false; + } + return incomingEdgePermitExterior(pos, face); + } + + public boolean incomingEdgePermitExterior(long pos, Face face) { + if (!inRange(pos)) { + return false; + } + return edges.get(bitIndex(pos, face)); + } + + private int bitIndex(long pos, Face face) { + return bounds().toIndex(pos) * 6 + face.index; + } + + public boolean airTreatedAsScaffolding(long pos) { + return treatAsScaffolding(state(pos)); + } + + private int state(long pos) { + return states.get(bounds().toIndex(pos)); + } + + private boolean inRange(long pos) { + return bounds().inRangePos(pos); + } + + public CuboidBounds bounds() { + return states.bounds; + } + + private boolean treatAsScaffolding(int state) { + return BlockStateCachedData.get(state).isAir; + } + + private void sanityCheck() { + int[] test = new int[bounds().sizeX * bounds().sizeY * bounds().sizeZ * 6]; + bounds().forEach(pos -> { + for (int face = 0; face < 6; face++) { + test[bitIndex(pos, Face.VALUES[face])]++; + } + }); + for (int i = 0; i < test.length; i++) { + if (test[i] != 1) { + throw new IllegalStateException(); + } + } + } +} diff --git a/src/main/java/baritone/builder/README.md b/src/main/java/baritone/builder/README.md new file mode 100644 index 000000000..8cf69d389 --- /dev/null +++ b/src/main/java/baritone/builder/README.md @@ -0,0 +1,24 @@ +This is where I am working on a rewrite from-scratch of the Baritone schematic builder. + +I will write more documentation eventually, I have some planning documents that I may eventually paste into here and such, I just don't want to commit to writing about something when it's still up for change :) + +The current Baritone BuilderProcess has a number of issues, no one disagrees there. The question is whether to try and fix those issues, or to start from scratch. I believe that starting from scratch is a good idea in this case. The reason is that I want to change the fundamental approach to building schematics. The current approach is basically "do whatever places / breaks I can from wherever I happen to be at the moment" and also "and if I can't work on anything from here, pathfind to a coordinate where I probably will be able to". This suffers from an inherently unfixable problem, which is that **there is no plan**. There are an untold number of infinite loops it can get stuck in that I have just hack patched over. Did you know that whenever the builder places a block, it increments a counter so that for the next five ticks (0.25 seconds) it won't consider breaking any blocks? This was my fix for it just placing and breaking the same block over and over. Did you know that the builder only places blocks at or below foot level purely because otherwise it would stupidly box itself in, because it isn't thinking ahead to "if I cover myself in blocks, I won't be able to walk to the next thing I need to do". And this doesn't even get into the larger scale infinite loops. I patched over the short ones, where it gets itself stuck within seconds, but the long term ones are a different beast. For example, the pathfinder that takes it to the next task can place / break blocks to get there. It has strict penalties for undoing work in the schematic, of course, but sometimes that's the only way forward. And in that scenario, it can cause infinite loops where each cycle lasts 30+ seconds ("nothing to do here, let's pathfind to the other side of the schematic" -> break a block in order to get there -> arrive at the other end -> "whoa i need to go place that block" -> walk back to where it started and place that block -> repeat the cycle). + +So the idea, as you might have guessed from that foreshadowing, is that the new builder will generate a **complete** plan that includes interspersed actions of placing blocks and moving the player. This plan will be coherent and will not suffer from the issues of the current pathfinder in which costs are unable to take into account the previous actions that have changed the world. I'll say more on that later. This does mean that the schematic builder will not use the same path planner as the rest of Baritone. It may use the same **executor** though: why not. Nothing wrong with `new MovementTraverse(src, dest).update()`. + +Current restrictions that the builder will have, at least initially: +* No breaking blocks. If you want to `cleararea` you can just use the old one, it works fine for that specific thing, at least. +* For that reason, the schematic area should be clear. Think of this like a 3d printer going from bottom to top. It takes in a schematic, generates a plan of how to build it, and executes it. +* Because it generates a full plan, if you think about it, this means that your schematic needs to be plannable. Therefore, if your schematic has something unplaceable in it, it won't even begin to do anything. I'll probably add an option to just skip impossible blocks with a warning. But plan for certain things (e.g. lava) to not be possible. + +I want to make this package less dependent on Minecraft code. Perhaps this will be fully strict (put all Minecraft imports into a subpackage), perhaps it will just be best-effort. The idea is that I hate having to change all the stupid mapping names and deal with merge conflicts on those. For that reason I plan to use `int` instead of `IBlockState` everywhere, and basically do like `cachedData[state].isAir` instead of `state.getBlock() instanceof BlockAir` (so like `cachedData` would be a `BlockStateCachedData[]` with one entry per valid `IBlockState`). Then, for different versions of the game, all that would need to change is the code to populate array. This should help with checks like can-walk-on and can-place-against. + +If all the blocks that existed in Minecraft were solid, can-walk-on, can-place-against, this would be trivially easy. (I know - even for schematics of blocks like that, the current builder has problems.) Even for blocks such as glazed terracotta that have orientation, it would be okay. + +The problem arises with all these blocks that people love to use in builds that are really frustrating to work around. I'm referring to slabs, stairs, fences, walls, etc. + +The current pathfinder makes a naive assumption that every movement's starting point is sane. In other words, it assumes that the previous movement will have gotten the player to the starting point stably. So even if the starting point is currently underground, the previous movement will have broken the two blocks that the player is now assumed to be standing in. If the starting point is floating in midair, the previous movement must have gotten a block placed that the player is currently standing on. This allows for incredibly fast long distance pathing. However, that isn't the goal here. In normal Baritone pathfinding, the goal is to get from point A to point B. Placing and breaking blocks along the way is only done as necessary to accomplish the first goal. Whereas in structure building, it's to place and break blocks, to realize a schematic. The player moving around is only done as necessary to accomplish the first goal. So, clearly, the builder pathfinder needs to be smarter than this. In order to generate these plans, it does need to be answer hypotheticals like "by this point in the schematic, if I stood here, could I reach this face of this block to complete this placement?". It also needs to be able to answer questions like "I'm standing at XYZ. But what am I actually standing on? At this point in the construction, is that a scaffolding throwaway block, or is it the half slab that we eventually intend to be placed here?". Therefore, a standard pathfinder, where a node is uniquely identified by its coordinate, will not be sufficient, as that doesn't convey any information about actions taken to the world up to that point. If we want to use a traditional graph search pathfinder, a node must actually be identified by the XYZ of the player and the set of blocks that have been modified so far. + +I don't want to go too much further into how a traditional graph search algorithm could work here, as I'm not even certain that's what would work best. I'm considering STAN (an offshoot of graphplan) as well. It might be a hybrid solution, who knows. (e.g. try naive DFS, if that can't find a plan after X milliseconds, then fall back to graphplan). + +I'm not sure how to think about performance for this. Obviously there are copes like "pregenerate a plan for a schematic, reuse it". There are also copes like "just throw it in another thread, who cares how long it takes". Because if I dump in a 128x128x128 schematic of all stone (two million blocks), it takes a few seconds to separate the strongly connected components. Is that okay? I think it is. Lol. diff --git a/src/main/java/baritone/builder/TarjansAlgorithm.java b/src/main/java/baritone/builder/TarjansAlgorithm.java new file mode 100644 index 000000000..3a70022a9 --- /dev/null +++ b/src/main/java/baritone/builder/TarjansAlgorithm.java @@ -0,0 +1,181 @@ +/* + * 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 . + */ + +package baritone.builder; + +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; + +import java.util.ArrayDeque; + +/** + * Tarjans algorithm destructured into a coroutine-like layout with an explicit "call stack" on the heap + *

+ * This is needed so as to not stack overflow on huge schematics sadly + */ +public class TarjansAlgorithm { + + private final DependencyGraphScaffoldingOverlay graph; + private final Long2ObjectOpenHashMap infoMap; + private ArrayDeque vposStack; + private ArrayDeque tarjanCallStack; + private int index; + private TarjansResult result; + + private TarjansAlgorithm(DependencyGraphScaffoldingOverlay overlayedGraph) { + this.graph = overlayedGraph; + this.infoMap = new Long2ObjectOpenHashMap<>(); + this.result = new TarjansResult(); + this.vposStack = new ArrayDeque<>(); + this.tarjanCallStack = new ArrayDeque<>(); + } + + public static TarjansResult run(DependencyGraphScaffoldingOverlay overlayedGraph) { + TarjansAlgorithm algo = new TarjansAlgorithm(overlayedGraph); + algo.run(); + return algo.result; + } + + private void run() { + if (Main.DEBUG) { + //System.out.println("Tarjan start"); + } + long a = System.currentTimeMillis(); + graph.forEachReal(pos -> { + strongConnect(pos); + while (!tarjanCallStack.isEmpty()) { + strongConnectPart2(tarjanCallStack.pop()); + } + }); + if (Main.DEBUG) { + //System.out.println("Tarjan end " + (System.currentTimeMillis() - a) + "ms"); + } + } + + private void strongConnect(long vpos) { + if (!graph.real(vpos)) { + throw new IllegalStateException(); + } + TarjanVertexInfo info = infoMap.get(vpos); + if (info == null) { + info = createInfo(vpos); + } else { + if (info.doneWithMainLoop) { + return; + } + } + strongConnectPart2(info); + } + + private TarjanVertexInfo createInfo(long vpos) { + TarjanVertexInfo info = new TarjanVertexInfo(); + info.pos = vpos; + info.index = index++; + info.lowlink = info.index; + vposStack.push(info); + info.onStack = true; + infoMap.put(vpos, info); + return info; + } + + private void strongConnectPart2(TarjanVertexInfo info) { + long vpos = info.pos; + for (Face face : Face.VALUES) { + if (face.index < info.facesCompleted) { + continue; + } + if (graph.outgoingEdge(vpos, face)) { + long wpos = face.offset(vpos); + TarjanVertexInfo winfo = infoMap.get(wpos); + + if (winfo == null) { + if (info.recursingInto != -1) { + throw new IllegalStateException(); + } + info.recursingInto = wpos; + winfo = createInfo(wpos); + tarjanCallStack.push(info); + tarjanCallStack.push(winfo); + return; + } + if (info.recursingInto == wpos) { + info.lowlink = Math.min(info.lowlink, winfo.lowlink); + info.recursingInto = -1; + } else if (winfo.onStack) { + info.lowlink = Math.min(info.lowlink, winfo.index); + } + } + info.facesCompleted = face.index; + } + if (info.recursingInto != -1) { + throw new IllegalStateException(); + } + info.doneWithMainLoop = true; + if (info.lowlink == info.index) { + result.startComponent(); + TarjanVertexInfo winfo; + do { + winfo = vposStack.pop(); + winfo.onStack = false; + result.addNode(winfo.pos); + } while (winfo.pos != vpos); + } + } + + public static class TarjansResult { + + private final Long2IntOpenHashMap posToComponent = new Long2IntOpenHashMap(); + private int componentID = -1; + + private TarjansResult() { + posToComponent.defaultReturnValue(-1); + } + + private void startComponent() { + componentID++; + } + + private void addNode(long nodepos) { + if (posToComponent.put(nodepos, componentID) != -1) { + throw new IllegalStateException(); + } + } + + public int getComponent(long nodepos) { + int result = posToComponent.get(nodepos); + if (result == -1) { + throw new IllegalStateException(); + } + return result; + } + + public int numComponents() { + return componentID + 1; + } + } + + private static class TarjanVertexInfo { + + private long pos; + private int index; + private int lowlink; + private boolean onStack; + private boolean doneWithMainLoop; + private long recursingInto = -1; + private int facesCompleted; + } +} diff --git a/src/main/java/baritone/builder/Testing.java b/src/main/java/baritone/builder/Testing.java new file mode 100644 index 000000000..d73b79c78 --- /dev/null +++ b/src/main/java/baritone/builder/Testing.java @@ -0,0 +1,271 @@ +/* + * 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 . + */ + +package baritone.builder; + +import baritone.api.utils.BetterBlockPos; +import baritone.pathing.calc.openset.BinaryHeapOpenSet; +import baritone.utils.BlockStateInterface; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; + +import java.util.Iterator; + +/** + * Just some testing stuff + */ +public class Testing { + + private static final int MAX_LEAF_LEVEL = 16; + + public static class AbstractNodeCostSearch { + + Long2ObjectOpenHashMap nodeMap; + BinaryHeapOpenSet openSet; + Long2ObjectOpenHashMap zobristMap; + + long[] KEYS_CACHE = new long[MAX_LEAF_LEVEL]; + int[] VALS_CACHE = new int[MAX_LEAF_LEVEL]; + + protected void search() { + while (!openSet.isEmpty()) { + PathNode node = null; + BlockStateInterfaceAbstractWrapper bsi = node.get(this); + + // consider actions: + // traverse + // pillar + // place blocks beneath feet + } + } + + /*protected PathNode getNodeAtPosition(int x, int y, int z, long hashCode) { + baritone.pathing.calc.PathNode node = map.get(hashCode); + if (node == null) { + node = new baritone.pathing.calc.PathNode(x, y, z, goal); + map.put(hashCode, node); + } + return node; + }*/ + } + + + public static final class PathNode { + + public int x; + public int y; + public int z; + + public long zobristBlocks; + + public int heuristic; + public int cost; + public int combinedCost; + public PathNode previous; + public int heapPosition; + + boolean unrealizedZobristBlockChange; + long packedUnrealizedCoordinate; + int unrealizedState; + long unrealizedZobristParentHash; + + public long nodeMapKey() { + return BetterBlockPos.longHash(x, y, z) ^ zobristBlocks; + } + + public BlockStateInterfaceAbstractWrapper get(AbstractNodeCostSearch ref) { + BlockStateInterfaceAbstractWrapper alr = ref.zobristMap.get(zobristBlocks); + if (alr != null) { + return alr; + } + if (!unrealizedZobristBlockChange || (BetterBlockPos.murmur64(BetterBlockPos.longHash(packedUnrealizedCoordinate) ^ unrealizedState) ^ zobristBlocks) != unrealizedZobristParentHash) { + throw new IllegalStateException(); + } + alr = ref.zobristMap.get(unrealizedZobristParentHash).with(packedUnrealizedCoordinate, unrealizedState, ref); + if (alr.zobrist != zobristBlocks) { + throw new IllegalStateException(); + } + ref.zobristMap.put(zobristBlocks, alr); + return alr; + } + } + + public static abstract class BlockStateInterfaceAbstractWrapper { + + protected long zobrist; + + public abstract IBlockState get(long coord); + + public IBlockState get1(int x, int y, int z) { + return get(BetterBlockPos.toLong(x, y, z)); + } + + public BlockStateInterfaceAbstractWrapper with(long packedCoordinate, int state, AbstractNodeCostSearch ref) { + return new BlockStateInterfaceLeafDiff(this, packedCoordinate, Block.BLOCK_STATE_IDS.getByValue(state)); + } + + } + + public static class BlockStateInterfaceLeafDiff extends BlockStateInterfaceAbstractWrapper { + + private BlockStateInterfaceAbstractWrapper delegate; + private final long coord; + private final IBlockState state; + + public BlockStateInterfaceLeafDiff(BlockStateInterfaceAbstractWrapper delegate, long coord, IBlockState state) { + this.delegate = delegate; + this.state = state; + this.coord = coord; + zobrist = updateZobristHash(delegate.zobrist, coord, state, delegate.get(coord)); + } + + public static long updateZobristHash(long oldZobristHash, long coord, IBlockState emplaced, IBlockState old) { + if (old != null) { + return updateZobristHash(updateZobristHash(oldZobristHash, coord, emplaced, null), coord, old, null); + } + return oldZobristHash ^ BetterBlockPos.murmur64(BetterBlockPos.longHash(coord) ^ Block.BLOCK_STATE_IDS.get(emplaced)); + } + + @Override + public IBlockState get(long coord) { + if (this.coord == coord) { + return state; + } + return delegate.get(coord); + } + + public int leafLevels() { // cant cache leaflevel because of how materialize changes it + return 1 + (delegate instanceof BlockStateInterfaceLeafDiff ? ((BlockStateInterfaceLeafDiff) delegate).leafLevels() : 0); + } + + @Override + public BlockStateInterfaceAbstractWrapper with(long packedCoordinate, int state, AbstractNodeCostSearch ref) { + int level = leafLevels(); + if (level < MAX_LEAF_LEVEL) { + return super.with(packedCoordinate, state, ref); + } + if (level > MAX_LEAF_LEVEL) { + throw new IllegalStateException(); + } + BlockStateInterfaceLeafDiff parent = this; + for (int i = 0; i < MAX_LEAF_LEVEL / 2; i++) { + parent = (BlockStateInterfaceLeafDiff) parent.delegate; + } + parent.delegate = ((BlockStateInterfaceLeafDiff) parent.delegate).materialize(ref); + return super.with(packedCoordinate, state, ref); + } + + public BlockStateInterfaceMappedDiff materialize(AbstractNodeCostSearch ref) { + BlockStateInterfaceLeafDiff parent = this; + int startIdx = MAX_LEAF_LEVEL; + BlockStateInterfaceMappedDiff ancestor; + long[] keys = ref.KEYS_CACHE; + int[] vals = ref.VALS_CACHE; + while (true) { + startIdx--; + keys[startIdx] = parent.coord; + vals[startIdx] = Block.BLOCK_STATE_IDS.get(parent.state); + if (parent.delegate instanceof BlockStateInterfaceLeafDiff) { + parent = (BlockStateInterfaceLeafDiff) parent.delegate; + } else { + if (parent.delegate instanceof BlockStateInterfaceMappedDiff) { + ancestor = (BlockStateInterfaceMappedDiff) parent.delegate; + } else { + ancestor = new BlockStateInterfaceMappedDiff((BlockStateInterfaceWrappedSubstrate) parent.delegate); + } + break; + } + } + BlockStateInterfaceMappedDiff coalesced = new BlockStateInterfaceMappedDiff(ancestor, keys, vals, startIdx, zobrist); + ref.zobristMap.put(zobrist, coalesced); + return coalesced; + } + } + + public static class BlockStateInterfaceMappedDiff extends BlockStateInterfaceAbstractWrapper { + + private final BlockStateInterfaceWrappedSubstrate delegate; + private final Long2ObjectOpenHashMap sections; + private static final long MASK = BetterBlockPos.toLong(15, 15, 15); + + public BlockStateInterfaceMappedDiff(BlockStateInterfaceWrappedSubstrate substrate) { + this.delegate = substrate; + this.sections = new Long2ObjectOpenHashMap<>(); + } + + public BlockStateInterfaceMappedDiff(BlockStateInterfaceMappedDiff delegate, long[] diffKeys, int[] diffVals, int startIdx, long zobrist) { + this.sections = new Long2ObjectOpenHashMap<>(); + this.zobrist = zobrist; + this.delegate = delegate.delegate; + for (int i = startIdx; i < diffKeys.length; i++) { + long bucket = diffKeys[i] & ~MASK; + Long2IntOpenHashMap val = sections.get(bucket); + if (val == null) { + Long2IntOpenHashMap parent = delegate.sections.get(bucket); + if (parent == null) { + val = new Long2IntOpenHashMap(); + val.defaultReturnValue(-1); + } else { + val = parent.clone(); + } + sections.put(bucket, val); + } + val.put(diffKeys[i] & MASK, diffVals[i]); + } + Iterator> it = delegate.sections.long2ObjectEntrySet().fastIterator(); + while (it.hasNext()) { + Long2ObjectMap.Entry entry = it.next(); + if (sections.containsKey(entry.getLongKey())) { + continue; + } + sections.put(entry.getLongKey(), entry.getValue()); + } + } + + @Override + public IBlockState get(long coord) { + Long2IntOpenHashMap candidateSection = sections.get(coord & ~MASK); + if (candidateSection != null) { + int val = candidateSection.get(coord & MASK); + if (val != -1) { + return Block.BLOCK_STATE_IDS.getByValue(val); + } + } + return delegate.get(coord); + } + } + + public static class BlockStateInterfaceWrappedSubstrate extends BlockStateInterfaceAbstractWrapper { + + private BlockStateInterface delegate; + + @Override + public IBlockState get(long coord) { + return delegate.get0(BetterBlockPos.XfromLong(coord), BetterBlockPos.YfromLong(coord), BetterBlockPos.ZfromLong(coord)); + } + + /*@Override + public IBlockState getOverridden(long coord) { + return null; + }*/ + } + + +} diff --git a/src/main/java/baritone/utils/Testing.java b/src/main/java/baritone/utils/Testing.java deleted file mode 100644 index d7a47ec0f..000000000 --- a/src/main/java/baritone/utils/Testing.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 . - */ - -package baritone.utils; - -import baritone.api.utils.BetterBlockPos; -import baritone.pathing.calc.openset.BinaryHeapOpenSet; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import net.minecraft.block.Block; -import net.minecraft.block.state.IBlockState; - -public class Testing { - public static class AbstractNodeCostSearch { - Long2ObjectOpenHashMap nodeMap; - BinaryHeapOpenSet openSet; - Long2ObjectOpenHashMap zobristMap; - - protected PathNode getNodeAtPosition(int x, int y, int z, long hashCode) { - baritone.pathing.calc.PathNode node = map.get(hashCode); - if (node == null) { - node = new baritone.pathing.calc.PathNode(x, y, z, goal); - map.put(hashCode, node); - } - return node; - } - } - - - public static final class PathNode { - public int x; - public int y; - public int z; - - public long zobristBlocks; - - public int estimatedCostToGoal; - public int cost; - public int combinedCost; - public PathNode previous; - public int heapPosition; - - public long nodeMapKey() { - return BetterBlockPos.longHash(x, y, z) ^ zobristBlocks; - } - } - - public static abstract class BlockStateInterfaceAbstractWrapper { - protected long zobrist; - - public abstract IBlockState get(int x, int y, int z); - - } - - public static class BlockStateInterfaceLeafDiff extends BlockStateInterfaceAbstractWrapper { - private BlockStateInterfaceAbstractWrapper delegate; - int x; - int y; - int z; - IBlockState state; - - @Override - public IBlockState get(int x, int y, int z) { - if (x == this.x && y == this.y && z == this.z) { - return state; - } - return delegate.get(x, y, z); - } - - public int leafLevels() { - return 1 + (delegate instanceof BlockStateInterfaceLeafDiff ? ((BlockStateInterfaceLeafDiff) delegate).leafLevels() : 0); - } - - public static long updateZobrist(long oldZobrist, int x, int y, int z, IBlockState emplaced, IBlockState old) { - if (old != null) { - return updateZobrist(updateZobrist(oldZobrist, x, y, z, emplaced, null), x, y, z, old, null); - } - return oldZobrist ^ BetterBlockPos.murmur64(BetterBlockPos.longHash(x, y, z) ^ Block.BLOCK_STATE_IDS.get(emplaced)); - } - } - - public static class BlockStateInterfaceWrappedSubstrate extends BlockStateInterfaceAbstractWrapper { - private BlockStateInterface delegate; - - @Override - public IBlockState get(int x, int y, int z) { - return delegate.get0(x, y, z); - } - } - - -}