mirror of
https://github.com/GTNewHorizons/StructureLib.git
synced 2026-11-09 15:04:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9672d40b34 | ||
|
|
4fdea65537 |
No files matched your search
@@ -3,7 +3,6 @@
|
||||
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')
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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,6 +30,7 @@ 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;
|
||||
@@ -40,6 +41,7 @@ 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;
|
||||
|
||||
@@ -53,6 +55,8 @@ 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 {
|
||||
|
||||
@@ -560,6 +564,35 @@ 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,6 +17,7 @@ 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;
|
||||
@@ -64,6 +65,8 @@ public class CommonProxy {
|
||||
|
||||
public void preInit(FMLPreInitializationEvent e) {}
|
||||
|
||||
public void init(FMLInitializationEvent e) {}
|
||||
|
||||
public long getOverworldTime() {
|
||||
return MinecraftServer.getServer().getEntityWorld().getTotalWorldTime();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ 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;
|
||||
@@ -128,6 +129,11 @@ 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);
|
||||
|
||||
+5
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizon.structurelib.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -7,7 +8,9 @@ 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 {
|
||||
@@ -44,6 +47,11 @@ public class StructureLibCore implements IFMLLoadingPlugin, IEarlyMixinLoader {
|
||||
|
||||
@Override
|
||||
public List<String> getMixins(Set<String> loadedCoreMods) {
|
||||
return Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld");
|
||||
List<String> mixins = new ArrayList<>(
|
||||
Arrays.asList("blockChangeNotifier.MixinForgeHooks", "blockChangeNotifier.MixinWorld"));
|
||||
if (FMLLaunchHandler.side() == Side.CLIENT) {
|
||||
mixins.add("MixinGuiContainer_PickChannel");
|
||||
}
|
||||
return mixins;
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
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,15 +23,22 @@ 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++) {
|
||||
@@ -41,6 +48,7 @@ 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());
|
||||
@@ -53,11 +61,11 @@ public class SetChannelDataMessage implements IMessage {
|
||||
@Override
|
||||
public IMessage onMessage(SetChannelDataMessage message, MessageContext ctx) {
|
||||
EntityPlayerMP player = ctx.getServerHandler().playerEntity;
|
||||
ItemStack heldItem = player.getHeldItem();
|
||||
if (heldItem != null && heldItem.getItem() instanceof ItemConstructableTrigger) {
|
||||
ChannelDataAccessor.wipeChannelData(heldItem);
|
||||
ItemStack target = message.onCursor ? player.inventory.getItemStack() : player.getHeldItem();
|
||||
if (target != null && target.getItem() instanceof ItemConstructableTrigger) {
|
||||
ChannelDataAccessor.wipeChannelData(target);
|
||||
for (Entry<String, Integer> e : message.data) {
|
||||
ChannelDataAccessor.setChannelData(heldItem, e.getKey(), e.getValue());
|
||||
ChannelDataAccessor.setChannelData(target, 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
|
||||
@@ -65,7 +73,7 @@ public class SetChannelDataMessage implements IMessage {
|
||||
StructureLib.LOGGER.warn(
|
||||
"{} trying to set channel data on {}, which is not a hologram projector!",
|
||||
player.getUniqueID(),
|
||||
heldItem);
|
||||
target);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -174,6 +174,37 @@ 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.<Controller>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.
|
||||
|
||||
@@ -44,7 +44,11 @@ item.structurelib.constructableTrigger.gui.info=Many multiblocks support some de
|
||||
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 to select this value for this channel
|
||||
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.autoplace.built_stat=Built %s blocks
|
||||
structurelib.autoplace.complete=§aThis multiblock has completed!
|
||||
|
||||
Reference in new issue
Block a user