package com.gtnewhorizon.structurelib.structure; 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.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 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> 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 { private static final char A = '\uA000'; private static final char B = '\uB000'; private static final char C = '\uC000'; private char d = '\uD000'; private final Map navigates; private final Map> elements; private final Map uncompiledShapes; 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<>(); uncompiledShapes = new HashMap<>(); shapes = new HashMap<>(); occupiedSpaces = new HashMap<>(); } public Map> getElements() { return elements; } public Map getShapes() { return shapes; } /** * Adds shape. Shape is a two-dimensional string array. Each character inside any of these strings will * be later mapped to a particular type of {@link IStructureElement} as indicated by * {@link #addElement(Character, IStructureElement)}. *

* next char is next block(a) next string is next line(b) next string[] is next slice(c) *

* There are a few special reserved characters that are pre-mapped to some common structure elements *

    *
  • + is anything but air
  • *
  • - is air checks
  • *
  • space bar is skip
  • *
  • ~ is also skip (but marks controller position, not optional)
  • *
  • char {@code A000-FFFF} range is reserved for generated skips
  • *
*

* * @param name unlocalized/code name * @param structurePiece generated or written struct - DO NOT STORE IT ANYWHERE, or at least set them to null * afterwards * @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 Builder compileShape(String name, String[][] structurePiece) { StringBuilder builder = new StringBuilder(); if (structurePiece.length > 0) { for (String[] strings : structurePiece) { if (strings.length > 0) { for (String string : strings) { builder.append(string).append(B); } builder.setLength(builder.length() - 1); } builder.append(C); } 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; // these track the vec3 towards next meaningful element int a = 0, b = 0, c = 0; // 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; } else if (ch == '~') { 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++; a++; } else if (ch == B) { aa = 0; a = 0; bb++; b++; } else if (ch == C) { aa = 0; bb = 0; a = 0; b = 0; c++; cc++; } else if (a != 0 || b != 0 || c != 0) { Vec3Impl vec3 = new Vec3Impl(a, b, c); Character navigate = navigates.get(vec3); if (navigate == null) { navigate = d++; navigates.put(vec3, navigate); addElement(navigate, step(vec3)); } builder.setCharAt(i - 1, navigate); occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc)); aa++; a = 0; b = 0; c = 0; } else { occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc)); aa++; } } occupiedSpaces.put(name, occupiedSpace); boolean hasPlus = false; boolean hasMinus = false; int write = 0; for (int read = 0; read < builder.length(); read++) { char ch = builder.charAt(read); if (ch == A || ch == B || ch == C) { continue; } if (ch == '+') { hasPlus = true; } else if (ch == '-') { hasMinus = true; } if (read != write) builder.setCharAt(write, ch); write++; } builder.setLength(write); if (hasPlus) { addElement('+', notAir()); } if (hasMinus) { addElement('-', isAir()); } shapes.put(name, builder.toString()); return this; } @SuppressWarnings("unchecked") private Map[]> compileStructureMap() { 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]; 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 CharOpenHashSet findMissing() { CharOpenHashSet missing = new CharOpenHashSet(); for (String shape : shapes.values()) { for (char c : shape.toCharArray()) { if (!elements.containsKey(c)) { missing.add(c); } } } return missing; } } public Map> getElements() { return elements; } public Map getShapes() { return shapes; } public Map[]> getStructures() { return structures; } @Override public IStructureElement[] getStructureFor(String name) { IStructureElement[] elements = structures.get(name); if (elements == null) throw new NoSuchElementException(name); return elements; } @Override public boolean isContainedInStructure(String name, int offsetA, int offsetB, int offsetC) { Set occupiedSpace = occupiedSpaces.get(name); 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; } }