diff --git a/src/main/java/com/gtnewhorizon/structurelib/alignment/IntegerAxisSwap.java b/src/main/java/com/gtnewhorizon/structurelib/alignment/IntegerAxisSwap.java index 526ba1f1..b9d69a5f 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/alignment/IntegerAxisSwap.java +++ b/src/main/java/com/gtnewhorizon/structurelib/alignment/IntegerAxisSwap.java @@ -5,7 +5,11 @@ import static java.lang.Math.abs; import net.minecraft.util.Vec3; import net.minecraftforge.common.util.ForgeDirection; +import org.joml.Vector3i; + import com.gtnewhorizon.structurelib.alignment.enumerable.Direction; +import com.gtnewhorizon.structurelib.coords.CoordinateSystem; +import com.gtnewhorizon.structurelib.coords.Position; import com.gtnewhorizon.structurelib.util.Vec3Impl; public class IntegerAxisSwap { @@ -91,4 +95,32 @@ public class IntegerAxisSwap { out[1] = forFirstAxis.get1() * point[0] + forSecondAxis.get1() * point[1] + forThirdAxis.get1() * point[2]; out[2] = forFirstAxis.get2() * point[0] + forSecondAxis.get2() * point[1] + forThirdAxis.get2() * point[2]; } + + public Vector3i translate(Vector3i point) { + return new Vector3i( + forFirstAxis.get0() * point.x() + forFirstAxis.get1() * point.y() + forFirstAxis.get2() * point.z(), + forSecondAxis.get0() * point.x() + forSecondAxis.get1() * point.y() + forSecondAxis.get2() * point.z(), + forThirdAxis.get0() * point.x() + forThirdAxis.get1() * point.y() + forThirdAxis.get2() * point.z()); + } + + public Vector3i inverseTranslate(Vector3i point) { + return new Vector3i( + forFirstAxis.get0() * point.x() + forSecondAxis.get0() * point.y() + forThirdAxis.get0() * point.z(), + forFirstAxis.get1() * point.x() + forSecondAxis.get1() * point.y() + forThirdAxis.get1() * point.z(), + forFirstAxis.get2() * point.x() + forSecondAxis.get2() * point.y() + forThirdAxis.get2() * point.z()); + } + + public > Position translate(Position point) { + return new Position<>( + forFirstAxis.get0() * point.x() + forFirstAxis.get1() * point.y() + forFirstAxis.get2() * point.z(), + forSecondAxis.get0() * point.x() + forSecondAxis.get1() * point.y() + forSecondAxis.get2() * point.z(), + forThirdAxis.get0() * point.x() + forThirdAxis.get1() * point.y() + forThirdAxis.get2() * point.z()); + } + + public > Position inverseTranslate(Position point) { + return new Position<>( + forFirstAxis.get0() * point.x() + forSecondAxis.get0() * point.y() + forThirdAxis.get0() * point.z(), + forFirstAxis.get1() * point.x() + forSecondAxis.get1() * point.y() + forThirdAxis.get1() * point.z(), + forFirstAxis.get2() * point.x() + forSecondAxis.get2() * point.y() + forThirdAxis.get2() * point.z()); + } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/alignment/enumerable/ExtendedFacing.java b/src/main/java/com/gtnewhorizon/structurelib/alignment/enumerable/ExtendedFacing.java index 6358b1a6..a92d840e 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/alignment/enumerable/ExtendedFacing.java +++ b/src/main/java/com/gtnewhorizon/structurelib/alignment/enumerable/ExtendedFacing.java @@ -24,6 +24,7 @@ import net.minecraftforge.common.util.ForgeDirection; import com.google.common.collect.ImmutableSet; import com.gtnewhorizon.structurelib.alignment.IAlignment; import com.gtnewhorizon.structurelib.alignment.IntegerAxisSwap; +import com.gtnewhorizon.structurelib.coords.StructureRelativeCoords; import com.gtnewhorizon.structurelib.util.Vec3Impl; public enum ExtendedFacing { @@ -166,6 +167,7 @@ public enum ExtendedFacing { private final String name; private final IntegerAxisSwap integerAxisSwap; + private final StructureRelativeCoords coordinateSystem; ExtendedFacing(String name) { this.name = name; @@ -247,6 +249,7 @@ public enum ExtendedFacing { this.b = b; this.c = c; integerAxisSwap = new IntegerAxisSwap(a, b, c); + coordinateSystem = new StructureRelativeCoords(integerAxisSwap); } public static ExtendedFacing of(ForgeDirection direction, Rotation rotation, Flip flip) { @@ -437,4 +440,8 @@ public enum ExtendedFacing { public ForgeDirection getRelativeForwardInWorld() { return c.getOpposite(); } + + public StructureRelativeCoords asCoordinateSystem() { + return coordinateSystem; + } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/ControllerRelativeCoords.java b/src/main/java/com/gtnewhorizon/structurelib/coords/ControllerRelativeCoords.java new file mode 100644 index 00000000..96fdb664 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/ControllerRelativeCoords.java @@ -0,0 +1,40 @@ +package com.gtnewhorizon.structurelib.coords; + +/** + * Controller-relative coordinates. The controller is at 0,0,0. No axis swapping takes place, each axis points in the + * same directions as the matching world axis. + */ +public class ControllerRelativeCoords implements CoordinateSystem { + + public final int controllerX, controllerY, controllerZ; + + public ControllerRelativeCoords(int controllerX, int controllerY, int controllerZ) { + this.controllerX = controllerX; + this.controllerY = controllerY; + this.controllerZ = controllerZ; + } + + @Override + public Position translate(Position position) { + return translate(position, controllerX, controllerY, controllerZ); + } + + @Override + public Position translateInverse(Position position) { + return translateInverse(position, controllerX, controllerY, controllerZ); + } + + public static Position translate(Position position, int controllerX, + int controllerY, int controllerZ) { + position.sub(controllerX, controllerY, controllerZ); + + return CoordinateSystem.transmute(position); + } + + public static Position translateInverse(Position position, int controllerX, + int controllerY, int controllerZ) { + position.add(controllerX, controllerY, controllerZ); + + return CoordinateSystem.transmute(position); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/CoordinateSystem.java b/src/main/java/com/gtnewhorizon/structurelib/coords/CoordinateSystem.java new file mode 100644 index 00000000..69e21457 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/CoordinateSystem.java @@ -0,0 +1,34 @@ +package com.gtnewhorizon.structurelib.coords; + +import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing; + +/** + * A coordinate system is a statically-checked helper for determining which coordinates a {@link Position} has. + * Translation methods never copy a {@link Position} - if a position must remain unchanged, it must be copied + * separately. Most coordinate systems will have static methods that allow you to avoid allocations, though some (such + * as the ones provided by {@link ExtendedFacing#asCoordinateSystem()}) have no reason to be static. + * + * @param + * @param + */ +public interface CoordinateSystem, Parent extends CoordinateSystem> { + + /** + * Translates a position from the parent coordinate system into this one. + */ + Position translate(Position position); + + /** + * Translates a position from this coordinate system into the parent. + */ + Position translateInverse(Position position); + + /** + * Helper method that converts a position's coordinate system. Should not be used unless you know it's correct. + */ + static , P2 extends CoordinateSystem> Position transmute( + Position pos) { + // noinspection unchecked + return (Position) pos; + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/Position.java b/src/main/java/com/gtnewhorizon/structurelib/coords/Position.java new file mode 100644 index 00000000..aefb8709 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/Position.java @@ -0,0 +1,39 @@ +package com.gtnewhorizon.structurelib.coords; + +import net.minecraft.util.MathHelper; +import net.minecraft.world.ChunkPosition; + +import org.joml.Vector3f; +import org.joml.Vector3i; + +import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; + +/** + * A {@link BlockPos} with a statically-checked coordinate system. + */ +public class Position> extends BlockPos { + + public Position() { + + } + + public Position(int x, int y, int z) { + super(x, y, z); + } + + public Position(ChunkPosition chunkPosition) { + super(chunkPosition); + } + + public Position(Vector3i v) { + super(v.x, v.y, v.z); + } + + public Position(Vector3f v) { + super(MathHelper.floor_float(v.x), MathHelper.floor_float(v.y), MathHelper.floor_float(v.z)); + } + + public Position copy() { + return new Position<>(x, y, z); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/StructureDefinitionCoords.java b/src/main/java/com/gtnewhorizon/structurelib/coords/StructureDefinitionCoords.java new file mode 100644 index 00000000..b764b1e6 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/StructureDefinitionCoords.java @@ -0,0 +1,38 @@ +package com.gtnewhorizon.structurelib.coords; + +/// The structure definition-local coords. 0,0,0 is the front-most left-most top-most element. Coordinates increase as +/// you go down, right, or back. +public class StructureDefinitionCoords implements CoordinateSystem { + + public final int offsetX, offsetY, offsetZ; + + public StructureDefinitionCoords(int offsetX, int offsetY, int offsetZ) { + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + } + + @Override + public Position translate(Position position) { + return translate(position, offsetX, offsetY, offsetZ); + } + + @Override + public Position translateInverse(Position position) { + return translateInverse(position, offsetX, offsetY, offsetZ); + } + + public static Position translate(Position position, int offsetX, + int offsetY, int offsetZ) { + position.add(offsetX, offsetY, offsetZ); + + return CoordinateSystem.transmute(position); + } + + public static Position translateInverse(Position position, + int offsetX, int offsetY, int offsetZ) { + position.sub(offsetX, offsetY, offsetZ); + + return CoordinateSystem.transmute(position); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java b/src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java new file mode 100644 index 00000000..04cd5ef0 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java @@ -0,0 +1,31 @@ +package com.gtnewhorizon.structurelib.coords; + +import com.gtnewhorizon.structurelib.alignment.IntegerAxisSwap; + +/** + * Coordinates relative to the structure. 0,0,0 is the controller. 1,0,0 is the block immediately behind the controller + * (looking at it from its face). 0,0,1 is the block to the right of the controller. 0,1,0 is the block below the + * controller. + */ +public class StructureRelativeCoords implements CoordinateSystem { + + public final IntegerAxisSwap axisSwap; + + public StructureRelativeCoords(IntegerAxisSwap axisSwap) { + this.axisSwap = axisSwap; + } + + @Override + public Position translate(Position position) { + position.set(axisSwap.translate(position)); + + return CoordinateSystem.transmute(position); + } + + @Override + public Position translateInverse(Position position) { + position.set(axisSwap.inverseTranslate(position)); + + return CoordinateSystem.transmute(position); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/WorldCoords.java b/src/main/java/com/gtnewhorizon/structurelib/coords/WorldCoords.java new file mode 100644 index 00000000..73b7e36b --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/WorldCoords.java @@ -0,0 +1,21 @@ +package com.gtnewhorizon.structurelib.coords; + +/** + * World block coordinates + */ +public class WorldCoords implements CoordinateSystem { + + public static final WorldCoords INSTANCE = new WorldCoords(); + + private WorldCoords() {} + + @Override + public Position translate(Position position) { + return position; + } + + @Override + public Position translateInverse(Position position) { + return position; + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java index d9cba10d..be1933e0 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java @@ -3,8 +3,11 @@ package com.gtnewhorizon.structurelib.structure; import static com.gtnewhorizon.structurelib.structure.IStructureWalker.ignoreBlockUnloaded; import static com.gtnewhorizon.structurelib.structure.IStructureWalker.skipBlockUnloaded; +import java.util.List; import java.util.function.Function; +import javax.annotation.Nonnull; + import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; @@ -15,6 +18,8 @@ import com.gtnewhorizon.structurelib.StructureLibAPI; import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; import com.gtnewhorizon.structurelib.alignment.constructable.ISurvivalConstructable; import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing; +import com.gtnewhorizon.structurelib.coords.Position; +import com.gtnewhorizon.structurelib.coords.StructureDefinitionCoords; /** * This is the structure definition of your multi. You will have one of these for each multi. @@ -319,6 +324,49 @@ public interface IStructureDefinition { return walker.getBuilt(); } + /** + * Gets the controller position for a piece. Controller positions are denoted by a tilde. + * + * @param piece The piece name + * @return The controller position, or null if it doesn't exist for the given piece + */ + Position getControllerPosition(String piece); + + /** + * Gets the coordinate system for a piece. This is used to translate structure definition coordinates into structure + * relative coordinates. + * + * @param piece The piece + * @return The coordinate system + * @throws IllegalArgumentException When the piece does not exist + */ + StructureDefinitionCoords getCoordinateSystem(String piece); + + /** + * Gets a socket from a piece. + * + * @param piece The piece name + * @param socket The socket character + * @return The socket's position + * @throws IllegalArgumentException When the piece does not exist + * @throws IllegalArgumentException When the socket does not exist + * @throws IllegalStateException When more than one socket exists with this character + * @see StructureDefinition.Builder#addSocket(char, char) + */ + Position getSocket(String piece, char socket); + + /** + * Gets all sockets with a given character for a piece. + * + * @param piece The piece name + * @param socket The socket character + * @return The list of socket positions, or an empty list if the socket is not present + * @throws IllegalArgumentException When the piece does not exist + * @see StructureDefinition.Builder#addSocket(char, char) + */ + @Nonnull + List> getAllSockets(String piece, char socket); + /** * Low level utility. * diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java index 2c335b9d..15062a56 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java @@ -4,33 +4,49 @@ import static com.gtnewhorizon.structurelib.structure.StructureUtility.isAir; import static com.gtnewhorizon.structurelib.structure.StructureUtility.notAir; import static com.gtnewhorizon.structurelib.structure.StructureUtility.step; -import java.util.Arrays; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; -import java.util.stream.Collectors; +import javax.annotation.Nonnull; + +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.gtnewhorizon.structurelib.coords.Position; +import com.gtnewhorizon.structurelib.coords.StructureDefinitionCoords; import com.gtnewhorizon.structurelib.util.Vec3Impl; +import it.unimi.dsi.fastutil.chars.Char2CharOpenHashMap; +import it.unimi.dsi.fastutil.chars.CharOpenHashSet; + public class StructureDefinition implements IStructureDefinition { private final Map> elements; private final Map shapes; private final Map[]> structures; private final Map> occupiedSpaces; + private final Map> controllers; + private final Map>> socketLocations; public static Builder builder() { return new Builder<>(); } private StructureDefinition(Map> elements, Map shapes, - Map[]> structures, Map> occupiedSpaces) { + Map[]> structures, Map> occupiedSpaces, + Map> controllers, + Map>> socketLocations) { this.elements = elements; this.shapes = shapes; this.structures = structures; this.occupiedSpaces = occupiedSpaces; + this.controllers = controllers; + this.socketLocations = socketLocations; } public static class Builder { @@ -44,6 +60,10 @@ public class StructureDefinition implements IStructureDefinition { private final Map shapes; private final Map> occupiedSpaces; + private final Map> controllers = new HashMap<>(); + private final Char2CharOpenHashMap socketTranslations = new Char2CharOpenHashMap(); + private final Map>> socketLocations = new HashMap<>(); + private Builder() { navigates = new HashMap<>(); elements = new HashMap<>(); @@ -83,6 +103,7 @@ public class StructureDefinition implements IStructureDefinition { */ public Builder addShape(String name, String[][] structurePiece) { StringBuilder builder = new StringBuilder(); + if (structurePiece.length > 0) { for (String[] strings : structurePiece) { if (strings.length > 0) { @@ -95,6 +116,7 @@ public class StructureDefinition implements IStructureDefinition { } builder.setLength(builder.length() - 1); } + Set occupiedSpace = new HashSet<>(); // these track the global current location int aa = 0, bb = 0, cc = 0; @@ -110,6 +132,7 @@ public class StructureDefinition implements IStructureDefinition { builder.setCharAt(i, A); ch = A; occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc)); + controllers.put(name, new Position<>(aa, bb, cc)); } if (ch == A) { aa++; @@ -150,10 +173,10 @@ public class StructureDefinition implements IStructureDefinition { String built = builder.toString().replaceAll("[\\uA000\\uB000\\uC000]", ""); - if (built.contains("+")) { + if (built.contains("+") || socketTranslations.values().contains('+')) { addElement('+', notAir()); } - if (built.contains("-")) { + if (built.contains("-") || socketTranslations.values().contains('-')) { addElement('-', isAir()); } shapes.put(name, built); @@ -174,50 +197,112 @@ public class StructureDefinition implements IStructureDefinition { return this; } + /** + * Denotes {@code name} as a socket. Sockets are virtual structure elements that record their position in the + * structure for future retrieval. They are transformed into real structure elements and do not need their own + * element (see {@code replacement}). + * + * @param name The socket name character + * @param replacement The replacement character + * @see IStructureDefinition#getSocket(String, char) + * @see IStructureDefinition#getAllSockets(String, char) + */ + public Builder addSocket(char name, char replacement) { + socketTranslations.put(name, replacement); + return this; + } + + /** + * Like {@link #addSocket(char, char)} but without the replacement. The structure must have a structure element + * with the given character. + * + * @param name The socket name character + */ + public Builder addSocket(char name) { + // Adding a name -> name entry like this isn't ideal but this is the cleanest solution + socketTranslations.put(name, name); + return this; + } + public IStructureDefinition build() { Map[]> structures = compileStructureMap(); return new StructureDefinition<>( new HashMap<>(elements), new HashMap<>(shapes), structures, - occupiedSpaces); - } - - @SuppressWarnings("unchecked") - private Map[]> compileElementSetMap() { - Set missing = findMissing(); - if (missing.isEmpty()) { - return shapes.entrySet().stream().collect( - Collectors.toMap( - Map.Entry::getKey, - e -> e.getValue().chars().mapToObj(c -> (char) c).distinct().map(elements::get) - .toArray(IStructureElement[]::new))); - } else { - throw new RuntimeException( - "Missing Structure Element bindings for (chars as integers): " - + Arrays.toString(missing.toArray())); - } + occupiedSpaces, + controllers, + socketLocations); } @SuppressWarnings("unchecked") private Map[]> compileStructureMap() { - Set missing = findMissing(); - if (missing.isEmpty()) { - return shapes.entrySet().stream().collect( - Collectors.toMap( - Map.Entry::getKey, - e -> e.getValue().chars().mapToObj(c -> elements.get((char) c)) - .toArray(IStructureElement[]::new))); - } else { - throw new RuntimeException( - "Missing Structure Element bindings for (chars as integers): " - + Arrays.toString(missing.toArray())); + CharOpenHashSet missing = findMissing(); + + if (!missing.isEmpty()) { + throw new RuntimeException("Missing Structure Element bindings for: " + missing); } + + Map[]> map = new HashMap<>(); + + for (Entry e : shapes.entrySet()) { + char[] chars = e.getValue().toCharArray(); + IStructureElement[] compiled = new IStructureElement[chars.length]; + + int a = 0, b = 0, c = 0; + + for (int i = 0; i < chars.length; i++) { + char ch = chars[i]; + + if (socketTranslations.containsKey(ch)) { + // noinspection UnstableApiUsage + var positions = socketLocations + .computeIfAbsent(e.getKey(), s -> MultimapBuilder.hashKeys().arrayListValues().build()); + + positions.put(ch, new Position<>(a, b, c)); + + ch = socketTranslations.get(ch); + } + + IStructureElement element = this.elements.get(ch); + + compiled[i] = element; + + if (element.isNavigating()) { + a = (element.resetA() ? 0 : a) + element.getStepA(); + b = (element.resetB() ? 0 : b) + element.getStepB(); + c = (element.resetC() ? 0 : c) + element.getStepC(); + } else { + a++; + } + } + + if (map.put(e.getKey(), compiled) != null) { + throw new IllegalStateException("Duplicate shape: " + e.getKey()); + } + } + + return map; } - private Set findMissing() { - return shapes.values().stream().flatMapToInt(CharSequence::chars) - .filter(i -> !elements.containsKey((char) i)).boxed().collect(Collectors.toSet()); + private CharOpenHashSet findMissing() { + CharOpenHashSet missing = new CharOpenHashSet(); + + for (String shape : shapes.values()) { + for (char c : shape.toCharArray()) { + if (!elements.containsKey(c) && !socketTranslations.containsKey(c)) { + missing.add(c); + } + } + } + + for (char c : socketTranslations.values()) { + if (!elements.containsKey(c)) { + missing.add(c); + } + } + + return missing; } } @@ -246,4 +331,61 @@ public class StructureDefinition implements IStructureDefinition { if (occupiedSpace == null) throw new NoSuchElementException(name); return occupiedSpace.contains(new Vec3Impl(offsetA, offsetB, offsetC)); } + + @Override + public Position getControllerPosition(String piece) { + return controllers.get(piece); + } + + @Override + public StructureDefinitionCoords getCoordinateSystem(String piece) { + var controller = controllers.get(piece); + + if (controller == null) { + throw new IllegalArgumentException("Invalid piece: " + piece); + } + + return new StructureDefinitionCoords(controller.x, controller.y, controller.z); + } + + @Override + public Position getSocket(String piece, char socket) { + var piecePositions = socketLocations.get(piece); + + if (piecePositions == null) { + throw new IllegalArgumentException("Invalid piece: " + piece); + } + + var positions = piecePositions.get(socket); + + if (positions.isEmpty()) { + throw new IllegalArgumentException("Socket " + socket + " for piece " + piece + " does not exist"); + } + + if (positions.size() > 1) { + throw new IllegalStateException( + "Socket " + socket + + " for piece " + + piece + + " has several positions, but expected one and only one"); + } + + return positions.get(0).copy(); + } + + @Override + @Nonnull + public List> getAllSockets(String piece, char socket) { + var piecePositions = socketLocations.get(piece); + + if (piecePositions == null) { + throw new IllegalArgumentException("Invalid piece: " + piece); + } + + var list = new ArrayList<>(piecePositions.get(socket)); + + list.replaceAll(Position::copy); + + return list; + } }