From c3671609d34ed66cfe20b68d99e3dd2922e77f14 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Tue, 28 Oct 2025 12:34:01 -0400 Subject: [PATCH 1/5] Track structure controller position and add structure sockets --- .../alignment/IntegerAxisSwap.java | 32 +++ .../alignment/enumerable/ExtendedFacing.java | 7 + .../coords/ControllerRelativeCoords.java | 40 ++++ .../structurelib/coords/CoordinateSystem.java | 34 +++ .../structurelib/coords/Position.java | 39 ++++ .../coords/StructureDefinitionCoords.java | 38 ++++ .../coords/StructureRelativeCoords.java | 31 +++ .../structurelib/coords/WorldCoords.java | 21 ++ .../structure/IStructureDefinition.java | 48 ++++ .../structure/StructureDefinition.java | 212 +++++++++++++++--- 10 files changed, 467 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/gtnewhorizon/structurelib/coords/ControllerRelativeCoords.java create mode 100644 src/main/java/com/gtnewhorizon/structurelib/coords/CoordinateSystem.java create mode 100644 src/main/java/com/gtnewhorizon/structurelib/coords/Position.java create mode 100644 src/main/java/com/gtnewhorizon/structurelib/coords/StructureDefinitionCoords.java create mode 100644 src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java create mode 100644 src/main/java/com/gtnewhorizon/structurelib/coords/WorldCoords.java 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 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 void inverseTranslate(double[] point, double[] out) { 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 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 ForgeDirection getRelativeBackInWorld() { 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 @@ 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.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 @@ default int survivalBuild(T object, ItemStack trigger, String piece, World world 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.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 static class Builder { 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 Map getShapes() { */ 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 Builder addShape(String name, String[][] structurePiece) { } 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 Builder addShape(String name, String[][] structurePiece) { 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 Builder addShape(String name, String[][] structurePiece) { 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 Builder addElement(char name, IStructureElement structurePiece) { 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); + occupiedSpaces, + controllers, + socketLocations); } @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())); + private Map[]> compileStructureMap() { + CharOpenHashSet missing = findMissing(); + + if (!missing.isEmpty()) { + throw new RuntimeException("Missing Structure Element bindings for: " + missing); } - } - @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())); + 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 boolean isContainedInStructure(String name, int offsetA, int offsetB, int 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; + } } From 7f42cc1de8a215c4825960284090a4dcc08cac95 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Sun, 2 Nov 2025 14:31:56 -0500 Subject: [PATCH 2/5] Add useful methods & constructors to Position --- .../structurelib/coords/Position.java | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/Position.java b/src/main/java/com/gtnewhorizon/structurelib/coords/Position.java index aefb8709..6b84b2a8 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/coords/Position.java +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/Position.java @@ -1,12 +1,14 @@ package com.gtnewhorizon.structurelib.coords; import net.minecraft.util.MathHelper; +import net.minecraft.util.Vec3; import net.minecraft.world.ChunkPosition; import org.joml.Vector3f; import org.joml.Vector3i; import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; +import com.gtnewhorizon.structurelib.util.Vec3Impl; /** * A {@link BlockPos} with a statically-checked coordinate system. @@ -21,6 +23,14 @@ public Position(int x, int y, int z) { super(x, y, z); } + public Position(float x, float y, float z) { + super(MathHelper.floor_float(x), MathHelper.floor_float(y), MathHelper.floor_float(z)); + } + + public Position(double x, double y, double z) { + super(MathHelper.floor_double(x), MathHelper.floor_double(y), MathHelper.floor_double(z)); + } + public Position(ChunkPosition chunkPosition) { super(chunkPosition); } @@ -30,10 +40,34 @@ public Position(Vector3i v) { } public Position(Vector3f v) { - super(MathHelper.floor_float(v.x), MathHelper.floor_float(v.y), MathHelper.floor_float(v.z)); + this(v.x, v.y, v.z); + } + + public Position(Vec3 v) { + this(v.xCoord, v.yCoord, v.zCoord); + } + + public Position(Vec3Impl v) { + this(v.get0(), v.get1(), v.get2()); } public Position copy() { return new Position<>(x, y, z); } + + public Vector3i toVector3i() { + return new Vector3i(x, y, z); + } + + public Vector3f toVector3f() { + return new Vector3f(x, y, z); + } + + public Vec3 toVec3() { + return Vec3.createVectorHelper(x, y, z); + } + + public Vec3Impl toVec3Impl() { + return new Vec3Impl(x, y, z); + } } From 1166c651d2942664b4902d61781cd51c0d8aca8b Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Sun, 2 Nov 2025 14:32:16 -0500 Subject: [PATCH 3/5] Add coordinate system tests --- .../coords/StructureRelativeCoords.java | 4 +- .../structurelib/coords/CoordinateTests.java | 100 ++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java diff --git a/src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java b/src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java index 04cd5ef0..ea52ffff 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java +++ b/src/main/java/com/gtnewhorizon/structurelib/coords/StructureRelativeCoords.java @@ -3,8 +3,8 @@ 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 + * Coordinates relative to the structure. 0,0,0 is the controller. 0,0,1 is the block immediately behind the controller + * (looking at it from its face). 1,0,0 is the block to the right of the controller. 0,1,0 is the block below the * controller. */ public class StructureRelativeCoords implements CoordinateSystem { diff --git a/src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java b/src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java new file mode 100644 index 00000000..cd30ea87 --- /dev/null +++ b/src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java @@ -0,0 +1,100 @@ +package com.gtnewhorizon.structurelib.coords; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing; +import com.gtnewhorizon.structurelib.structure.IStructureDefinition; +import com.gtnewhorizon.structurelib.structure.StructureUtility; + +class CoordinateTests { + + private IStructureDefinition getDefinition() { + String[][] main = { + { " ", " ", " ", " ", " " }, + { " ", " ", " ", " ", " " }, + { " ", " ", " X ", " ", " " }, + { " ", " ", " YY ", " ", " " }, + { " ~ ", " ", " ", " ", " " } + }; + + return IStructureDefinition.builder() + .addShape("main", StructureUtility.transpose(main)) + .addSocket('X', ' ') + .addSocket('Y', ' ') + .build(); + } + + @Test + void testSocketAndCoordSystemTranslationToWorld() { + IStructureDefinition def = getDefinition(); + + // Note: the position is the same object between all of these transforms, it is never copied, its generic just + // changes + + var p = def.getSocket("main", 'X'); + Assertions.assertEquals(new Position<>(2, 2, 2), p); + + var p2 = def.getCoordinateSystem("main").translateInverse(p); + Assertions.assertEquals(new Position<>(0, -2, 2), p2); + + var p3 = ExtendedFacing.NORTH_NORMAL_NONE.asCoordinateSystem().translateInverse(p2); + Assertions.assertEquals(new Position<>(0, 2, 2), p3); + + var p4 = ControllerRelativeCoords.translateInverse(p3, 5, 5, 5); + Assertions.assertEquals(new Position<>(5, 7, 7), p4); + } + + @Test + void testSocketAndCoordSystemTranslationToStructureDef() { + IStructureDefinition def = getDefinition(); + + // Note: the position is the same object between all of these transforms, it is never copied, its generic just + // changes + + var p = new Position(5, 7, 7); + + var p2 = ControllerRelativeCoords.translate(p, 5, 5, 5); + Assertions.assertEquals(new Position<>(0, 2, 2), p2); + + var p3 = ExtendedFacing.NORTH_NORMAL_NONE.asCoordinateSystem().translate(p2); + Assertions.assertEquals(new Position<>(0, -2, 2), p3); + + var p4 = def.getCoordinateSystem("main").translate(p3); + Assertions.assertEquals(new Position<>(2, 2, 2), p4); + + Assertions.assertEquals(def.getSocket("main", 'X'), p4); + } + + @Test + void testSocketGetters() { + IStructureDefinition def = getDefinition(); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> def.getSocket("invalid", 'X')); + Assertions.assertThrows( + IllegalStateException.class, + () -> def.getSocket("main", 'Y')); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> def.getSocket("main", 'Z')); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> def.getAllSockets("invalid", 'X')); + + Assertions.assertEquals( + Collections.emptyList(), + def.getAllSockets("main", 'Z')); + Assertions.assertEquals( + Arrays.asList(new Position<>(2, 2, 2)), + def.getAllSockets("main", 'X')); + Assertions.assertEquals( + Arrays.asList(new Position<>(2, 3, 2), new Position<>(3, 3, 2)), + def.getAllSockets("main", 'Y')); + } +} From 87903fead7bcd488c98bc31512ddaaa449efdc55 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Sun, 2 Nov 2025 14:40:20 -0500 Subject: [PATCH 4/5] Defer shape compilation into .build() + socket cleanup --- .../structure/StructureDefinition.java | 152 +++++++++--------- 1 file changed, 80 insertions(+), 72 deletions(-) diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java index 15062a56..02448d04 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java @@ -57,6 +57,7 @@ public static class Builder { private char d = '\uD000'; private final Map navigates; private final Map> elements; + private final Map uncompiledShapes; private final Map shapes; private final Map> occupiedSpaces; @@ -67,6 +68,7 @@ public static class Builder { private Builder() { navigates = new HashMap<>(); elements = new HashMap<>(); + uncompiledShapes = new HashMap<>(); shapes = new HashMap<>(); occupiedSpaces = new HashMap<>(); } @@ -102,6 +104,67 @@ public Map getShapes() { * @return this builder */ public Builder addShape(String name, String[][] structurePiece) { + uncompiledShapes.put(name, structurePiece); + + return this; + } + + /** + * @deprecated use the unboxed version + */ + @Deprecated + public Builder addElement(Character name, IStructureElement structurePiece) { + elements.putIfAbsent(name, structurePiece); + return this; + } + + public Builder addElement(char name, IStructureElement structurePiece) { + elements.putIfAbsent(name, structurePiece); + 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() { + uncompiledShapes.forEach(this::compileShape); + + Map[]> structures = compileStructureMap(); + + return new StructureDefinition<>( + new HashMap<>(elements), + new HashMap<>(shapes), + structures, + occupiedSpaces, + controllers, + socketLocations); + } + + private void compileShape(String name, String[][] structurePiece) { StringBuilder builder = new StringBuilder(); if (structurePiece.length > 0) { @@ -117,6 +180,10 @@ public Builder addShape(String name, String[][] structurePiece) { builder.setLength(builder.length() - 1); } + // Always create a map for this piece, even if it doesn't have any sockets + // noinspection UnstableApiUsage + socketLocations.put(name, MultimapBuilder.hashKeys().arrayListValues().build()); + Set occupiedSpace = new HashSet<>(); // these track the global current location int aa = 0, bb = 0, cc = 0; @@ -125,6 +192,15 @@ public Builder addShape(String name, String[][] structurePiece) { // I do know these are pretty bad variable naming, but I have no better idea for (int i = 0; i < builder.length(); i++) { char ch = builder.charAt(i); + + // Translate the element char if it's a socket, and record the socket position + if (socketTranslations.containsKey(ch)) { + socketLocations.get(name).put(ch, new Position<>(aa, bb, cc)); + + ch = socketTranslations.get(ch); + builder.setCharAt(i, ch); + } + if (ch == ' ') { builder.setCharAt(i, A); ch = A; @@ -173,66 +249,14 @@ public Builder addShape(String name, String[][] structurePiece) { String built = builder.toString().replaceAll("[\\uA000\\uB000\\uC000]", ""); - if (built.contains("+") || socketTranslations.values().contains('+')) { + if (built.contains("+")) { addElement('+', notAir()); } - if (built.contains("-") || socketTranslations.values().contains('-')) { + if (built.contains("-")) { addElement('-', isAir()); } - shapes.put(name, built); - return this; - } - - /** - * @deprecated use the unboxed version - */ - @Deprecated - public Builder addElement(Character name, IStructureElement structurePiece) { - elements.putIfAbsent(name, structurePiece); - return this; - } - - public Builder addElement(char name, IStructureElement structurePiece) { - elements.putIfAbsent(name, structurePiece); - 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, - controllers, - socketLocations); + shapes.put(name, built); } @SuppressWarnings("unchecked") @@ -254,16 +278,6 @@ private Map[]> compileStructureMap() { 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; @@ -290,18 +304,12 @@ private CharOpenHashSet findMissing() { for (String shape : shapes.values()) { for (char c : shape.toCharArray()) { - if (!elements.containsKey(c) && !socketTranslations.containsKey(c)) { + if (!elements.containsKey(c)) { missing.add(c); } } } - for (char c : socketTranslations.values()) { - if (!elements.containsKey(c)) { - missing.add(c); - } - } - return missing; } } From 26b88a5f27f98a415f8868804ef1062e35d0d002 Mon Sep 17 00:00:00 2001 From: RecursivePineapple Date: Sun, 2 Nov 2025 14:41:25 -0500 Subject: [PATCH 5/5] disable spotless for new test class --- .../com/gtnewhorizon/structurelib/coords/CoordinateTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java b/src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java index cd30ea87..963611ac 100644 --- a/src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java +++ b/src/test/java/com/gtnewhorizon/structurelib/coords/CoordinateTests.java @@ -10,6 +10,7 @@ import com.gtnewhorizon.structurelib.structure.IStructureDefinition; import com.gtnewhorizon.structurelib.structure.StructureUtility; +// spotless:off class CoordinateTests { private IStructureDefinition getDefinition() { @@ -98,3 +99,4 @@ void testSocketGetters() { def.getAllSockets("main", 'Y')); } } +// spotless:on