Add standalone structure channel definitions (#76)

This commit is contained in:
ABKQPO authored and GitHub committed 2026-10-15 09:11:03 +08:00
1 parent 4fdea65537
commit 9672d40b34
8 files changed
+351 -22

No files matched your search

@@ -38,7 +38,9 @@ public class 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 class ChannelDataAccessor {
*
* @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());
@@ -26,7 +26,7 @@ public class CommandConfigureChannels extends CommandBase {
public String getCommandUsage(ICommandSender p_71518_1_) {
return "/sl_channel <wipe/getall>\n" + "/sl_channel <get/unset> <channel name>\n"
+ "/sl_channel <set> <channel name> <value>\n"
+ "<channel name> can have space in it. <value> must be positive integer";
+ "<channel name> can have space in it. Values below 1 clear the channel";
}
@Override
@@ -108,11 +108,14 @@ public class CommandConfigureChannels extends CommandBase {
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;
}
}
@@ -0,0 +1,98 @@
package com.gtnewhorizon.structurelib.structure;
/**
* Describes a channel used by one structure definition.
*
* <p>
* 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.
* </p>
*/
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));
}
}
@@ -3,6 +3,8 @@ package com.gtnewhorizon.structurelib.structure;
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 @@ import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing;
*/
public interface IStructureDefinition<T> {
/**
* Returns channel definitions declared by this structure.
*
* @return an immutable or otherwise read-only channel collection
*/
default Collection<IChannelDefinition> getChannelDefinitions() {
return Collections.emptyList();
}
/**
* Returns channel definitions explicitly added by the structure builder.
*
* @return explicitly declared channel definitions
*/
default Collection<IChannelDefinition> 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
* <p>
@@ -4,6 +4,7 @@ import static com.gtnewhorizon.structurelib.StructureLib.LOGGER;
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<T> {
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<String> 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
@@ -5,8 +5,12 @@ import static com.gtnewhorizon.structurelib.structure.StructureUtility.notAir;
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<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, OccupiedSpace> occupiedSpaces;
private final Map<String, IChannelDefinition> channelDefinitions;
private final Set<String> explicitChannels;
private final Collection<IChannelDefinition> channelDefinitionValues;
private final Collection<IChannelDefinition> explicitChannelDefinitions;
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, OccupiedSpace> occupiedSpaces,
Map<String, IChannelDefinition> channelDefinitions, Set<String> 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<T> {
@@ -42,13 +56,17 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
private final Map<Vec3Impl, Character> navigates;
private final Map<Character, IStructureElement<T>> elements;
private final Map<String, String> shapes;
private final Map<String, Set<Vec3Impl>> occupiedSpaces;
private final Map<String, OccupiedSpace> occupiedSpaces;
private final Map<String, IChannelDefinition> channels;
private final Set<String> explicitChannels;
private Builder() {
navigates = new HashMap<>();
elements = new HashMap<>();
shapes = new HashMap<>();
occupiedSpaces = new HashMap<>();
channels = new HashMap<>();
explicitChannels = new HashSet<>();
}
public Map<Character, IStructureElement<T>> getElements() {
@@ -95,7 +113,19 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
}
builder.setLength(builder.length() - 1);
}
Set<Vec3Impl> 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 class StructureDefinition<T> implements IStructureDefinition<T> {
} 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 class StructureDefinition<T> implements IStructureDefinition<T> {
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 class StructureDefinition<T> implements IStructureDefinition<T> {
*/
@Deprecated
public Builder<T> addElement(Character name, IStructureElement<T> structurePiece) {
elements.putIfAbsent(name, structurePiece);
if (elements.putIfAbsent(name, structurePiece) == null) registerElementChannel(structurePiece);
return this;
}
public Builder<T> addElement(char name, IStructureElement<T> structurePiece) {
elements.putIfAbsent(name, structurePiece);
if (elements.putIfAbsent(name, structurePiece) == null) registerElementChannel(structurePiece);
return this;
}
public Builder<T> addChannel(IChannelDefinition definition) {
validateChannelDefinition(definition);
channels.put(definition.getChannel(), definition);
explicitChannels.add(definition.getChannel());
return this;
}
public Builder<T> addChannel(String channel, int minimumValue, int maximumValue) {
return addChannel(IChannelDefinition.bounded(channel, minimumValue, maximumValue));
}
private void registerElementChannel(IStructureElement<T> 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<T> build() {
for (IStructureElement<T> element : elements.values()) {
registerElementChannel(element);
}
Map<String, IStructureElement<T>[]> structures = compileStructureMap();
return new StructureDefinition<>(
new HashMap<>(elements),
new HashMap<>(shapes),
structures,
occupiedSpaces);
occupiedSpaces,
channels,
explicitChannels);
}
@SuppressWarnings("unchecked")
@@ -250,6 +319,21 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
return structures;
}
@Override
public Collection<IChannelDefinition> getChannelDefinitions() {
return channelDefinitionValues;
}
@Override
public Collection<IChannelDefinition> getExplicitChannelDefinitions() {
return explicitChannelDefinitions;
}
@Override
public IChannelDefinition getChannelDefinition(String channel) {
return channelDefinitions.get(channel);
}
@Override
public IStructureElement<T>[] getStructureFor(String name) {
IStructureElement<T>[] elements = structures.get(name);
@@ -259,8 +343,49 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
@Override
public boolean isContainedInStructure(String name, int offsetA, int offsetB, int offsetC) {
Set<Vec3Impl> 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<Vec3Impl> 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;
}
}
}
@@ -4,12 +4,14 @@ import static com.gtnewhorizon.structurelib.StructureLib.LOGGER;
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 class StructureUtility {
Function<T, TIER> getter) {
List<String> descriptions = null;
if (allKnownTiers != null) {
descriptions = new java.util.ArrayList<>();
descriptions = new ArrayList<>();
for (Pair<Block, Integer> tier : allKnownTiers) {
Item item = Item.getItemFromBlock(tier.getLeft());
if (item != null) {
@@ -2992,7 +2994,14 @@ public class StructureUtility {
* See channels.md in docs folder
*/
public static <T> IStructureElement<T> withChannel(String channel, IStructureElement<T> backing) {
return new IStructureElement<T>() {
return new IStructureElement<>() {
private final Collection<String> channels = channelsFor(backing, channel);
@Override
public Collection<String> 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 class StructureUtility {
}
};
}
private static <T> Collection<String> channelsFor(IStructureElement<T> backing, String channel) {
Set<String> channels = new LinkedHashSet<>();
channels.add(channel);
channels.addAll(backing.getChannels());
return Collections.unmodifiableSet(channels);
}
// endregion
/**