diff --git a/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ChannelDataAccessor.java b/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ChannelDataAccessor.java index 6b12efd3..98e501ab 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ChannelDataAccessor.java +++ b/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ChannelDataAccessor.java @@ -38,7 +38,9 @@ private ChannelDataAccessor() {} * If given subchannel does not exist, masterStack may or may not be returned to save performance. Code within * StructureLib can depend on this behavior, you cannot. * - * @param masterStack trigger item. can be the return value of another {@link #withChannel(ItemStack, String)} call + * @param masterStack trigger item. can be the return value of another + * {@link com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor#withChannel(net.minecraft.item.ItemStack, String)} + * call * @param channel subchannel to use. will use current master channel data instead if this channel does not exist * Note: all channel identifiers are supposed to be lower case and not empty. * @return a trigger item with new master channel @@ -107,13 +109,13 @@ public static int getChannelData(ItemStack masterStack, String channel) { * * @param masterStack trigger stack to check * @param channel channel identifier. Note: all channel identifiers are supposed to be lower case and not empty. - * @param data subchannel data. should always be a positive value + * @param data positive subchannel data */ public static void setChannelData(ItemStack masterStack, String channel, int data) { if (StringUtils.isEmpty(channel) || masterStack == null) throw new IllegalArgumentException(); + if (data < 1) throw new IllegalArgumentException("Channel data must be positive"); if (StructureLibAPI.isDebugEnabled() && !channel.toLowerCase(Locale.ROOT).equals(channel)) throw new IllegalArgumentException("Channel name can be lower case ONLY"); - if (data <= 0) throw new IllegalArgumentException(); if (masterStack.stackTagCompound == null) masterStack.stackTagCompound = new NBTTagCompound(); NBTTagCompound main = masterStack.stackTagCompound; if (!main.hasKey(SECONDARY_HINT_TAG, TAG_COMPOUND)) main.setTag(SECONDARY_HINT_TAG, new NBTTagCompound()); diff --git a/src/main/java/com/gtnewhorizon/structurelib/command/CommandConfigureChannels.java b/src/main/java/com/gtnewhorizon/structurelib/command/CommandConfigureChannels.java index 8fde0d7d..1fd46618 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/command/CommandConfigureChannels.java +++ b/src/main/java/com/gtnewhorizon/structurelib/command/CommandConfigureChannels.java @@ -26,7 +26,7 @@ public String getCommandName() { public String getCommandUsage(ICommandSender p_71518_1_) { return "/sl_channel \n" + "/sl_channel \n" + "/sl_channel \n" - + " can have space in it. must be positive integer"; + + " can have space in it. Values below 1 clear the channel"; } @Override @@ -108,11 +108,14 @@ public void processCommand(ICommandSender sender, String[] args) { throw new WrongUsageException("%s not a valid value", args[args.length - 1]); } - if (value <= 0) throw new WrongUsageException("%s not a valid value", args[args.length - 1]); - - ChannelDataAccessor.setChannelData(heldItem, channel, value); + if (value < 1) { + ChannelDataAccessor.unsetChannelData(heldItem, channel); + } else { + ChannelDataAccessor.setChannelData(heldItem, channel, value); + } player.inventoryContainer.detectAndSendChanges(); - sender.addChatMessage(new ChatComponentText(channel + " value: " + value)); + sender.addChatMessage( + new ChatComponentText(value < 1 ? channel + " no value" : channel + " value: " + value)); break; } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/IChannelDefinition.java b/src/main/java/com/gtnewhorizon/structurelib/structure/IChannelDefinition.java new file mode 100644 index 00000000..bd1aa34a --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/IChannelDefinition.java @@ -0,0 +1,98 @@ +package com.gtnewhorizon.structurelib.structure; + +/** + * Describes a channel used by one structure definition. + * + *

+ * Channel values are integers stored on the constructable trigger item. Implementations may override the default + * validation and normalization policy when a simple bounded range is not sufficient. + *

+ */ +public interface IChannelDefinition { + + /** + * Creates a channel with a bounded inclusive range and clamping normalization. + * + * @param channel channel identifier + * @param minimumValue inclusive minimum value + * @param maximumValue inclusive maximum value + * @return bounded channel definition + */ + static IChannelDefinition bounded(String channel, int minimumValue, int maximumValue) { + if (minimumValue < 1 || maximumValue < minimumValue) { + throw new IllegalArgumentException("Invalid channel bounds"); + } + return new IChannelDefinition() { + + @Override + public String getChannel() { + return channel; + } + + @Override + public int getMinimumValue() { + return minimumValue; + } + + @Override + public int getMaximumValue() { + return maximumValue; + } + }; + } + + /** + * Creates a channel accepting every positive integer. + * + * @param channel channel identifier + * @return unbounded channel definition + */ + static IChannelDefinition unbounded(String channel) { + return () -> channel; + } + + /** + * Returns the lower-case channel identifier. + * + * @return channel identifier + */ + String getChannel(); + + /** + * Returns the smallest value advertised by this definition. The default is {@code 1}. + * + * @return minimum value + */ + default int getMinimumValue() { + return 1; + } + + /** + * Returns the largest value advertised by this definition. The default is {@link java.lang.Integer#MAX_VALUE}. + * + * @return maximum value + */ + default int getMaximumValue() { + return Integer.MAX_VALUE; + } + + /** + * Checks whether a value is already valid without normalization. + * + * @param value value to check + * @return true when the value is valid + */ + default boolean isValueValid(int value) { + return value >= getMinimumValue() && value <= getMaximumValue(); + } + + /** + * Converts a stored value to the value used by the structure definition. + * + * @param value stored value + * @return normalized value + */ + default int normalizeValue(int value) { + return Math.max(getMinimumValue(), Math.min(getMaximumValue(), value)); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java index c95f8172..51d9d4bd 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java @@ -3,6 +3,8 @@ import static com.gtnewhorizon.structurelib.structure.IStructureWalker.ignoreBlockUnloaded; import static com.gtnewhorizon.structurelib.structure.IStructureWalker.skipBlockUnloaded; +import java.util.Collection; +import java.util.Collections; import java.util.function.Function; import net.minecraft.entity.player.EntityPlayer; @@ -56,6 +58,48 @@ */ public interface IStructureDefinition { + /** + * Returns channel definitions declared by this structure. + * + * @return an immutable or otherwise read-only channel collection + */ + default Collection getChannelDefinitions() { + return Collections.emptyList(); + } + + /** + * Returns channel definitions explicitly added by the structure builder. + * + * @return explicitly declared channel definitions + */ + default Collection getExplicitChannelDefinitions() { + return getChannelDefinitions(); + } + + /** + * Finds a channel definition by identifier. + * + * @param channel channel identifier + * @return channel definition, or {@code null} when it is not declared + */ + default IChannelDefinition getChannelDefinition(String channel) { + for (IChannelDefinition definition : getChannelDefinitions()) { + if (definition.getChannel().equals(channel)) return definition; + } + return null; + } + + /** + * Reads one channel using the standard trigger-item channel semantics. + * + * @param trigger trigger item containing channel data + * @param channel channel identifier + * @return stored channel data, or the trigger stack size when absent + */ + default int getChannelData(ItemStack trigger, String channel) { + return ChannelDataAccessor.getChannelData(trigger, channel); + } + /** * Used internally *

diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java index f3486a44..da8a5ec4 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureElement.java @@ -4,6 +4,7 @@ import static com.gtnewhorizon.structurelib.StructureLib.PANIC_MODE; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -33,6 +34,15 @@ public interface IStructureElement { boolean check(T t, World world, int x, int y, int z); + /** + * Returns the channels used by this element. + * + * @return lower-case channel identifiers + */ + default Collection getChannels() { + return Collections.emptySet(); + } + /** * Pure (stateless and side effect free) function to test if current block could be valid. Used to give user hints * about incorrectly placed blocks given a controller and trigger item. If couldBeValid(...) == false for a set of diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java index 6b97a344..8980ad5f 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureDefinition.java @@ -5,8 +5,12 @@ import static com.gtnewhorizon.structurelib.structure.StructureUtility.step; import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; @@ -19,18 +23,28 @@ public class StructureDefinition implements IStructureDefinition { private final Map> elements; private final Map shapes; private final Map[]> structures; - private final Map> occupiedSpaces; + private final Map occupiedSpaces; + private final Map channelDefinitions; + private final Set explicitChannels; + private final Collection channelDefinitionValues; + private final Collection explicitChannelDefinitions; public static Builder builder() { return new Builder<>(); } private StructureDefinition(Map> elements, Map shapes, - Map[]> structures, Map> occupiedSpaces) { + Map[]> structures, Map occupiedSpaces, + Map channelDefinitions, Set explicitChannels) { this.elements = elements; this.shapes = shapes; this.structures = structures; this.occupiedSpaces = occupiedSpaces; + this.channelDefinitions = Collections.unmodifiableMap(new HashMap<>(channelDefinitions)); + this.explicitChannels = Collections.unmodifiableSet(new HashSet<>(explicitChannels)); + this.channelDefinitionValues = Collections.unmodifiableCollection(this.channelDefinitions.values()); + this.explicitChannelDefinitions = Collections.unmodifiableList( + this.explicitChannels.stream().map(this.channelDefinitions::get).collect(Collectors.toList())); } public static class Builder { @@ -42,13 +56,17 @@ public static class Builder { private final Map navigates; private final Map> elements; private final Map shapes; - private final Map> occupiedSpaces; + private final Map occupiedSpaces; + private final Map channels; + private final Set explicitChannels; private Builder() { navigates = new HashMap<>(); elements = new HashMap<>(); shapes = new HashMap<>(); occupiedSpaces = new HashMap<>(); + channels = new HashMap<>(); + explicitChannels = new HashSet<>(); } public Map> getElements() { @@ -95,7 +113,19 @@ public Builder addShape(String name, String[][] structurePiece) { } builder.setLength(builder.length() - 1); } - Set occupiedSpace = new HashSet<>(); + int sizeA = 0; + int sizeB = 0; + long occupiedCount = 0; + for (String[] strings : structurePiece) { + sizeB = Math.max(sizeB, strings.length); + for (String string : strings) { + sizeA = Math.max(sizeA, string.length()); + for (int i = 0; i < string.length(); i++) { + if (string.charAt(i) != ' ') occupiedCount++; + } + } + } + OccupiedSpace occupiedSpace = new OccupiedSpace(sizeA, sizeB, structurePiece.length, occupiedCount); // these track the global current location int aa = 0, bb = 0, cc = 0; // these track the vec3 towards next meaningful element @@ -109,7 +139,7 @@ public Builder addShape(String name, String[][] structurePiece) { } else if (ch == '~') { builder.setCharAt(i, A); ch = A; - occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc)); + occupiedSpace.add(aa, bb, cc); } if (ch == A) { aa++; @@ -135,13 +165,13 @@ public Builder addShape(String name, String[][] structurePiece) { addElement(navigate, step(vec3)); } builder.setCharAt(i - 1, navigate); - occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc)); + occupiedSpace.add(aa, bb, cc); aa++; a = 0; b = 0; c = 0; } else { - occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc)); + occupiedSpace.add(aa, bb, cc); aa++; } } @@ -182,22 +212,61 @@ public Builder addShape(String name, String[][] structurePiece) { */ @Deprecated public Builder addElement(Character name, IStructureElement structurePiece) { - elements.putIfAbsent(name, structurePiece); + if (elements.putIfAbsent(name, structurePiece) == null) registerElementChannel(structurePiece); return this; } public Builder addElement(char name, IStructureElement structurePiece) { - elements.putIfAbsent(name, structurePiece); + if (elements.putIfAbsent(name, structurePiece) == null) registerElementChannel(structurePiece); + return this; + } + + public Builder addChannel(IChannelDefinition definition) { + validateChannelDefinition(definition); + channels.put(definition.getChannel(), definition); + explicitChannels.add(definition.getChannel()); return this; } + public Builder addChannel(String channel, int minimumValue, int maximumValue) { + return addChannel(IChannelDefinition.bounded(channel, minimumValue, maximumValue)); + } + + private void registerElementChannel(IStructureElement structurePiece) { + for (String channel : structurePiece.getChannels()) { + validateChannelName(channel); + if (!explicitChannels.contains(channel)) { + channels.putIfAbsent(channel, IChannelDefinition.unbounded(channel)); + } + } + } + + private void validateChannelDefinition(IChannelDefinition definition) { + if (definition == null) throw new IllegalArgumentException("Channel definition cannot be null"); + validateChannelName(definition.getChannel()); + if (definition.getMinimumValue() < 1 || definition.getMaximumValue() < definition.getMinimumValue()) { + throw new IllegalArgumentException("Invalid channel definition values"); + } + } + + private void validateChannelName(String channel) { + if (channel == null || channel.isEmpty() || !channel.toLowerCase(Locale.ROOT).equals(channel)) { + throw new IllegalArgumentException("Channel name must be non-empty and lower case"); + } + } + public IStructureDefinition build() { + for (IStructureElement element : elements.values()) { + registerElementChannel(element); + } Map[]> structures = compileStructureMap(); return new StructureDefinition<>( new HashMap<>(elements), new HashMap<>(shapes), structures, - occupiedSpaces); + occupiedSpaces, + channels, + explicitChannels); } @SuppressWarnings("unchecked") @@ -250,6 +319,21 @@ public Map[]> getStructures() { return structures; } + @Override + public Collection getChannelDefinitions() { + return channelDefinitionValues; + } + + @Override + public Collection getExplicitChannelDefinitions() { + return explicitChannelDefinitions; + } + + @Override + public IChannelDefinition getChannelDefinition(String channel) { + return channelDefinitions.get(channel); + } + @Override public IStructureElement[] getStructureFor(String name) { IStructureElement[] elements = structures.get(name); @@ -259,8 +343,49 @@ public IStructureElement[] getStructureFor(String name) { @Override public boolean isContainedInStructure(String name, int offsetA, int offsetB, int offsetC) { - Set occupiedSpace = occupiedSpaces.get(name); + OccupiedSpace occupiedSpace = occupiedSpaces.get(name); if (occupiedSpace == null) throw new NoSuchElementException(name); - return occupiedSpace.contains(new Vec3Impl(offsetA, offsetB, offsetC)); + return occupiedSpace.contains(offsetA, offsetB, offsetC); + } + + private static class OccupiedSpace { + + private final int sizeA; + private final int sizeB; + private final int sizeC; + private final BitSet occupied; + private final Set sparseOccupied; + + private OccupiedSpace(int sizeA, int sizeB, int sizeC, long occupiedCount) { + this.sizeA = sizeA; + this.sizeB = sizeB; + this.sizeC = sizeC; + long volume = (long) sizeA * sizeB * sizeC; + if (volume <= Integer.MAX_VALUE && volume <= Math.max(1024L, occupiedCount * 256L)) { + occupied = new BitSet((int) volume); + sparseOccupied = null; + } else { + occupied = null; + sparseOccupied = new HashSet<>(); + } + } + + private void add(int a, int b, int c) { + if (occupied != null) { + occupied.set(index(a, b, c)); + } else { + sparseOccupied.add(new Vec3Impl(a, b, c)); + } + } + + private boolean contains(int a, int b, int c) { + if (a < 0 || b < 0 || c < 0 || a >= sizeA || b >= sizeB || c >= sizeC) return false; + if (occupied != null) return occupied.get(index(a, b, c)); + return sparseOccupied.contains(new Vec3Impl(a, b, c)); + } + + private int index(int a, int b, int c) { + return (c * sizeB + b) * sizeA + a; + } } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java index a1ac478e..039e120a 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java @@ -4,12 +4,14 @@ import static com.gtnewhorizon.structurelib.StructureLib.PANIC_MODE; import static java.lang.Integer.MIN_VALUE; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -727,7 +729,7 @@ public static IStructureElement ofBlocksTiered(ITierConverter Function getter) { List descriptions = null; if (allKnownTiers != null) { - descriptions = new java.util.ArrayList<>(); + descriptions = new ArrayList<>(); for (Pair tier : allKnownTiers) { Item item = Item.getItemFromBlock(tier.getLeft()); if (item != null) { @@ -2992,7 +2994,14 @@ public static IStructureElementDeferred partitionBy(Function * See channels.md in docs folder */ public static IStructureElement withChannel(String channel, IStructureElement backing) { - return new IStructureElement() { + return new IStructureElement<>() { + + private final Collection channels = channelsFor(backing, channel); + + @Override + public Collection getChannels() { + return channels; + } public boolean check(T t, World world, int x, int y, int z) { return backing.check(t, world, x, y, z); @@ -3065,6 +3074,13 @@ public List getDescription(T context) { } }; } + + private static Collection channelsFor(IStructureElement backing, String channel) { + Set channels = new LinkedHashSet<>(); + channels.add(channel); + channels.addAll(backing.getChannels()); + return Collections.unmodifiableSet(channels); + } // endregion /** diff --git a/src/main/javadoc/overview.html b/src/main/javadoc/overview.html index deb993f2..f50f33ff 100644 --- a/src/main/javadoc/overview.html +++ b/src/main/javadoc/overview.html @@ -174,6 +174,37 @@

Channels

master channel.

+

Metadata-only channels

+ +

+ A channel does not need to represent a block or an element in a structure shape. Structure definitions can declare + metadata-only channels for values such as a widget selection or a variable structure height. These declarations are + independent of addElement and do not require a fake block or a shape character. +

+ +
+IStructureDefinition.<Controller>builder()
+        .addChannel("height", 3, 100)
+        .addChannel("length", 4, 50)
+        .addChannel(IChannelDefinition.unbounded("widget"))
+        .addElement('A', ...)
+        .build();
+
+ +

+ Channel values are still stored as integers in the trigger item's channels NBT compound. The + generic channel GUI has no active structure definition, so it edits integers and does not impose one + structure's range on every other structure using the same channel name. Structure-aware code reads values through + IStructureDefinition#getChannelData, which uses the definition's default when the subchannel is absent + and then applies that definition's local normalization policy. +

+ +

+ Existing elements wrapped with StructureUtility.withChannel are discovered automatically. An explicit + channel declaration supplies its local range and normalization policy for that structure. The same channel name may + therefore use different ranges in different structure definitions. +

+

why is this called channel?

Just give me a better name and I'd do the renaming.