Track structure controller position and add structure sockets

This commit is contained in:
RecursivePineapple committed 2025-10-28 12:34:01 -04:00
1 parent cd4f3617be
commit c3671609d3
10 files changed
+468 -36

No files matched your search

@@ -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 <C extends CoordinateSystem<C, ?>> Position<C> 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 <C extends CoordinateSystem<C, ?>> Position<C> 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());
}
}
@@ -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;
}
}
@@ -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<ControllerRelativeCoords, WorldCoords> {
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<ControllerRelativeCoords> translate(Position<WorldCoords> position) {
return translate(position, controllerX, controllerY, controllerZ);
}
@Override
public Position<WorldCoords> translateInverse(Position<ControllerRelativeCoords> position) {
return translateInverse(position, controllerX, controllerY, controllerZ);
}
public static Position<ControllerRelativeCoords> translate(Position<WorldCoords> position, int controllerX,
int controllerY, int controllerZ) {
position.sub(controllerX, controllerY, controllerZ);
return CoordinateSystem.transmute(position);
}
public static Position<WorldCoords> translateInverse(Position<ControllerRelativeCoords> position, int controllerX,
int controllerY, int controllerZ) {
position.add(controllerX, controllerY, controllerZ);
return CoordinateSystem.transmute(position);
}
}
@@ -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 <Self>
* @param <Parent>
*/
public interface CoordinateSystem<Self extends CoordinateSystem<Self, Parent>, Parent extends CoordinateSystem<Parent, ?>> {
/**
* Translates a position from the parent coordinate system into this one.
*/
Position<Self> translate(Position<Parent> position);
/**
* Translates a position from this coordinate system into the parent.
*/
Position<Parent> translateInverse(Position<Self> position);
/**
* Helper method that converts a position's coordinate system. Should not be used unless you know it's correct.
*/
static <P1 extends CoordinateSystem<P1, ?>, P2 extends CoordinateSystem<P2, ?>> Position<P1> transmute(
Position<P2> pos) {
// noinspection unchecked
return (Position<P1>) pos;
}
}
@@ -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<C extends CoordinateSystem<C, ?>> 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<C> copy() {
return new Position<>(x, y, z);
}
}
@@ -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<StructureDefinitionCoords, StructureRelativeCoords> {
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<StructureDefinitionCoords> translate(Position<StructureRelativeCoords> position) {
return translate(position, offsetX, offsetY, offsetZ);
}
@Override
public Position<StructureRelativeCoords> translateInverse(Position<StructureDefinitionCoords> position) {
return translateInverse(position, offsetX, offsetY, offsetZ);
}
public static Position<StructureDefinitionCoords> translate(Position<StructureRelativeCoords> position, int offsetX,
int offsetY, int offsetZ) {
position.add(offsetX, offsetY, offsetZ);
return CoordinateSystem.transmute(position);
}
public static Position<StructureRelativeCoords> translateInverse(Position<StructureDefinitionCoords> position,
int offsetX, int offsetY, int offsetZ) {
position.sub(offsetX, offsetY, offsetZ);
return CoordinateSystem.transmute(position);
}
}
@@ -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<StructureRelativeCoords, ControllerRelativeCoords> {
public final IntegerAxisSwap axisSwap;
public StructureRelativeCoords(IntegerAxisSwap axisSwap) {
this.axisSwap = axisSwap;
}
@Override
public Position<StructureRelativeCoords> translate(Position<ControllerRelativeCoords> position) {
position.set(axisSwap.translate(position));
return CoordinateSystem.transmute(position);
}
@Override
public Position<ControllerRelativeCoords> translateInverse(Position<StructureRelativeCoords> position) {
position.set(axisSwap.inverseTranslate(position));
return CoordinateSystem.transmute(position);
}
}
@@ -0,0 +1,21 @@
package com.gtnewhorizon.structurelib.coords;
/**
* World block coordinates
*/
public class WorldCoords implements CoordinateSystem<WorldCoords, WorldCoords> {
public static final WorldCoords INSTANCE = new WorldCoords();
private WorldCoords() {}
@Override
public Position<WorldCoords> translate(Position<WorldCoords> position) {
return position;
}
@Override
public Position<WorldCoords> translateInverse(Position<WorldCoords> position) {
return position;
}
}
@@ -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<T> {
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<StructureDefinitionCoords> 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<StructureDefinitionCoords> 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<Position<StructureDefinitionCoords>> getAllSockets(String piece, char socket);
/**
* Low level utility.
*
@@ -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<T> implements IStructureDefinition<T> {
private final Map<Character, IStructureElement<T>> elements;
private final Map<String, String> shapes;
private final Map<String, IStructureElement<T>[]> structures;
private final Map<String, Set<Vec3Impl>> occupiedSpaces;
private final Map<String, Position<StructureDefinitionCoords>> controllers;
private final Map<String, ListMultimap<Character, Position<StructureDefinitionCoords>>> socketLocations;
public static <B> Builder<B> builder() {
return new Builder<>();
}
private StructureDefinition(Map<Character, IStructureElement<T>> elements, Map<String, String> shapes,
Map<String, IStructureElement<T>[]> structures, Map<String, Set<Vec3Impl>> occupiedSpaces) {
Map<String, IStructureElement<T>[]> structures, Map<String, Set<Vec3Impl>> occupiedSpaces,
Map<String, Position<StructureDefinitionCoords>> controllers,
Map<String, ListMultimap<Character, Position<StructureDefinitionCoords>>> socketLocations) {
this.elements = elements;
this.shapes = shapes;
this.structures = structures;
this.occupiedSpaces = occupiedSpaces;
this.controllers = controllers;
this.socketLocations = socketLocations;
}
public static class Builder<T> {
@@ -44,6 +60,10 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
private final Map<String, String> shapes;
private final Map<String, Set<Vec3Impl>> occupiedSpaces;
private final Map<String, Position<StructureDefinitionCoords>> controllers = new HashMap<>();
private final Char2CharOpenHashMap socketTranslations = new Char2CharOpenHashMap();
private final Map<String, ListMultimap<Character, Position<StructureDefinitionCoords>>> socketLocations = new HashMap<>();
private Builder() {
navigates = new HashMap<>();
elements = new HashMap<>();
@@ -83,6 +103,7 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
*/
public Builder<T> 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<T> implements IStructureDefinition<T> {
}
builder.setLength(builder.length() - 1);
}
Set<Vec3Impl> occupiedSpace = new HashSet<>();
// these track the global current location
int aa = 0, bb = 0, cc = 0;
@@ -110,6 +132,7 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
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<T> implements IStructureDefinition<T> {
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<T> implements IStructureDefinition<T> {
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<T> 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<T> 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<T> build() {
Map<String, IStructureElement<T>[]> structures = compileStructureMap();
return new StructureDefinition<>(
new HashMap<>(elements),
new HashMap<>(shapes),
structures,
occupiedSpaces);
}
@SuppressWarnings("unchecked")
private Map<String, IStructureElement<T>[]> compileElementSetMap() {
Set<Integer> 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<String, IStructureElement<T>[]> compileStructureMap() {
Set<Integer> 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<String, IStructureElement<T>[]> map = new HashMap<>();
for (Entry<String, String> e : shapes.entrySet()) {
char[] chars = e.getValue().toCharArray();
IStructureElement<T>[] 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<T> 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<Integer> 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<T> implements IStructureDefinition<T> {
if (occupiedSpace == null) throw new NoSuchElementException(name);
return occupiedSpace.contains(new Vec3Impl(offsetA, offsetB, offsetC));
}
@Override
public Position<StructureDefinitionCoords> 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<StructureDefinitionCoords> 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<Position<StructureDefinitionCoords>> 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;
}
}