Compare commits

..
27 changed files with 344 additions and 1017 deletions

No files matched your search

+1
View File
@@ -3,6 +3,7 @@
dependencies {
api("com.github.GTNewHorizons:GTNHLib:0.11.21:dev")
implementation("com.github.GTNewHorizons:NotEnoughItems:2.8.110-GTNH:dev")
testImplementation(platform('org.junit:junit-bom:5.9.2'))
testImplementation('org.junit.jupiter:junit-jupiter')
testRuntimeOnly ('org.junit.platform:junit-platform-launcher')
@@ -1,88 +0,0 @@
package com.gtnewhorizon.structurelib;
import java.util.Collection;
import java.util.Map;
import javax.annotation.Nullable;
import net.minecraft.block.Block;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.world.World;
import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor;
import com.gtnewhorizon.structurelib.net.SetChannelDataMessage;
/**
* Handler for middle-click channel picking. When the player holds a hologram projector on the cursor and middle-clicks
* a registered indicator item, the corresponding channel is set on the projector. The same applies in world when the
* projector is held in hand and a registered indicator block is middle-clicked. Shift+middle-click wipes all channels.
* Was made side neutral to support server-side validation at some point if necessary.
*/
public final class ChannelPickHandler {
private ChannelPickHandler() {}
/**
* Handle a middle-click event while holding a hologram projector on the cursor.
*/
public static boolean handleMiddleClick(EntityPlayer player, ItemStack holoStack, @Nullable ItemStack targetStack,
final boolean isSneaking) {
return handlePick(player, holoStack, targetStack, isSneaking, true);
}
public static boolean handleWorldPick(World world, EntityPlayer player, ItemStack holoStack,
@Nullable MovingObjectPosition hit, final boolean isSneaking) {
ItemStack targetStack = null;
if (hit != null) {
Block block = world.getBlock(hit.blockX, hit.blockY, hit.blockZ);
if (!block.isAir(world, hit.blockX, hit.blockY, hit.blockZ)) {
targetStack = block.getPickBlock(hit, world, hit.blockX, hit.blockY, hit.blockZ, player);
}
}
// A null target still goes through so that shift+middle-click wipes even when aiming at nothing
return handlePick(player, holoStack, targetStack, isSneaking, false);
}
private static boolean handlePick(EntityPlayer player, ItemStack holoStack, @Nullable ItemStack targetStack,
final boolean isSneaking, final boolean onCursor) {
// Clear channels on shift+middle-click. The "press again to confirm" gesture is gated client-side
// (see ChannelWipeConfirmation) before this method is ever called.
if (isSneaking) {
ChannelDataAccessor.wipeChannelData(holoStack);
sendChannelData(player, holoStack, onCursor);
player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.cleared"));
return true;
}
if (targetStack == null) return false; // nothing hovered, cancel silently
Collection<Map.Entry<String, Integer>> channels = ChannelDescription.iterate(targetStack);
if (channels.isEmpty()) {
player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.nomapping"));
return false;
}
// Assuming there's only 1 channel per item for now (same behavior as drag and drop)
Map.Entry<String, Integer> first = channels.iterator().next();
ChannelDataAccessor.setChannelData(holoStack, first.getKey(), first.getValue());
sendChannelData(player, holoStack, onCursor);
player.addChatMessage(
new ChatComponentTranslation("structurelib.pickchannel.success", first.getKey(), first.getValue()));
return true;
}
/**
* Player is the one triggering this update, so should already be in-sync.
*/
private static void sendChannelData(EntityPlayer player, ItemStack holoStack, boolean onCursor) {
if (!(player instanceof EntityPlayerMP)) {
StructureLib.net.sendToServer(new SetChannelDataMessage(holoStack, onCursor));
}
}
}
@@ -1,33 +0,0 @@
package com.gtnewhorizon.structurelib;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.ChatComponentTranslation;
/**
* Client-side-only confirmation gate for "wipe all channels" shift+middle-click gesture.
*/
public final class ChannelWipeConfirmation {
private static final long CONFIRM_WINDOW_MS = 5000;
private static long armedAtMs = -1;
private ChannelWipeConfirmation() {}
/**
* Call this before invoking {@link ChannelPickHandler} whenever the wipe gesture (shift+middle-click) is detected.
*
* @return true if this is a confirming second press within the window and the wipe should proceed now; false if
* this press only armed the confirmation.
*/
public static boolean confirm(EntityPlayer player) {
long now = Minecraft.getSystemTime();
if (now - armedAtMs > CONFIRM_WINDOW_MS) {
armedAtMs = now;
player.addChatMessage(new ChatComponentTranslation("structurelib.pickchannel.confirmwipe"));
return false;
}
armedAtMs = -1;
return true;
}
}
@@ -30,7 +30,6 @@ import net.minecraft.util.IChatComponent;
import net.minecraft.util.IIcon;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import net.minecraftforge.client.event.MouseEvent;
import net.minecraftforge.client.event.RenderWorldLastEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.ConfigElement;
@@ -41,7 +40,6 @@ import org.apache.commons.lang3.tuple.Pair;
import org.lwjgl.opengl.GL11;
import com.gtnewhorizon.structurelib.entity.fx.WeightlessParticleFX;
import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger;
import com.gtnewhorizon.structurelib.net.RegistryOrderSyncMessage;
import com.gtnewhorizon.structurelib.net.SetChannelDataMessage;
@@ -55,8 +53,6 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent;
import cpw.mods.fml.common.gameevent.TickEvent.Phase;
import cpw.mods.fml.common.network.FMLNetworkEvent;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
public class ClientProxy extends CommonProxy {
@@ -564,35 +560,6 @@ public class ClientProxy extends CommonProxy {
public static class ForgeEventHandler {
@SubscribeEvent
@SideOnly(Side.CLIENT)
public void onMouseEvent(MouseEvent event) {
final EntityPlayer player = Minecraft.getMinecraft().thePlayer;
if (player == null || player.isDead) {
return;
}
final ItemStack heldItem = player.getHeldItem();
if (heldItem == null || !(heldItem.getItem() instanceof ItemConstructableTrigger)) return;
if (event.button != 2 || !event.buttonstate) return;
// Sink the middle click as soon as a projector is held, so vanilla pick block does not run
event.setCanceled(true);
final boolean isSneaking = player.isSneaking();
if (isSneaking && !ChannelWipeConfirmation.confirm(player)) return;
ChannelPickHandler.handleWorldPick(
player.worldObj,
player,
heldItem,
Minecraft.getMinecraft().objectMouseOver,
isSneaking);
}
@SubscribeEvent
public void onEntityJoinWorld(EntityJoinWorldEvent e) {
if (e.world.isRemote && e.entity instanceof EntityClientPlayerMP) {
@@ -17,7 +17,6 @@ import net.minecraftforge.common.MinecraftForge;
import com.gtnewhorizon.structurelib.net.ErrorHintParticleMessage;
import com.gtnewhorizon.structurelib.net.UpdateHintParticleMessage;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLLoadCompleteEvent;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
@@ -65,8 +64,6 @@ public class CommonProxy {
public void preInit(FMLPreInitializationEvent e) {}
public void init(FMLInitializationEvent e) {}
public long getOverworldTime() {
return MinecraftServer.getServer().getEntityWorld().getTotalWorldTime();
}
@@ -22,7 +22,6 @@ import com.gtnewhorizon.structurelib.command.CommandConfigureChannels;
import com.gtnewhorizon.structurelib.command.CommandRegistryDebug;
import com.gtnewhorizon.structurelib.item.ItemBlockHint;
import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger;
import com.gtnewhorizon.structurelib.item.ItemDebugConstructableTrigger;
import com.gtnewhorizon.structurelib.item.ItemFrontRotationTool;
import com.gtnewhorizon.structurelib.net.AlignmentMessage;
import com.gtnewhorizon.structurelib.net.ErrorHintParticleMessage;
@@ -34,7 +33,6 @@ import com.gtnewhorizon.structurelib.util.XSTR;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.SidedProxy;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLInterModComms;
import cpw.mods.fml.common.event.FMLLoadCompleteEvent;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
@@ -100,7 +98,6 @@ public class StructureLib {
static Item itemBlockHint;
static Item itemFrontRotationTool;
static Item itemConstructableTrigger;
static Item itemDebugConstructableTrigger;
public static final CreativeTabs creativeTab = new CreativeTabs("structurelib") {
@Override
@@ -121,9 +118,6 @@ public class StructureLib {
GameRegistry.registerItem(
itemConstructableTrigger = new ItemConstructableTrigger(),
itemConstructableTrigger.getUnlocalizedName());
GameRegistry.registerItem(
itemDebugConstructableTrigger = new ItemDebugConstructableTrigger(),
itemDebugConstructableTrigger.getUnlocalizedName());
proxy.preInit(e);
NetworkRegistry.INSTANCE.registerGuiHandler(instance(), new GuiHandler());
@@ -134,11 +128,6 @@ public class StructureLib {
DevelopHelper.onPreInit();
}
@Mod.EventHandler
public void init(FMLInitializationEvent e) {
proxy.init(e);
}
@Mod.EventHandler
public void loadComplete(FMLLoadCompleteEvent e) {
proxy.loadComplete(e);
@@ -38,9 +38,7 @@ 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 com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor#withChannel(net.minecraft.item.ItemStack, String)}
* call
* @param masterStack trigger item. can be the return value of another {@link #withChannel(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
@@ -109,13 +107,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 positive subchannel data
* @param data subchannel data. should always be a positive value
*/
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());
@@ -148,12 +146,7 @@ public class ChannelDataAccessor {
*/
public static void wipeChannelData(ItemStack masterStack) {
if (masterStack == null) throw new IllegalArgumentException();
if (masterStack.stackTagCompound != null) {
masterStack.stackTagCompound.removeTag(SECONDARY_HINT_TAG);
if (masterStack.stackTagCompound.hasNoTags()) {
masterStack.stackTagCompound = null;
}
}
if (masterStack.stackTagCompound != null) masterStack.stackTagCompound.removeTag(SECONDARY_HINT_TAG);
}
/**
@@ -16,7 +16,6 @@ import com.gtnewhorizon.structurelib.StructureLib;
import com.gtnewhorizon.structurelib.StructureLibAPI;
import com.gtnewhorizon.structurelib.alignment.IAlignment;
import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing;
import com.gtnewhorizon.structurelib.item.ItemDebugConstructableTrigger;
import com.gtnewhorizon.structurelib.structure.IItemSource;
import com.gtnewhorizon.structurelib.structure.ISurvivalBuildEnvironment;
@@ -74,7 +73,7 @@ public class ConstructableUtility {
if (aPlayer instanceof EntityPlayerMP playerMP) {
// server side and sneaking (already checked above)
// do construct
if (aPlayer.capabilities.isCreativeMode || aStack.getItem() instanceof ItemDebugConstructableTrigger) {
if (aPlayer.capabilities.isCreativeMode) {
constructable.construct(aStack, false);
} else if (constructable instanceof ISurvivalConstructable) {
int built = ((ISurvivalConstructable) constructable).survivalConstruct(
@@ -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. Values below 1 clear the channel";
+ "<channel name> can have space in it. <value> must be positive integer";
}
@Override
@@ -108,14 +108,11 @@ public class CommandConfigureChannels extends CommandBase {
throw new WrongUsageException("%s not a valid value", args[args.length - 1]);
}
if (value < 1) {
ChannelDataAccessor.unsetChannelData(heldItem, channel);
} else {
ChannelDataAccessor.setChannelData(heldItem, channel, value);
}
if (value <= 0) throw new WrongUsageException("%s not a valid value", args[args.length - 1]);
ChannelDataAccessor.setChannelData(heldItem, channel, value);
player.inventoryContainer.detectAndSendChanges();
sender.addChatMessage(
new ChatComponentText(value < 1 ? channel + " no value" : channel + " value: " + value));
sender.addChatMessage(new ChatComponentText(channel + " value: " + value));
break;
}
}
@@ -1,6 +1,5 @@
package com.gtnewhorizon.structurelib.core;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -8,9 +7,7 @@ import java.util.Set;
import com.gtnewhorizon.gtnhmixins.IEarlyMixinLoader;
import cpw.mods.fml.relauncher.FMLLaunchHandler;
import cpw.mods.fml.relauncher.IFMLLoadingPlugin;
import cpw.mods.fml.relauncher.Side;
@IFMLLoadingPlugin.MCVersion("1.7.10")
public class StructureLibCore implements IFMLLoadingPlugin, IEarlyMixinLoader {
@@ -47,11 +44,6 @@ public class StructureLibCore implements IFMLLoadingPlugin, IEarlyMixinLoader {
@Override
public List<String> getMixins(Set<String> loadedCoreMods) {
List<String> mixins = new ArrayList<>(
Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld"));
if (FMLLaunchHandler.side() == Side.CLIENT) {
mixins.add("MixinGuiContainer_PickChannel");
}
return mixins;
return Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld");
}
}
@@ -1,14 +0,0 @@
package com.gtnewhorizon.structurelib.item;
import static com.gtnewhorizon.structurelib.StructureLibAPI.MOD_ID;
/**
* Hologram projector that always performs a creative build, whatever the player's game mode.
*/
public class ItemDebugConstructableTrigger extends ItemConstructableTrigger {
public ItemDebugConstructableTrigger() {
setUnlocalizedName("structurelib.debugConstructableTrigger");
setTextureName(MOD_ID + ":itemDebugConstructableTrigger");
}
}
@@ -1,64 +0,0 @@
package com.gtnewhorizon.structurelib.mixins.early;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.Slot;
import net.minecraft.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.gtnewhorizon.structurelib.ChannelPickHandler;
import com.gtnewhorizon.structurelib.ChannelWipeConfirmation;
import com.gtnewhorizon.structurelib.StructureLib;
import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger;
import codechicken.nei.NEIClientConfig;
import codechicken.nei.guihook.GuiContainerManager;
import cpw.mods.fml.common.Loader;
@Mixin(value = GuiContainer.class, priority = 100)
public abstract class MixinGuiContainer_PickChannel {
@Inject(method = "mouseClicked(III)V", at = @At("HEAD"), cancellable = true)
private void slib$onMouseClicked(int mouseX, int mouseY, int mouseButton, CallbackInfo ci) {
if (mouseButton != 2) return;
EntityPlayer player = Minecraft.getMinecraft().thePlayer;
if (player == null) return;
ItemStack cursorStack = player.inventory.getItemStack();
if (cursorStack == null || !(cursorStack.getItem() instanceof ItemConstructableTrigger)) return;
// Sink middle click as soon as projector is picked up
ci.cancel();
ItemStack hoveredStack = null;
try {
Slot slot = slib$invokeGetSlotAtPosition(mouseX, mouseY);
if (slot != null) {
hoveredStack = slot.getStack();
}
// try NEI
if (hoveredStack == null && Loader.isModLoaded("NotEnoughItems") && !NEIClientConfig.isHidden()) {
hoveredStack = GuiContainerManager.getStackMouseOver((GuiContainer) (Object) this);
}
} catch (Exception e) {
StructureLib.LOGGER.error("Error while resolving hovered slot for channel pick", e);
}
boolean isSneaking = GuiScreen.isShiftKeyDown();
if (isSneaking && !ChannelWipeConfirmation.confirm(player)) return;
ChannelPickHandler.handleMiddleClick(player, cursorStack, hoveredStack, isSneaking);
}
@Invoker("getSlotAtPosition")
protected abstract Slot slib$invokeGetSlotAtPosition(int x, int y);
}
@@ -23,22 +23,15 @@ import io.netty.buffer.ByteBuf;
public class SetChannelDataMessage implements IMessage {
private final List<Map.Entry<String, Integer>> data = new ArrayList<>();
private boolean onCursor;
public SetChannelDataMessage() {}
public SetChannelDataMessage(ItemStack trigger) {
this(trigger, false);
}
public SetChannelDataMessage(ItemStack trigger, final boolean onCursor) {
this.onCursor = onCursor;
ChannelDataAccessor.iterateChannelData(trigger).forEach(data::add);
}
@Override
public void fromBytes(ByteBuf buf) {
onCursor = buf.readBoolean();
data.clear();
int size = ByteBufUtils.readVarShort(buf);
for (int i = 0; i < size; i++) {
@@ -48,7 +41,6 @@ public class SetChannelDataMessage implements IMessage {
@Override
public void toBytes(ByteBuf buf) {
buf.writeBoolean(onCursor);
ByteBufUtils.writeVarShort(buf, data.size());
for (Entry<String, Integer> e : data) {
ByteBufUtils.writeUTF8String(buf, e.getKey());
@@ -61,11 +53,11 @@ public class SetChannelDataMessage implements IMessage {
@Override
public IMessage onMessage(SetChannelDataMessage message, MessageContext ctx) {
EntityPlayerMP player = ctx.getServerHandler().playerEntity;
ItemStack target = message.onCursor ? player.inventory.getItemStack() : player.getHeldItem();
if (target != null && target.getItem() instanceof ItemConstructableTrigger) {
ChannelDataAccessor.wipeChannelData(target);
ItemStack heldItem = player.getHeldItem();
if (heldItem != null && heldItem.getItem() instanceof ItemConstructableTrigger) {
ChannelDataAccessor.wipeChannelData(heldItem);
for (Entry<String, Integer> e : message.data) {
ChannelDataAccessor.setChannelData(target, e.getKey(), e.getValue());
ChannelDataAccessor.setChannelData(heldItem, e.getKey(), e.getValue());
}
// since this is a set all channel request from the client, we would assume client already know
// what this would look like on the client, so no sync
@@ -73,7 +65,7 @@ public class SetChannelDataMessage implements IMessage {
StructureLib.LOGGER.warn(
"{} trying to set channel data on {}, which is not a hologram projector!",
player.getUniqueID(),
target);
heldItem);
}
return null;
}
@@ -1,98 +0,0 @@
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,8 +3,6 @@ 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;
@@ -58,48 +56,6 @@ 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>
@@ -111,7 +67,7 @@ public interface IStructureDefinition<T> {
* @throws java.util.NoSuchElementException if the given structure piece is not found and the moon phase perfectly
* matches
*/
IStructureElement<T>[] getStructureFor(String name);
IStructureElement<? super T>[] getStructureFor(String name);
boolean isContainedInStructure(String name, int offsetA, int offsetB, int offsetC);
@@ -400,7 +356,7 @@ public interface IStructureDefinition<T> {
* @return true if iteration completed successfully.
* @param <T> type of context object.
*/
static <T> boolean iterate(T object, ItemStack trigger, IStructureElement<T>[] elements, World world,
static <T> boolean iterate(T object, ItemStack trigger, IStructureElement<? super T>[] elements, World world,
ExtendedFacing extendedFacing, int basePositionX, int basePositionY, int basePositionZ, int basePositionA,
int basePositionB, int basePositionC, boolean hintsOnly, Boolean checkBlocksIfNotNullForceCheckAllIfTrue) {
if (!world.isRemote && hintsOnly) {
@@ -4,7 +4,6 @@ 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;
@@ -29,20 +28,14 @@ import com.gtnewhorizon.structurelib.util.ItemStackPredicate.NBTMode;
/**
* Use StructureUtility to instantiate. These are the building blocks for your {@link IStructureDefinition}. It
* represents what a particular element can be and how this particular element should be autoplaced.
*
* @param <T> Type of the context object. {@link IStructureElement} is contravariant over T, so parameters and fields of
* type {@link IStructureElement} should be declared as {@code IStructureElement<? super T>}
*/
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
@@ -223,7 +216,7 @@ public interface IStructureElement<T> {
* Forget the messed up class dependency graph for now. this is just so convenient.
*/
default IStructureElementNoPlacement<T> noPlacement() {
return new IStructureElementNoPlacement<T>() {
return new IStructureElementNoPlacement<>() {
@Override
public boolean check(T t, World world, int x, int y, int z) {
@@ -21,11 +21,11 @@ import com.google.common.collect.Iterables;
*/
public interface IStructureElementChain<T> extends IStructureElement<T> {
IStructureElement<T>[] fallbacks();
IStructureElement<? super T>[] fallbacks();
@Override
default boolean check(T t, World world, int x, int y, int z) {
for (IStructureElement<T> fallback : fallbacks()) {
for (IStructureElement<? super T> fallback : fallbacks()) {
if (fallback.check(t, world, x, y, z)) {
return true;
}
@@ -35,7 +35,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
@Override
default boolean couldBeValid(T t, World world, int x, int y, int z, ItemStack trigger) {
for (IStructureElement<T> fallback : fallbacks()) {
for (IStructureElement<? super T> fallback : fallbacks()) {
if (fallback.couldBeValid(t, world, x, y, z, trigger)) {
return true;
}
@@ -45,7 +45,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
@Override
default boolean spawnHint(T t, World world, int x, int y, int z, ItemStack trigger) {
for (IStructureElement<T> fallback : fallbacks()) {
for (IStructureElement<? super T> fallback : fallbacks()) {
if (fallback.spawnHint(t, world, x, y, z, trigger)) {
return true;
}
@@ -55,7 +55,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
@Override
default boolean placeBlock(T t, World world, int x, int y, int z, ItemStack trigger) {
for (IStructureElement<T> fallback : fallbacks()) {
for (IStructureElement<? super T> fallback : fallbacks()) {
if (fallback.placeBlock(t, world, x, y, z, trigger)) {
return true;
}
@@ -67,7 +67,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
@Override
default List<String> getDescription(T context) {
Set<String> descriptions = new LinkedHashSet<>();
for (IStructureElement<T> fallback : fallbacks()) {
for (IStructureElement<? super T> fallback : fallbacks()) {
List<String> desc = fallback.getDescription(context);
if (desc != null) {
descriptions.addAll(desc);
@@ -82,7 +82,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
AutoPlaceEnvironment env) {
Predicate<ItemStack> predicate = null;
List<Iterable<ItemStack>> is = new ArrayList<>();
for (IStructureElement<T> fallback : fallbacks()) {
for (IStructureElement<? super T> fallback : fallbacks()) {
BlocksToPlace e = fallback.getBlocksToPlace(t, world, x, y, z, trigger, env);
if (e == null) continue;
if (predicate == null) predicate = e.getPredicate();
@@ -99,7 +99,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
EntityPlayerMP actor, Consumer<IChatComponent> chatter) {
boolean allContinue = true;
List<IChatComponent> bufferedNoise = new ArrayList<>();
for (IStructureElement<T> fallback : fallbacks()) {
for (IStructureElement<? super T> fallback : fallbacks()) {
PlaceResult result = fallback.survivalPlaceBlock(t, world, x, y, z, trigger, s, actor, bufferedNoise::add);
switch (result) {
case REJECT_CONTINUE:
@@ -124,7 +124,7 @@ public interface IStructureElementChain<T> extends IStructureElement<T> {
AutoPlaceEnvironment env) {
boolean allContinue = true;
List<IChatComponent> bufferedNoise = new ArrayList<>();
for (IStructureElement<T> fallback : fallbacks()) {
for (IStructureElement<? super T> fallback : fallbacks()) {
PlaceResult result = fallback
.survivalPlaceBlock(t, world, x, y, z, trigger, env.withChatter(bufferedNoise::add));
switch (result) {
@@ -10,36 +10,36 @@ import net.minecraft.world.World;
/**
* Use StructureUtility to instantiate
*/
interface IStructureNavigate<T> extends IStructureElement<T> {
interface IStructureNavigate extends IStructureElement<Object> {
@Override
default boolean check(T t, World world, int x, int y, int z) {
default boolean check(Object t, World world, int x, int y, int z) {
return true;
}
@Override
default boolean couldBeValid(T t, World world, int x, int y, int z, ItemStack trigger) {
default boolean couldBeValid(Object t, World world, int x, int y, int z, ItemStack trigger) {
return true;
}
@Override
default boolean spawnHint(T t, World world, int x, int y, int z, ItemStack trigger) {
default boolean spawnHint(Object t, World world, int x, int y, int z, ItemStack trigger) {
return true;
}
@Override
default boolean placeBlock(T t, World world, int x, int y, int z, ItemStack trigger) {
default boolean placeBlock(Object t, World world, int x, int y, int z, ItemStack trigger) {
return true;
}
@Override
default PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger, IItemSource s,
default PlaceResult survivalPlaceBlock(Object t, World world, int x, int y, int z, ItemStack trigger, IItemSource s,
EntityPlayerMP actor, Consumer<IChatComponent> chatter) {
return PlaceResult.SKIP;
}
@Override
default PlaceResult survivalPlaceBlock(T t, World world, int x, int y, int z, ItemStack trigger,
default PlaceResult survivalPlaceBlock(Object t, World world, int x, int y, int z, ItemStack trigger,
AutoPlaceEnvironment env) {
return PlaceResult.SKIP;
}
@@ -21,40 +21,42 @@ public interface IStructureWalker<T> {
* @param c The relative C position of the element in structure-local coordinates (pre transform)
* @return False to stop iterating, true to continue.
*/
boolean visit(IStructureElement<T> element, World world, int x, int y, int z, int a, int b, int c);
boolean visit(IStructureElement<? super T> element, World world, int x, int y, int z, int a, int b, int c);
default boolean blockNotLoaded(IStructureElement<T> element, World world, int x, int y, int z, int a, int b,
default boolean blockNotLoaded(IStructureElement<? super T> element, World world, int x, int y, int z, int a, int b,
int c) {
return false;
}
static <T> IStructureWalker<T> ignoreBlockUnloaded(IStructureWalker<T> walker) {
return new IStructureWalker<T>() {
return new IStructureWalker<>() {
@Override
public boolean visit(IStructureElement<T> element, World world, int x, int y, int z, int a, int b, int c) {
public boolean visit(IStructureElement<? super T> element, World world, int x, int y, int z, int a, int b,
int c) {
return walker.visit(element, world, x, y, z, a, b, c);
}
@Override
public boolean blockNotLoaded(IStructureElement<T> element, World world, int x, int y, int z, int a, int b,
int c) {
public boolean blockNotLoaded(IStructureElement<? super T> element, World world, int x, int y, int z, int a,
int b, int c) {
return walker.visit(element, world, x, y, z, a, b, c);
}
};
}
static <T> IStructureWalker<T> skipBlockUnloaded(IStructureWalker<T> walker) {
return new IStructureWalker<T>() {
return new IStructureWalker<>() {
@Override
public boolean visit(IStructureElement<T> element, World world, int x, int y, int z, int a, int b, int c) {
public boolean visit(IStructureElement<? super T> element, World world, int x, int y, int z, int a, int b,
int c) {
return walker.visit(element, world, x, y, z, a, b, c);
}
@Override
public boolean blockNotLoaded(IStructureElement<T> element, World world, int x, int y, int z, int a, int b,
int c) {
public boolean blockNotLoaded(IStructureElement<? super T> element, World world, int x, int y, int z, int a,
int b, int c) {
return true;
}
};
@@ -13,14 +13,14 @@ import net.minecraft.world.World;
class LazyStructureElement<T> implements IStructureElementDeferred<T> {
private Function<T, IStructureElement<T>> to;
private IStructureElement<T> elem;
private Function<? super T, ? extends IStructureElement<? super T>> to;
private IStructureElement<? super T> elem;
public LazyStructureElement(Function<T, IStructureElement<T>> to) {
public LazyStructureElement(Function<? super T, ? extends IStructureElement<? super T>> to) {
this.to = to;
}
private IStructureElement<T> get(T t) {
private IStructureElement<? super T> get(T t) {
if (to != null) {
elem = to.apply(t);
to = null;
@@ -5,12 +5,8 @@ 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;
@@ -20,31 +16,21 @@ import com.gtnewhorizon.structurelib.util.Vec3Impl;
public class StructureDefinition<T> implements IStructureDefinition<T> {
private final Map<Character, IStructureElement<T>> elements;
private final Map<Character, IStructureElement<? super T>> elements;
private final Map<String, String> shapes;
private final Map<String, IStructureElement<T>[]> structures;
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;
private final Map<String, IStructureElement<? super T>[]> structures;
private final Map<String, Set<Vec3Impl>> occupiedSpaces;
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, OccupiedSpace> occupiedSpaces,
Map<String, IChannelDefinition> channelDefinitions, Set<String> explicitChannels) {
private StructureDefinition(Map<Character, IStructureElement<? super T>> elements, Map<String, String> shapes,
Map<String, IStructureElement<? super T>[]> structures, Map<String, Set<Vec3Impl>> occupiedSpaces) {
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> {
@@ -54,22 +40,18 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
private static final char C = '\uC000';
private char d = '\uD000';
private final Map<Vec3Impl, Character> navigates;
private final Map<Character, IStructureElement<T>> elements;
private final Map<Character, IStructureElement<? super T>> elements;
private final Map<String, String> shapes;
private final Map<String, OccupiedSpace> occupiedSpaces;
private final Map<String, IChannelDefinition> channels;
private final Set<String> explicitChannels;
private final Map<String, Set<Vec3Impl>> occupiedSpaces;
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() {
public Map<Character, IStructureElement<? super T>> getElements() {
return elements;
}
@@ -113,19 +95,7 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
}
builder.setLength(builder.length() - 1);
}
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);
Set<Vec3Impl> occupiedSpace = new HashSet<>();
// these track the global current location
int aa = 0, bb = 0, cc = 0;
// these track the vec3 towards next meaningful element
@@ -139,7 +109,7 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
} else if (ch == '~') {
builder.setCharAt(i, A);
ch = A;
occupiedSpace.add(aa, bb, cc);
occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc));
}
if (ch == A) {
aa++;
@@ -165,13 +135,13 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
addElement(navigate, step(vec3));
}
builder.setCharAt(i - 1, navigate);
occupiedSpace.add(aa, bb, cc);
occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc));
aa++;
a = 0;
b = 0;
c = 0;
} else {
occupiedSpace.add(aa, bb, cc);
occupiedSpace.add(Vec3Impl.getFromPool(aa, bb, cc));
aa++;
}
}
@@ -211,66 +181,27 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
* @deprecated use the unboxed version
*/
@Deprecated
public Builder<T> addElement(Character name, IStructureElement<T> structurePiece) {
if (elements.putIfAbsent(name, structurePiece) == null) registerElementChannel(structurePiece);
public Builder<T> addElement(Character name, IStructureElement<? super T> structurePiece) {
elements.putIfAbsent(name, structurePiece);
return this;
}
public Builder<T> addElement(char name, IStructureElement<T> structurePiece) {
if (elements.putIfAbsent(name, structurePiece) == null) registerElementChannel(structurePiece);
public Builder<T> addElement(char name, IStructureElement<? super T> structurePiece) {
elements.putIfAbsent(name, 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();
Map<String, IStructureElement<? super T>[]> structures = compileStructureMap();
return new StructureDefinition<>(
new HashMap<>(elements),
new HashMap<>(shapes),
structures,
occupiedSpaces,
channels,
explicitChannels);
occupiedSpaces);
}
@SuppressWarnings("unchecked")
private Map<String, IStructureElement<T>[]> compileElementSetMap() {
private Map<String, IStructureElement<? super T>[]> compileElementSetMap() {
Set<Integer> missing = findMissing();
if (missing.isEmpty()) {
return shapes.entrySet().stream().collect(
@@ -286,7 +217,7 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
}
@SuppressWarnings("unchecked")
private Map<String, IStructureElement<T>[]> compileStructureMap() {
private Map<String, IStructureElement<? super T>[]> compileStructureMap() {
Set<Integer> missing = findMissing();
if (missing.isEmpty()) {
return shapes.entrySet().stream().collect(
@@ -307,7 +238,7 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
}
}
public Map<Character, IStructureElement<T>> getElements() {
public Map<Character, IStructureElement<? super T>> getElements() {
return elements;
}
@@ -315,77 +246,21 @@ public class StructureDefinition<T> implements IStructureDefinition<T> {
return shapes;
}
public Map<String, IStructureElement<T>[]> getStructures() {
public Map<String, IStructureElement<? super T>[]> getStructures() {
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);
public IStructureElement<? super T>[] getStructureFor(String name) {
IStructureElement<? super T>[] 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) {
OccupiedSpace occupiedSpace = occupiedSpaces.get(name);
Set<Vec3Impl> occupiedSpace = occupiedSpaces.get(name);
if (occupiedSpace == null) throw new NoSuchElementException(name);
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;
}
return occupiedSpace.contains(new Vec3Impl(offsetA, offsetB, offsetC));
}
}
File diff suppressed because it is too large. Load diff
@@ -37,7 +37,7 @@ class SurvivalBuildStructureWalker<T> implements IStructureWalker<T> {
}
@Override
public boolean visit(IStructureElement<T> element, World world, int x, int y, int z, int a, int b, int c) {
public boolean visit(IStructureElement<? super T> element, World world, int x, int y, int z, int a, int b, int c) {
env.offsetABC[0] = a;
env.offsetABC[1] = b;
env.offsetABC[2] = c;
@@ -1,64 +0,0 @@
package com.gtnewhorizon.structurelib.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NavigableMap;
import java.util.Spliterator;
import java.util.TreeMap;
import java.util.function.Consumer;
public class SortedRegistry<V> implements Iterable<V> {
private final NavigableMap<String, V> store = new TreeMap<>();
/**
* Store stuff in an array for faster iteration access than a red black tree
*/
private List<V> baked = Collections.emptyList();
public void register(String key, V val) {
if (key == null || val == null) throw new NullPointerException();
V old = store.putIfAbsent(key, val);
if (old != null) {
throw new IllegalArgumentException("Duplicate key: " + key);
}
// TODO allow a user to edit the order with a configuration file
baked = new ArrayList<>(store.values());
}
public int size() {
return store.size();
}
public boolean isEmpty() {
return store.isEmpty();
}
public boolean containsKey(String key) {
return store.containsKey(key);
}
public boolean containsValue(V value) {
return store.containsValue(value);
}
public V get(String key) {
return store.get(key);
}
@Override
public Iterator<V> iterator() {
return baked.iterator();
}
@Override
public void forEach(Consumer<? super V> action) {
baked.forEach(action);
}
@Override
public Spliterator<V> spliterator() {
return baked.spliterator();
}
}
-31
View File
@@ -174,37 +174,6 @@ StructureLib API documentation.
master channel.
</p>
<h3>Metadata-only channels</h3>
<p>
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 <code>addElement</code> and do not require a fake block or a shape character.
</p>
<pre>
IStructureDefinition.&lt;Controller&gt;builder()
.addChannel("height", 3, 100)
.addChannel("length", 4, 50)
.addChannel(IChannelDefinition.unbounded("widget"))
.addElement('A', ...)
.build();
</pre>
<p>
Channel values are still stored as integers in the trigger item's <code>channels</code> 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
<code>IStructureDefinition#getChannelData</code>, which uses the definition's default when the subchannel is absent
and then applies that definition's local normalization policy.
</p>
<p>
Existing elements wrapped with <code>StructureUtility.withChannel</code> 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.
</p>
<h3>why is this called channel?</h3>
Just give me a better name and I'd do the renaming.
@@ -41,16 +41,10 @@ item.structurelib.constructableTrigger.gui.wipe.confirm=§4Sure?
item.structurelib.constructableTrigger.gui.channels.from=§rMod: §9§o%s
item.structurelib.constructableTrigger.gui.info=Many multiblocks support some degree of variation in their structure and a sub channel allows indication of which variant to use.\n All sub channels have a sequence of lowercase characters as key and a positive integer as value. Most known sub channels should be available as typeahead completions.\n You can also drag and drop items from NEI to set the corresponding sub channel value. These items all have a tooltip of "§oIndicates§r §ovalue§r §oX§r §ofor§r §osub§r §ochannel§r §oY§r".
item.structurelib.debugConstructableTrigger.name=Debug Multiblock Structure Hologram Projector
channels.structurelib.show_errors=When set, will highlight parts of the multiblock that is definitely wrong
structurelib.tooltip.channelvalue=§7Value §b%d§r§7 for subchannel §b%s
structurelib.tooltip.indicator_dnd=Drop into GUI or middle-click to select this value for this channel
structurelib.pickchannel.success=§aSet §b%s§r§a to §b%d§r
structurelib.pickchannel.cleared=§aAll channels cleared§r
structurelib.pickchannel.confirmwipe=§6Sneak+Middle-click again to clear all channels
structurelib.pickchannel.nomapping=§cNo channel mapping found for this item
structurelib.tooltip.indicator_dnd=Drop into GUI to select this value for this channel
structurelib.autoplace.built_stat=Built %s blocks
structurelib.autoplace.complete=§aThis multiblock has completed!
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB