diff --git a/build.gradle b/build.gradle index 4c2b5f59..62067b74 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ plugins { id 'com.palantir.git-version' version '0.13.0' apply false id 'de.undercouch.download' version '5.0.1' id 'com.github.gmazzo.buildconfig' version '3.0.3' apply false - id "com.diffplug.spotless" version "6.7.2" + id 'com.diffplug.spotless' version '6.7.2' apply false } verifySettingsGradle() @@ -90,7 +90,13 @@ idea { downloadSources = true } } -apply from: Blowdryer.file('spotless.gradle') + +boolean disableSpotless = project.hasProperty("disableSpotless") ? project.disableSpotless.toBoolean() : false + +if (!disableSpotless) { + apply plugin: 'com.diffplug.spotless' + apply from: Blowdryer.file('spotless.gradle') +} if(JavaVersion.current() != JavaVersion.VERSION_1_8) { throw new GradleException("This project requires Java 8, but it's running on " + JavaVersion.current()) diff --git a/dependencies.gradle b/dependencies.gradle index f20c9900..f129e37c 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -2,4 +2,5 @@ dependencies { compile("com.github.GTNewHorizons:GTNHLib:0.0.1:dev") + runtime("com.github.GTNewHorizons:NotEnoughItems:2.2.20-GTNH:dev") } diff --git a/docs/channels.md b/docs/channels.md new file mode 100644 index 00000000..f83a6b8f --- /dev/null +++ b/docs/channels.md @@ -0,0 +1,72 @@ +# Constructable Tool + +The convention is to use the stack size of tool stack to pass tier info. + +## Channels + +When one multi have multiple tiered component that can have interleaving tier, this becomes an obnoxious restriction, as +one ItemStack cannot have two stack sizes at the same time. +Here we use the item NBT to bypass this restriction. + +Basically, one hologram can contain multiple channels. +Each channel can be identified with lower case strings and has the value being one positive Java int. +There will be one and only one master channel and 0 or more sub channel. + +When a non-existent sub channel is queried, master channel will be used instead to fill its place. + +Structure element designers will usually not need to consider channels, i.e. proceed as if this concept does not exist, unless it must take input from multiple channels. + +Multi Designers will consider how to map each channel to each tiered component. +He will be using `com.gtnewhorizon.structurelib.structure.StructureUtility.withChannel` to direct structure element on which channel to use. +Conceptually, it will return an ItemStack with the master channel being the value of selected channel. +In implementation, withChannel will return a new ItemStack with same item, nbt but different stack size. + +In any case, it is NOT safe to mutate or query this ItemStack beyond its stack size. +What information this carries (beyond the stack size) is an implementation detail and should not be relied on. +__For the future of mankind please do not rely on this.__ + +### why is this called channel? + +IDK man, just give me a better name and I'd do the renaming. + +### Example: Multiblock Structure Designer + +GT++ Chemical Plant is a multi with 4 distinctly tiered component. + +It will use +* master channel for its outer shell +* channel `"casing"` for machine casings +* channel `"pipe"` for pipe casings +* channel `"coil"` for heating coils + +Its structure code will then look like +```java +.addElement('a', shellStructureElement()) +.addElement('b', withChannel("casing", machineCasingStructureElement())) +.addElement('c', withChannel("pipe", pipeCasingStructureElement())) +.addElement('d', withChannel("coil", coilStructureElement())) +``` + +whereas all these structure elements are designed without considering channels whatsoever + +### Example: Multi channel input Structure Element + +Suppose your hatch element has a required facing and can accept arbitrary meta tile entity, so it will need both meta id and facing input. + + +It will use +* master channel for its outer shell +* channel `"casing"` for machine casings +* channel `"pipe"` for pipe casings +* channel `"coil"` for heating coils + +Its structure code will then look like +```java +.addElement('a', shellStructureElement()) +.addElement('b', withChannel("casing", machineCasingStructureElement())) +.addElement('c', withChannel("pipe", pipeCasingStructureElement())) +.addElement('d', withChannel("coil", coilStructureElement())) +``` + +whereas all these structure elements are designed without considering channels whatsoever + diff --git a/docs/walkthrough.md b/docs/walkthrough.md new file mode 100644 index 00000000..b3f60a6f --- /dev/null +++ b/docs/walkthrough.md @@ -0,0 +1,62 @@ +# StructureLib Developer Guide v0.1-SNAPSHOT + +## `IStructureDefinition` + +`IStructureDefinition` is the structure definition of your multi. You will have one `IStructureDefinition` for each multi. + +### Caching? + +You will construct this definition once per multi, either at class load or at first call to `getStructureDefinition()`. +You can use a `ClassValue>` if you have a template for subclass. +You can also use `StructureUtility.defer()` for simpler cases, but it is slightly less performant as it will allocate new +objects on each structure check, whereas `ClassValue` will allocate once and cache it onwards. + +### Constructing + +IStructureDefinition is constructed using `IStructureDefinition#builder()`. + +Builder has two main methods +1. `addShape` is a 2d string array describing the layout of structure. You can optionally use `StructureUtility#transpose()` +to transpose the array to make the structure look nicer in source code. +2. `addElement`. It takes a char identifier (mapped to a char in any of `addShape()` calls) and a IStructureElement. + +## `IStructureElement` + +`IStructureElement` are how you define which block will be accepted. +They are simple stateless classes. They will rely on the multiblock controller to store any states they need. +If you don't want to store these transient states in multiblock controller, you can use `StructureUtility#withContext()` + +They will usually come from `StructureUtility`. +There is a vast library of `IStructureElement` there to cover basic needs. +For more complex usage, you can define your own subclass, e.g. GregTech 5 Unofficial defines `ofHatchAdder()`, `ofCoil()`, +`ofFrame()` and `buildHatchAdder()` in its `GT_StructureUtility` class. + +Here we will detail a few more complex `IStructureElement` that comes with standard library. + +### `ofBlocksTiered` + +This assumes you have a series of block, e.g. T1 motor, T2 motor, etc. Your multi will accept any of them (__as long as +player does not mix them (*)__), but would like to know which is used. + +(*): Implementation wise, this structure element will only accept a block if its tier is the same as existing tier or +if existing tier is unset (i.e. value of third argument) + +The first argument is tier extractor. +You will need to provide a function to convert block+meta to a tier. +This tier cam be an Enum, an Integer, a String, or basically anything. +It doesn't even have to implement `Comparable`. +The choice is yours. +The extractor can return null and will never be passed a null block or an invalid block meta. + +The second argument is a list of all blocks you know that you will accept. +This list is only for hint particle spawning and survival build. +The hint/autoplace code will choose 1st pair to spawn/place if hologram has [channel data](./channels.md) of 1, +2nd pair if 2, and so on. + +The third argument is the value when the tier has not been set by structure code. +This means getter (5th argument) will usually return this upon first invocation for each round of structure check. + +The fourth argument will notify the multi of a tier change. + +The fifth argument is how multi notify the structure element about current tier. +This is necessary as all structure element are stateless themselves. diff --git a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java index 351aea87..e253cb9a 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java +++ b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java @@ -3,6 +3,7 @@ package com.gtnewhorizon.structurelib; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.gtnewhorizon.structurelib.entity.fx.EntityFXBlockHint; +import com.gtnewhorizon.structurelib.net.SetChannelDataMessage; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import java.util.ArrayList; @@ -14,6 +15,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiNewChat; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; import net.minecraft.util.ChatComponentText; import net.minecraft.util.IIcon; import net.minecraft.world.World; @@ -149,6 +151,11 @@ public class ClientProxy extends CommonProxy { return Minecraft.getMinecraft().theWorld.getTotalWorldTime(); } + @Override + public void uploadChannels(ItemStack trigger) { + StructureLib.net.sendToServer(new SetChannelDataMessage(trigger)); + } + private static class HintParticleInfo { private final int x, y, z; private final List owner; diff --git a/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java b/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java index 0751b4ba..283ae93a 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java +++ b/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java @@ -5,6 +5,7 @@ import cpw.mods.fml.common.event.FMLPreInitializationEvent; 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.server.MinecraftServer; import net.minecraft.util.IIcon; import net.minecraft.world.World; @@ -48,4 +49,6 @@ public class CommonProxy { public long getOverworldTime() { return MinecraftServer.getServer().getEntityWorld().getTotalWorldTime(); } + + public void uploadChannels(ItemStack trigger) {} } diff --git a/src/main/java/com/gtnewhorizon/structurelib/GuiHandler.java b/src/main/java/com/gtnewhorizon/structurelib/GuiHandler.java new file mode 100644 index 00000000..ec13d349 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/GuiHandler.java @@ -0,0 +1,23 @@ +package com.gtnewhorizon.structurelib; + +import com.gtnewhorizon.structurelib.gui.GuiScreenConfigureChannels; +import cpw.mods.fml.common.network.IGuiHandler; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.world.World; + +public class GuiHandler implements IGuiHandler { + @Override + public Object getServerGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) { + return null; + } + + @Override + public Object getClientGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) { + switch (ID) { + case 0: + return new GuiScreenConfigureChannels(player.inventory.getStackInSlot(x)); + default: + return null; + } + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java b/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java index 19875020..8c972c69 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java +++ b/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java @@ -1,15 +1,18 @@ package com.gtnewhorizon.structurelib; import com.gtnewhorizon.structurelib.block.BlockHint; +import com.gtnewhorizon.structurelib.command.CommandConfigureChannels; import com.gtnewhorizon.structurelib.item.ItemBlockHint; import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; import com.gtnewhorizon.structurelib.item.ItemFrontRotationTool; import com.gtnewhorizon.structurelib.net.AlignmentMessage; +import com.gtnewhorizon.structurelib.net.SetChannelDataMessage; import com.gtnewhorizon.structurelib.net.UpdateHintParticleMessage; import com.gtnewhorizon.structurelib.util.XSTR; import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.SidedProxy; import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.event.FMLServerStartingEvent; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper; import cpw.mods.fml.common.registry.GameRegistry; @@ -49,10 +52,14 @@ public class StructureLib { AlignmentMessage.ServerHandler.class, AlignmentMessage.AlignmentQuery.class, 0, Side.SERVER); net.registerMessage(AlignmentMessage.ClientHandler.class, AlignmentMessage.AlignmentData.class, 1, Side.CLIENT); net.registerMessage(UpdateHintParticleMessage.Handler.class, UpdateHintParticleMessage.class, 2, Side.CLIENT); + net.registerMessage(SetChannelDataMessage.Handler.class, SetChannelDataMessage.class, 3, Side.SERVER); } public static final XSTR RANDOM = new XSTR(); + @Mod.Instance + static StructureLib INSTANCE; + static Block blockHint; static Item itemBlockHint; static Item itemFrontRotationTool; @@ -76,6 +83,12 @@ public class StructureLib { itemConstructableTrigger = new ItemConstructableTrigger(), itemConstructableTrigger.getUnlocalizedName()); proxy.preInit(e); + NetworkRegistry.INSTANCE.registerGuiHandler(instance(), new GuiHandler()); + } + + @Mod.EventHandler + public void serverStarting(FMLServerStartingEvent e) { + e.registerServerCommand(new CommandConfigureChannels()); } public static void addClientSideChatMessages(String... messages) { @@ -93,4 +106,12 @@ public class StructureLib { public static long getOverworldTime() { return proxy.getOverworldTime(); } + + public static StructureLib instance() { + return INSTANCE; + } + + public CommonProxy proxy() { + return proxy; + } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ChannelDataAccessor.java b/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ChannelDataAccessor.java new file mode 100644 index 00000000..32a028bf --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/alignment/constructable/ChannelDataAccessor.java @@ -0,0 +1,109 @@ +package com.gtnewhorizon.structurelib.alignment.constructable; + +import java.util.Locale; +import java.util.Map.Entry; +import java.util.stream.Stream; + +import com.gtnewhorizon.structurelib.StructureLib; +import net.minecraft.init.Items; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraftforge.common.util.Constants.NBT; +import org.apache.commons.lang3.tuple.ImmutablePair; + + +import static com.gtnewhorizon.structurelib.util.MiscUtils.getTagKeys; +import static net.minecraftforge.common.util.Constants.NBT.TAG_COMPOUND; + +public class ChannelDataAccessor { + private static final String SECONDARY_HINT_TAG = "channels"; + + private ChannelDataAccessor() {} + + public static ItemStack withChannel(ItemStack masterStack, String channel) { + if (channel == null || channel.isEmpty()) throw new IllegalArgumentException(); + if (StructureLib.DEBUG_MODE && !channel.toLowerCase(Locale.ROOT).equals(channel)) + throw new IllegalArgumentException("Channel name can be lower case ONLY"); + if (!masterStack.hasTagCompound() + || !masterStack.stackTagCompound.hasKey(SECONDARY_HINT_TAG, TAG_COMPOUND) + || !masterStack + .stackTagCompound + .getCompoundTag(SECONDARY_HINT_TAG) + .hasKey(channel, NBT.TAG_INT)) return masterStack; + ItemStack ret = new ItemStack( + masterStack.getItem(), + masterStack.stackTagCompound.getCompoundTag(SECONDARY_HINT_TAG).getInteger(channel), + Items.feather.getDamage(masterStack)); + ret.setTagCompound(masterStack.stackTagCompound); + return ret; + } + + public static boolean hasSubChannel(ItemStack masterStack) { + return masterStack.hasTagCompound() && masterStack.stackTagCompound.hasKey(SECONDARY_HINT_TAG, TAG_COMPOUND); + } + + public static boolean hasSubChannel(ItemStack masterStack, String channel) { + if (StructureLib.DEBUG_MODE && !channel.toLowerCase(Locale.ROOT).equals(channel)) + throw new IllegalArgumentException("Channel name can be lower case ONLY"); + return !channel.isEmpty() + && masterStack.hasTagCompound() + && masterStack.stackTagCompound.hasKey(SECONDARY_HINT_TAG, TAG_COMPOUND) + && masterStack + .stackTagCompound + .getCompoundTag(SECONDARY_HINT_TAG) + .hasKey(channel, NBT.TAG_INT); + } + + public static int getChannelData(ItemStack masterStack, String channel) { + if (channel == null || channel.isEmpty()) throw new IllegalArgumentException(); + if (StructureLib.DEBUG_MODE && !channel.toLowerCase(Locale.ROOT).equals(channel)) + throw new IllegalArgumentException("Channel name can be lower case ONLY"); + if (!masterStack.hasTagCompound() + || !masterStack.stackTagCompound.hasKey(SECONDARY_HINT_TAG, TAG_COMPOUND) + || !masterStack + .stackTagCompound + .getCompoundTag(SECONDARY_HINT_TAG) + .hasKey(channel, NBT.TAG_INT)) return masterStack.stackSize; + return masterStack.stackTagCompound.getCompoundTag(SECONDARY_HINT_TAG).getInteger(channel); + } + + public static void setChannelData(ItemStack masterStack, String channel, int data) { + if (channel == null || channel.isEmpty()) throw new IllegalArgumentException(); + if (StructureLib.DEBUG_MODE && !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()); + main.getCompoundTag(SECONDARY_HINT_TAG).setInteger(channel, data); + } + + public static void unsetChannelData(ItemStack masterStack, String channel) { + if (channel == null || channel.isEmpty()) throw new IllegalArgumentException(); + if (StructureLib.DEBUG_MODE && !channel.toLowerCase(Locale.ROOT).equals(channel)) + throw new IllegalArgumentException("Channel name can be lower case ONLY"); + 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()); + NBTTagCompound tag = main.getCompoundTag(SECONDARY_HINT_TAG); + tag.removeTag(channel); + if (tag.hasNoTags()) main.removeTag(SECONDARY_HINT_TAG); + if (main.hasNoTags()) masterStack.stackTagCompound = null; + } + + public static void wipeChannelData(ItemStack masterStack) { + if (masterStack.stackTagCompound != null) masterStack.stackTagCompound.removeTag(SECONDARY_HINT_TAG); + } + + public static Stream> iterateChannelData(ItemStack masterStack) { + if (!hasSubChannel(masterStack)) return Stream.empty(); + NBTTagCompound tag = masterStack.stackTagCompound.getCompoundTag(SECONDARY_HINT_TAG); + return getTagKeys(tag).stream().map(s -> new ImmutablePair<>(s, tag.getInteger(s))); + } + + public static int countChannelData(ItemStack masterStack) { + if (!hasSubChannel(masterStack)) return 0; + NBTTagCompound tag = masterStack.stackTagCompound.getCompoundTag(SECONDARY_HINT_TAG); + return tag.func_150296_c().size(); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/command/CommandConfigureChannels.java b/src/main/java/com/gtnewhorizon/structurelib/command/CommandConfigureChannels.java new file mode 100644 index 00000000..c066ee6e --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/command/CommandConfigureChannels.java @@ -0,0 +1,121 @@ +package com.gtnewhorizon.structurelib.command; + +import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; +import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import net.minecraft.command.CommandBase; +import net.minecraft.command.ICommandSender; +import net.minecraft.command.WrongUsageException; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.ChatComponentTranslation; + +public class CommandConfigureChannels extends CommandBase { + @Override + public String getCommandName() { + return "sl_channel"; + } + + @Override + public String getCommandUsage(ICommandSender p_71518_1_) { + return "/sl_channel \n" + "/sl_channel \n" + + "/sl_channel \n" + + " can have space in it. must be positive integer"; + } + + @Override + public int getRequiredPermissionLevel() { + return 0; + } + + @Override + public List addTabCompletionOptions(ICommandSender p_71516_1_, String[] p_71516_2_) { + if (p_71516_2_.length == 1) + return getListOfStringsMatchingLastWord(p_71516_2_, "get", "set", "unset", "wipe", "getall"); + return null; + } + + @Override + public void processCommand(ICommandSender sender, String[] args) { + if (args.length == 0) throw new WrongUsageException(getCommandUsage(sender)); + + if (!(sender instanceof EntityPlayerMP)) throw new WrongUsageException("must be a player"); + + EntityPlayerMP player = (EntityPlayerMP) sender; + + ItemStack heldItem = player.getHeldItem(); + if (heldItem == null || !(heldItem.getItem() instanceof ItemConstructableTrigger)) { + throw new WrongUsageException("must hold a hologram"); + } + + String channel; + + switch (args[0]) { + case "getall": + if (!ChannelDataAccessor.hasSubChannel(heldItem)) { + sender.addChatMessage(new ChatComponentText("No subchannel")); + } else { + sender.addChatMessage(new ChatComponentTranslation( + "item.structurelib.constructableTrigger.desc.lshift.0", + ChannelDataAccessor.countChannelData(heldItem))); + ChannelDataAccessor.iterateChannelData(heldItem) + .map(e -> e.getKey() + ": " + e.getValue()) + .map(ChatComponentText::new) + .forEach(player::addChatMessage); + } + return; + case "wipe": + ChannelDataAccessor.wipeChannelData(heldItem); + return; + case "get": + case "unset": + if (args.length < 2) throw new WrongUsageException(getCommandUsage(sender)); + channel = + args.length == 2 ? args[1] : Arrays.stream(args).skip(1).collect(Collectors.joining()); + break; + case "set": + if (args.length < 3) throw new WrongUsageException(getCommandUsage(sender)); + channel = args.length == 3 + ? args[1] + : Arrays.stream(args).skip(1).limit(args.length - 2).collect(Collectors.joining()); + break; + default: + throw new WrongUsageException(getCommandUsage(sender)); + } + + switch (args[0]) { + case "get": + if (ChannelDataAccessor.hasSubChannel(heldItem, channel)) + sender.addChatMessage(new ChatComponentText( + channel + " value: " + ChannelDataAccessor.getChannelData(heldItem, channel))); + else sender.addChatMessage(new ChatComponentText(channel + " value: N/A")); + break; + case "clear": + if (ChannelDataAccessor.hasSubChannel(heldItem, channel)) + sender.addChatMessage(new ChatComponentText(channel + " no value")); + else { + ChannelDataAccessor.unsetChannelData(heldItem, channel); + sender.addChatMessage(new ChatComponentText(channel + " cleared")); + player.inventoryContainer.detectAndSendChanges(); + } + break; + case "set": + int value; + try { + value = Integer.parseInt(args[args.length - 1]); + } catch (NumberFormatException e) { + 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); + player.inventoryContainer.detectAndSendChanges(); + sender.addChatMessage(new ChatComponentText(channel + " value: " + value)); + break; + } + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScreenConfigureChannels.java b/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScreenConfigureChannels.java new file mode 100644 index 00000000..53de759f --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScreenConfigureChannels.java @@ -0,0 +1,378 @@ +package com.gtnewhorizon.structurelib.gui; + +import com.gtnewhorizon.structurelib.StructureLib; +import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; +import com.gtnewhorizon.structurelib.gui.GuiScrollableList.IGuiScreen; +import java.util.List; +import java.util.Locale; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiTextField; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.resources.I18n; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.lang3.StringUtils; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen { + private static final int KEY_MAX_WIDTH = 50; + private final ItemStack trigger; + private final GuiChannelsList list; + private GuiTextField key, value; + protected int guiTop, guiLeft; + + public GuiScreenConfigureChannels(ItemStack trigger) { + this.trigger = trigger; + list = new GuiChannelsList(152, 100, 12, 12, 14); + list.addSelectionListener((list, selectedIndex) -> { + Entry e = list.getElementAt(selectedIndex); + key.setText(e.getKey()); + value.setText(e.getValue().toString()); + updateButtons(); + }); + } + + @Override + public void initGui() { + super.initGui(); + // so you can keep holding backspace to delete a long sequence of chars + Keyboard.enableRepeatEvents(true); + + guiLeft = (this.width - this.getXSize()) / 2; + guiTop = (this.height - this.getYSize()) / 2; + + key = new GuiTextField(fontRendererObj, guiLeft + 45, guiTop + 119, 151 + 12 - 45, 12) { + @Override + public void writeText(String text) { + // force lower case + super.writeText(text.toLowerCase(Locale.ROOT)); + updateButtons(); + } + + @Override + public void mouseClicked(int p_146192_1_, int p_146192_2_, int p_146192_3_) { + super.mouseClicked(p_146192_1_, p_146192_2_, p_146192_3_); + boolean flag = p_146192_1_ >= this.xPosition + && p_146192_1_ < this.xPosition + this.width + && p_146192_2_ >= this.yPosition + && p_146192_2_ < this.yPosition + this.height; + if (flag && p_146192_3_ == 1) { + key.setText(""); + value.setText(""); + } + } + + @Override + public void setText(String p_146180_1_) { + super.setText(p_146180_1_); + updateButtons(); + } + }; + value = new GuiTextField(fontRendererObj, guiLeft + 45, guiTop + 139, 151 + 12 - 45, 12) { + @Override + public void writeText(String text) { + // ignore write requests containing non digit characters. + // we don't accept decimals or negative numbers, so we can safely use isDigit instead of something more + // advanced + // use codePoints() in case someone pasted an emoji by mistake + if (text != null && text.codePoints().allMatch(Character::isDigit)) { + super.writeText(text); + updateButtons(); + } + } + + @Override + public void setFocused(boolean p_146195_1_) { + if (!p_146195_1_ && isFocused()) { + int result; + try { + result = Math.max(Integer.parseInt(getText()), 1); + } catch (NumberFormatException e) { + result = 1; + } + setText(String.valueOf(result)); + } + super.setFocused(p_146195_1_); + } + + @Override + public void mouseClicked(int p_146192_1_, int p_146192_2_, int p_146192_3_) { + super.mouseClicked(p_146192_1_, p_146192_2_, p_146192_3_); + boolean flag = p_146192_1_ >= this.xPosition + && p_146192_1_ < this.xPosition + this.width + && p_146192_2_ >= this.yPosition + && p_146192_2_ < this.yPosition + this.height; + if (flag && p_146192_3_ == 1) { + setText(""); + } + } + + @Override + public void setText(String p_146180_1_) { + super.setText(p_146180_1_); + updateButtons(); + } + }; + + list.onGuiInit(this); + + addButton(new GuiButton( + 0, guiLeft + 12, guiTop + 157, 47, 20, I18n.format("item.structurelib.constructableTrigger.gui.add"))); + addButton(new GuiButton( + 1, + guiLeft + 65, + guiTop + 157, + 47, + 20, + I18n.format("item.structurelib.constructableTrigger.gui.unset"))); + addButton(new GuiButton( + 2, + guiLeft + 118, + guiTop + 157, + 47, + 20, + I18n.format("item.structurelib.constructableTrigger.gui.wipe"))); + + updateButtons(); + } + + @Override + public int getGuiLeft() { + return guiLeft; + } + + @Override + public int getGuiTop() { + return guiTop; + } + + @Override + public int getXSize() { + return 176; + } + + @Override + public int getYSize() { + return 188; + } + + @Override + public void addButton(GuiButton button) { + getButtonList().add(button); + } + + @Override + public void removeButton(GuiButton button) { + getButtonList().remove(button); + } + + @Override + public int getOverlayOffsetX() { + return 0; + } + + @Override + public void doActionPerformed(GuiButton but) { + actionPerformed(but); + } + + @SuppressWarnings("unchecked") + private List getButtonList() { + return buttonList; + } + + @Override + public void handleMouseInput() { + int delta = Mouse.getEventDWheel(); + if (delta != 0) list.handleDWheel(delta); + super.handleMouseInput(); + } + + @Override + protected void mouseClicked(int mX, int mY, int button) { + key.mouseClicked(mX, mY, button); + value.mouseClicked(mX, mY, button); + super.mouseClicked(mX, mY, button); + } + + @Override + protected void keyTyped(char aChar, int aKey) { + if (key.textboxKeyTyped(aChar, aKey)) { + updateButtons(); + return; + } + if (value.textboxKeyTyped(aChar, aKey)) return; + super.keyTyped(aChar, aKey); + } + + private void updateButtons() { + // this will be called from setText of key and value. NEVER UPDATE THE VALUE OF THESE HERE OR GET A + // STACKOVERFLOW! + boolean existing = ChannelDataAccessor.hasSubChannel(trigger, key.getText()); + getButtonList().get(0).displayString = existing + ? I18n.format("item.structurelib.constructableTrigger.gui.set") + : I18n.format("item.structurelib.constructableTrigger.gui.add"); + getButtonList().get(0).enabled = !StringUtils.isBlank(value.getText()); + getButtonList().get(1).enabled = existing && !StringUtils.isBlank(value.getText()); + } + + private int getValue() { + try { + return Integer.parseInt(value.getText()); + } catch (NumberFormatException e) { + return -1; + } + } + + @Override + protected void actionPerformed(GuiButton btn) { + if (btn == null) return; + switch (btn.id) { + case 0: + int value = getValue(); + if (value <= 0) return; + ChannelDataAccessor.setChannelData(trigger, key.getText(), value); + break; + case 1: + ChannelDataAccessor.unsetChannelData(trigger, key.getText()); + break; + case 2: + ChannelDataAccessor.wipeChannelData(trigger); + break; + } + super.actionPerformed(btn); + } + + @Override + public void onGuiClosed() { + super.onGuiClosed(); + Keyboard.enableRepeatEvents(false); + StructureLib.instance().proxy().uploadChannels(trigger); + } + + @Override + public void updateScreen() { + super.updateScreen(); + key.updateCursorCounter(); + value.updateCursorCounter(); + } + + @Override + public boolean doesGuiPauseGame() { + return false; + } + + @Override + public void drawScreen(int mX, int mY, float partialTick) { + mc.renderEngine.bindTexture(new ResourceLocation("structurelib", "textures/gui/channels.png")); + int topLeftX = (this.width - this.getXSize()) / 2; + int topLeftY = (this.height - this.getYSize()) / 2; + drawTexturedModalRect(topLeftX, topLeftY, 0, 0, getXSize(), getYSize()); + super.drawScreen(mX, mY, partialTick); + list.drawScreen(mX, mY, partialTick); + fontRendererObj.drawString( + I18n.format("item.structurelib.constructableTrigger.gui.key"), guiLeft + 12, guiTop + 122, 0); + key.drawTextBox(); + fontRendererObj.drawString( + I18n.format("item.structurelib.constructableTrigger.gui.value"), guiLeft + 12, guiTop + 142, 0); + value.drawTextBox(); + } + + private class GuiChannelsList extends GuiScrollableList> { + private List> cache; + + public GuiChannelsList(int width, int height, int originX, int originY, int slotHeight) { + super(width, height, originX, originY, slotHeight); + } + + @Override + public Entry getElementAt(int index) { + return ChannelDataAccessor.iterateChannelData(trigger) + .sorted() + .skip(index) + .findFirst() + .orElse(null); + } + + @Override + public int getNumElements() { + return ChannelDataAccessor.countChannelData(trigger); + } + + @Override + protected boolean elementClicked(int elementIndex, boolean doubleClick, int mXRelative, int mYRelative) { + if (mXRelative >= margin + 1 + && mXRelative <= margin + 5 + && mYRelative >= margin / 2 + 1 + && mYRelative <= margin / 2 + 5) { + Entry e = getElementAt(elementIndex); + if (e != null) { + ChannelDataAccessor.unsetChannelData(trigger, e.getKey()); + return false; + } + } + return super.elementClicked(elementIndex, doubleClick, mXRelative, mYRelative); + } + + @Override + protected void prepareDrawElements() { + cache = ChannelDataAccessor.iterateChannelData(trigger).sorted().collect(Collectors.toList()); + } + + @Override + protected void drawElement( + int elementIndex, int x, int y, int height, Tessellator tessellator, boolean isHovering) { + if (elementIndex < 0 || elementIndex >= cache.size()) { + return; + } + Entry e = cache.get(elementIndex); + if (e == null) return; + if (elementIndex > 0) { + // args x1, x2, y1, color + drawHorizontalLine(minX + 1, maxX - 1, y - 2, 0xffaaaaaa); + } + int keyMaxWidth = KEY_MAX_WIDTH * 2; + if (isHovering) { + mc.renderEngine.bindTexture(new ResourceLocation("structurelib", "textures/gui/channels.png")); + GL11.glColor4f(1, 1, 1, 1); + drawTexturedModalRect(x + margin + 1, y + margin / 2 + 1, 251, 251, 5, 5); + } + fontRendererObj.drawString(trim(e.getKey(), keyMaxWidth - 9), x + margin + 8, y + margin / 2, 0xffffff); + int valueX = x + margin + keyMaxWidth + margin; + fontRendererObj.drawString(trim(e.getValue().toString(), maxX - valueX), valueX, y + margin / 2, 0xffffff); + } + + @Override + public void drawScreen(int mX, int mY, float partialTick) { + super.drawScreen(mX, mY, partialTick); + // args x1, y1, y2, color + drawVerticalLine(minX + margin + margin / 2 + KEY_MAX_WIDTH * 2, minY + 1, maxY - 1, 0xffaaaaaa); + } + + public String trim(String e, int keyMaxWidth) { + // TODO optimize this to not create a billion string, or maybe cache this + String s = fontRendererObj.trimStringToWidth(e, keyMaxWidth); + if (s.length() != e.length()) { + StringBuilder buffer = + new StringBuilder(s).deleteCharAt(s.length() - 1).append("..."); + while ((s = fontRendererObj.trimStringToWidth(buffer.toString(), keyMaxWidth)).length() + != buffer.length()) + // drop last original char + buffer.deleteCharAt(s.length() - 4); + do { + buffer.append('.'); + } while (fontRendererObj.getStringWidth(buffer.toString()) <= keyMaxWidth); + s = buffer.deleteCharAt(s.length() - 1) + .insert(s.length() - 3, EnumChatFormatting.GRAY) + .toString(); + } + return s; + } + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScrollableList.java b/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScrollableList.java new file mode 100644 index 00000000..7f537d7a --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScrollableList.java @@ -0,0 +1,443 @@ +package com.gtnewhorizon.structurelib.gui; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +/** + * Shamelessly stolen from EnderCore, with some amount of edit to fit in one file. Original code from CrazyPants. + *

+ * As the code has been released into public domain, it is now LGPL as part of this library as a whole. + * You can go to EnderCore's repository to obtain a public domain copy though + * + * Major edits + *

    + *
  • Separate dwheel handling to handleDWheel. The next event calls often cause the mouse click event to + * not fire on other widgets
  • + *
  • scrollbar are now rendered within width
  • + *
  • added prepareDrawElements
  • + *
+ * + * @author CrazyPants + * @author glee8e + */ +public abstract class GuiScrollableList { + protected final int slotHeight; + private final Minecraft mc = Minecraft.getMinecraft(); + protected int originX; + protected int originY; + protected int width; + protected int height; + protected int minY; + protected int maxY; + protected int minX; + protected int maxX; + protected int mouseX; + protected int mouseY; + protected int selectedIndex = -1; + protected int margin = 4; + protected List> listeners = new CopyOnWriteArrayList<>(); + private int scrollUpButtonID; + private int scrollDownButtonID; + private float initialClickY = -2.0F; + private float scrollMultiplier; + private float amountScrolled; + private long lastClickedTime; + + private boolean showSelectionBox = true; + + public GuiScrollableList(int width, int height, int originX, int originY, int slotHeight) { + this.width = width; + this.height = height; + this.originX = originX; + this.originY = originY; + this.slotHeight = slotHeight; + + minY = originY; + maxY = minY + height; + minX = originX; + maxX = minX + width; + } + + public void onGuiInit(IGuiScreen gui) { + minY = originY + gui.getGuiTop(); + maxY = minY + height; + minX = originX + gui.getGuiLeft(); + maxX = minX + width; + } + + public void addSelectionListener(ListSelectionListener listener) { + listeners.add(listener); + } + + public void removeSelectionListener(ListSelectionListener listener) { + listeners.remove(listener); + } + + public T getSelectedElement() { + return getElementAt(selectedIndex); + } + + public void setSelection(T selection) { + setSelection(getIndexOf(selection)); + } + + public void setSelection(int index) { + if (index == selectedIndex) { + return; + } + selectedIndex = index; + for (ListSelectionListener listener : listeners) { + listener.selectionChanged(this, selectedIndex); + } + } + + public int getIndexOf(T element) { + if (element == null) { + return -1; + } + for (int i = 0; i < getNumElements(); i++) { + if (element.equals(getElementAt(i))) { + return i; + } + } + return -1; + } + + public abstract T getElementAt(int index); + + public abstract int getNumElements(); + + protected abstract void drawElement( + int elementIndex, int x, int y, int height, Tessellator tessellator, boolean isHovering); + + protected boolean elementClicked(int elementIndex, boolean doubleClick, int mXRelative, int mYRelative) { + return true; + } + + public void setShowSelectionBox(boolean val) { + showSelectionBox = val; + } + + protected int getContentHeight() { + return getNumElements() * slotHeight; + } + + public void setScrollButtonIds(int scrollUpButtonID, int scrollDownButtonID) { + this.scrollUpButtonID = scrollUpButtonID; + this.scrollDownButtonID = scrollDownButtonID; + } + + private void clampScrollToBounds() { + int i = getContentOverhang(); + if (i < 0) { + i *= -1; + } + if (amountScrolled < 0.0F) { + amountScrolled = 0.0F; + } + if (amountScrolled > i) { + amountScrolled = i; + } + } + + public int getContentOverhang() { + return getContentHeight() - (height - margin); + } + + public void actionPerformed(GuiButton b) { + if (b.enabled) { + if (b.id == scrollUpButtonID) { + amountScrolled -= slotHeight * 2 / 3; + initialClickY = -2.0F; + clampScrollToBounds(); + } else if (b.id == scrollDownButtonID) { + amountScrolled += slotHeight * 2 / 3; + initialClickY = -2.0F; + clampScrollToBounds(); + } + } + } + + /** + * draws the slot to the screen, pass in mouse's current x and y and partial + * ticks + */ + public void drawScreen(int mX, int mY, float partialTick) { + this.mouseX = mX; + this.mouseY = mY; + + processMouseEvents(); + + if (getContentOverhang() > 0) maxX = minX + width - 6; + else maxX = minX + width; + + clampScrollToBounds(); + + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_FOG); + + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int sx = minX * sr.getScaleFactor(); + int sw = width * sr.getScaleFactor(); + int sy = mc.displayHeight - (maxY * sr.getScaleFactor()); + int sh = height * sr.getScaleFactor(); + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GL11.glScissor(sx, sy, sw, sh); + + Tessellator tessellator = Tessellator.instance; + drawContainerBackground(tessellator); + + int amountScrolled = (int) this.amountScrolled; + int contentYOffset = this.minY + margin - amountScrolled; + + boolean hoveringPrecondition = mX >= minX && mX <= maxX && mY >= minY && mY <= maxY; + + prepareDrawElements(); + + for (int i = 0; i < getNumElements(); ++i) { + + int elementY = contentYOffset + i * this.slotHeight; + int slotHeight = this.slotHeight - margin; + + if (elementY <= maxY && elementY + slotHeight >= minY) { + + if (showSelectionBox && i == selectedIndex) { + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + GL11.glDisable(GL11.GL_TEXTURE_2D); + tessellator.startDrawingQuads(); + tessellator.setColorOpaque_I(8421504); + tessellator.addVertexWithUV(minX, elementY + slotHeight + 2, 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(maxX, elementY + slotHeight + 2, 0.0D, 1.0D, 1.0D); + tessellator.addVertexWithUV(maxX, elementY - 2, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(minX, elementY - 2, 0.0D, 0.0D, 0.0D); + tessellator.setColorOpaque_I(0); + tessellator.addVertexWithUV(minX + 1, elementY + slotHeight + 1, 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(maxX - 1, elementY + slotHeight + 1, 0.0D, 1.0D, 1.0D); + tessellator.addVertexWithUV(maxX - 1, elementY - 1, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(minX + 1, elementY - 1, 0.0D, 0.0D, 0.0D); + tessellator.draw(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + + boolean isHovering = hoveringPrecondition && elementY - 2 <= mY && mY < elementY + slotHeight + 2; + drawElement(i, minX, elementY, slotHeight, tessellator, isHovering); + } + } + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_ALPHA_TEST); + GL11.glShadeModel(GL11.GL_SMOOTH); + GL11.glDisable(GL11.GL_TEXTURE_2D); + + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_I(0, 0); + tessellator.addVertexWithUV(this.minX, this.minY + margin, 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(this.maxX, this.minY + margin, 0.0D, 1.0D, 1.0D); + tessellator.setColorRGBA_I(0, 255); + tessellator.addVertexWithUV(this.maxX, this.minY, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(this.minX, this.minY, 0.0D, 0.0D, 0.0D); + tessellator.draw(); + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_I(0, 255); + tessellator.addVertexWithUV(this.minX, this.maxY, 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(this.maxX, this.maxY, 0.0D, 1.0D, 1.0D); + tessellator.setColorRGBA_I(0, 0); + tessellator.addVertexWithUV(this.maxX, this.maxY - margin, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(this.minX, this.maxY - margin, 0.0D, 0.0D, 0.0D); + tessellator.draw(); + + renderScrollBar(tessellator); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glDisable(GL11.GL_BLEND); + } + + protected void prepareDrawElements() {} + + protected void renderScrollBar(Tessellator tessellator) { + + int contentHeightOverBounds = getContentOverhang(); + if (contentHeightOverBounds > 0) { + + int clear = (maxY - minY) * (maxY - minY) / getContentHeight(); + + if (clear < 32) { + clear = 32; + } + + if (clear > maxY - minY - 8) { + clear = maxY - minY - 8; + } + + int y = (int) this.amountScrolled * (maxY - minY - clear) / contentHeightOverBounds + minY; + if (y < minY) { + y = minY; + } + + GL11.glDisable(GL11.GL_TEXTURE_2D); + int scrollBarMinX = getScrollBarX(); + int scrollBarMaxX = scrollBarMinX + 6; + tessellator.startDrawingQuads(); + + tessellator.setColorRGBA_I(0, 255); + tessellator.addVertexWithUV(scrollBarMinX, maxY, 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(scrollBarMaxX, maxY, 0.0D, 1.0D, 1.0D); + tessellator.addVertexWithUV(scrollBarMaxX, minY, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(scrollBarMinX, minY, 0.0D, 0.0D, 0.0D); + + tessellator.setColorRGBA_F(0.3f, 0.3f, 0.3f, 1); + tessellator.addVertexWithUV(scrollBarMinX, (y + clear), 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(scrollBarMaxX, (y + clear), 0.0D, 1.0D, 1.0D); + tessellator.addVertexWithUV(scrollBarMaxX, y, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(scrollBarMinX, y, 0.0D, 0.0D, 0.0D); + + tessellator.setColorRGBA_F(0.7f, 0.7f, 0.7f, 1); + tessellator.addVertexWithUV(scrollBarMinX, (y + clear - 1), 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV((scrollBarMaxX - 1), (y + clear - 1), 0.0D, 1.0D, 1.0D); + tessellator.addVertexWithUV((scrollBarMaxX - 1), y, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(scrollBarMinX, y, 0.0D, 0.0D, 0.0D); + + tessellator.draw(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + } + + private void processMouseEvents() { + if (Mouse.isButtonDown(0)) { + processMouseBown(); + } else { + initialClickY = -1.0F; + } + } + + public void handleDWheel(int mouseWheelDelta) { + if (!Mouse.isButtonDown(0)) { + if (mouseWheelDelta > 0) { + amountScrolled -= slotHeight / 2; + } else /* if (mouseWheelDelta < 0) */ { + amountScrolled += slotHeight / 2; + } + } + } + + private void processMouseBown() { + int contentHeightOverBounds; + if (initialClickY == -1.0F) { + + if (mouseY >= minY && mouseY <= maxY && mouseX >= minX && mouseX <= maxX + 6) { + + boolean clickInBounds = true; + + int y = mouseY - minY + (int) amountScrolled - margin; + int mouseOverElement = y / slotHeight; + + if (mouseX >= minX + && mouseX <= maxX + && mouseOverElement >= 0 + && y >= 0 + && mouseOverElement < getNumElements()) { + boolean doubleClick = + mouseOverElement == selectedIndex && Minecraft.getSystemTime() - lastClickedTime < 250L; + if (elementClicked( + mouseOverElement, doubleClick, mouseX - minX, y - slotHeight * mouseOverElement)) { + setSelection(mouseOverElement); + lastClickedTime = Minecraft.getSystemTime(); + } + } else if (mouseX >= minX && mouseX <= maxX && y < 0) { + clickInBounds = false; + } + + int scrollBarMinX = getScrollBarX(); + int scrollBarMaxX = scrollBarMinX + 6; + if (mouseX >= scrollBarMinX && mouseX <= scrollBarMaxX) { + + scrollMultiplier = -1.0F; + contentHeightOverBounds = getContentOverhang(); + + if (contentHeightOverBounds < 1) { + contentHeightOverBounds = 1; + } + + int empty = (int) ((float) ((maxY - minY) * (maxY - minY)) / (float) getContentHeight()); + if (empty < 32) { + empty = 32; + } + if (empty > maxY - minY - 8) { + empty = maxY - minY - 8; + } + scrollMultiplier /= (float) (maxY - minY - empty) / (float) contentHeightOverBounds; + + } else { + scrollMultiplier = 1.0F; + } + + if (clickInBounds) { + initialClickY = mouseY; + } else { + initialClickY = -2.0F; + } + + } else { + initialClickY = -2.0F; + } + + } else if (initialClickY >= 0.0F) { + // Scrolling + amountScrolled -= (mouseY - initialClickY) * scrollMultiplier; + initialClickY = mouseY; + } + } + + protected int getScrollBarX() { + return maxX; + } + + protected void drawContainerBackground(Tessellator tess) { + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + GL11.glDisable(GL11.GL_TEXTURE_2D); + tess.startDrawingQuads(); + tess.setColorOpaque_I(2105376); + tess.addVertex(minX, maxY, 0.0D); + tess.addVertex(maxX, maxY, 0.0D); + tess.addVertex(maxX, minY, 0.0D); + tess.addVertex(minX, minY, 0.0D); + tess.draw(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + + public interface IGuiScreen { + + int getGuiLeft(); + + int getGuiTop(); + + int getXSize(); + + int getYSize(); + + void addButton(GuiButton button); + + void removeButton(GuiButton button); + + int getOverlayOffsetX(); + + void doActionPerformed(GuiButton but); + } + + public interface ListSelectionListener { + + void selectionChanged(GuiScrollableList list, int selectedIndex); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java b/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java index 14b2a784..962bfb1b 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java +++ b/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java @@ -3,8 +3,10 @@ package com.gtnewhorizon.structurelib.item; import static com.gtnewhorizon.structurelib.StructureLibAPI.MOD_ID; import static net.minecraft.util.EnumChatFormatting.BLUE; import static net.minecraft.util.StatCollector.translateToLocal; +import static net.minecraft.util.StatCollector.translateToLocalFormatted; import com.gtnewhorizon.structurelib.StructureLib; +import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; import com.gtnewhorizon.structurelib.alignment.constructable.ConstructableUtility; import java.util.List; import net.minecraft.entity.Entity; @@ -13,6 +15,7 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; +import org.lwjgl.input.Keyboard; public class ItemConstructableTrigger extends Item { public ItemConstructableTrigger() { @@ -21,6 +24,13 @@ public class ItemConstructableTrigger extends Item { setCreativeTab(StructureLib.creativeTab); } + @Override + public ItemStack onItemRightClick(ItemStack stack, World world, EntityPlayer player) { + if (world.isRemote && getMovingObjectPositionFromPlayer(world, player, true) == null) + player.openGui(StructureLib.instance(), 0, world, player.inventory.currentItem, 0, 0); + return super.onItemRightClick(stack, world, player); + } + @Override public boolean onItemUseFirst( ItemStack stack, @@ -39,19 +49,31 @@ public class ItemConstructableTrigger extends Item { @SuppressWarnings("unchecked") @Override public void addInformation(ItemStack aStack, EntityPlayer ep, List aList, boolean boo) { - aList.add( - translateToLocal("item.structurelib.constructableTrigger.desc.0")); // Triggers Constructable Interface - aList.add(BLUE - + translateToLocal( - "item.structurelib.constructableTrigger.desc.1")); // Shows multiblock construction details, - aList.add(BLUE - + translateToLocal( - "item.structurelib.constructableTrigger.desc.2")); // just Use on a multiblock controller. - aList.add(BLUE - + translateToLocal( - "item.structurelib.constructableTrigger.desc.3")); // (Sneak Use in creative to build) - aList.add(BLUE - + translateToLocal("item.structurelib.constructableTrigger.desc.4")); // Quantity affects tier/mode/type + if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT)) { + aList.add(translateToLocalFormatted( + "item.structurelib.constructableTrigger.desc.lshift.0", + ChannelDataAccessor.countChannelData(aStack))); + ChannelDataAccessor.iterateChannelData(aStack) + .map(e -> e.getKey() + ": " + e.getValue()) + .forEach(aList::add); + } else { + aList.add(translateToLocal( + "item.structurelib.constructableTrigger.desc.0")); // Triggers Constructable Interface + aList.add(BLUE + + translateToLocal( + "item.structurelib.constructableTrigger.desc.1")); // Shows multiblock construction details, + aList.add(BLUE + + translateToLocal( + "item.structurelib.constructableTrigger.desc.2")); // just Use on a multiblock controller. + aList.add(BLUE + + translateToLocal( + "item.structurelib.constructableTrigger.desc.3")); // (Sneak Use in creative to build) + aList.add(BLUE + + translateToLocal( + "item.structurelib.constructableTrigger.desc.4")); // Quantity affects tier/mode/type + if (ChannelDataAccessor.hasSubChannel(aStack)) + aList.add(translateToLocal("item.structurelib.constructableTrigger.desc.5")); + } } @Override diff --git a/src/main/java/com/gtnewhorizon/structurelib/net/SetChannelDataMessage.java b/src/main/java/com/gtnewhorizon/structurelib/net/SetChannelDataMessage.java new file mode 100644 index 00000000..70445ed7 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/net/SetChannelDataMessage.java @@ -0,0 +1,67 @@ +package com.gtnewhorizon.structurelib.net; + +import com.gtnewhorizon.structurelib.StructureLib; +import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; +import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; +import cpw.mods.fml.common.network.ByteBufUtils; +import cpw.mods.fml.common.network.simpleimpl.IMessage; +import cpw.mods.fml.common.network.simpleimpl.IMessageHandler; +import cpw.mods.fml.common.network.simpleimpl.MessageContext; +import io.netty.buffer.ByteBuf; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import org.apache.commons.lang3.tuple.Pair; + +public class SetChannelDataMessage implements IMessage { + private List> data = new ArrayList<>(); + + public SetChannelDataMessage() {} + + public SetChannelDataMessage(ItemStack trigger) { + ChannelDataAccessor.iterateChannelData(trigger).forEach(data::add); + } + + @Override + public void fromBytes(ByteBuf buf) { + data.clear(); + int size = ByteBufUtils.readVarShort(buf); + for (int i = 0; i < size; i++) { + data.add(Pair.of(ByteBufUtils.readUTF8String(buf), ByteBufUtils.readVarInt(buf, 4))); + } + } + + @Override + public void toBytes(ByteBuf buf) { + ByteBufUtils.writeVarShort(buf, data.size()); + for (Entry e : data) { + ByteBufUtils.writeUTF8String(buf, e.getKey()); + ByteBufUtils.writeVarInt(buf, e.getValue(), 4); + } + } + + public static class Handler implements IMessageHandler { + @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); + for (Entry e : message.data) { + 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 + } else { + StructureLib.LOGGER.warn( + "{} trying to set channel data on {}, which is not a hologram projector!", + player.getUniqueID(), + heldItem); + } + return null; + } + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java index cb1991b4..39744f67 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java @@ -5,6 +5,7 @@ import static java.lang.Integer.MIN_VALUE; import com.gtnewhorizon.structurelib.StructureLib; import com.gtnewhorizon.structurelib.StructureLibAPI; +import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing; import com.gtnewhorizon.structurelib.structure.IStructureElement.PlaceResult; import com.gtnewhorizon.structurelib.structure.adders.IBlockAdder; @@ -1895,6 +1896,42 @@ public class StructureUtility { // endregion + // region channels + + /** + * See channels.md in docs folder + */ + public static IStructureElement withChannel(String channel, IStructureElement backing) { + return new IStructureElement() { + public boolean check(T t, World world, int x, int y, int z) { + return backing.check(t, world, x, y, z); + } + + public boolean spawnHint(T t, World world, int x, int y, int z, ItemStack trigger) { + return backing.spawnHint(t, world, x, y, z, ChannelDataAccessor.withChannel(trigger, channel)); + } + + public boolean placeBlock(T t, World world, int x, int y, int z, ItemStack trigger) { + return backing.placeBlock(t, world, x, y, z, ChannelDataAccessor.withChannel(trigger, channel)); + } + + public PlaceResult survivalPlaceBlock( + T t, + World world, + int x, + int y, + int z, + ItemStack trigger, + IItemSource s, + EntityPlayerMP actor, + Consumer chatter) { + return backing.survivalPlaceBlock( + t, world, x, y, z, ChannelDataAccessor.withChannel(trigger, channel), s, actor, chatter); + } + }; + } + // endregion + /** * Used internally, to generate skips for structure definitions */ diff --git a/src/main/resources/assets/structurelib/lang/en_US.lang b/src/main/resources/assets/structurelib/lang/en_US.lang index 450cbb76..72414bf4 100644 --- a/src/main/resources/assets/structurelib/lang/en_US.lang +++ b/src/main/resources/assets/structurelib/lang/en_US.lang @@ -26,7 +26,15 @@ item.structurelib.constructableTrigger.desc.1=Shows multiblock construction sche item.structurelib.constructableTrigger.desc.2=use on the multiblock controller. item.structurelib.constructableTrigger.desc.3=(Sneak use on controller to build) item.structurelib.constructableTrigger.desc.4=Quantity of schematics in item stack affects tier/mode/type +item.structurelib.constructableTrigger.desc.5=Hold §l[LSHIFT]§r to display channel data +item.structurelib.constructableTrigger.desc.lshift.0=Contain %d sub channel. item.structurelib.constructableTrigger.too_fast=Please wait %d ticks before another autoplace... +item.structurelib.constructableTrigger.gui.key=Key: +item.structurelib.constructableTrigger.gui.value=Value: +item.structurelib.constructableTrigger.gui.add=Add +item.structurelib.constructableTrigger.gui.set=Set +item.structurelib.constructableTrigger.gui.unset=Unset +item.structurelib.constructableTrigger.gui.wipe=§4Wipe structurelib.autoplace.built_stat=Built %s blocks structurelib.autoplace.complete=§aThis multiblock has completed! diff --git a/src/main/resources/assets/structurelib/lang/zh_CN.lang b/src/main/resources/assets/structurelib/lang/zh_CN.lang index f3364afe..77f433ac 100644 --- a/src/main/resources/assets/structurelib/lang/zh_CN.lang +++ b/src/main/resources/assets/structurelib/lang/zh_CN.lang @@ -26,8 +26,17 @@ item.structurelib.constructableTrigger.desc.1=只能在多方块主机上使用 item.structurelib.constructableTrigger.desc.2=结构的示意图。 (译者注:请搭高使用) item.structurelib.constructableTrigger.desc.3= (潜行右键可以直接完成搭建) item.structurelib.constructableTrigger.desc.4=此物品堆叠数量会影响显示的机器等级/模式/类型 +item.structurelib.constructableTrigger.desc.5=按下 §l[左SHIFT]§r 以显示子信道 +item.structurelib.constructableTrigger.desc.lshift.0=包含 %d 个子信道 item.structurelib.constructableTrigger.too_fast=请在下一次自动搭建前等待%dtick... +item.structurelib.constructableTrigger.gui.key=信道名: +item.structurelib.constructableTrigger.gui.value=值: +item.structurelib.constructableTrigger.gui.add=添加 +item.structurelib.constructableTrigger.gui.set=替换 +item.structurelib.constructableTrigger.gui.unset=删除 +item.structurelib.constructableTrigger.gui.wipe=§4全部清空 +structurelib.autoplace.built_stat=已放置 %s 个方块 structurelib.autoplace.complete=§a这个多方块结构已经完成自动搭建! structurelib.autoplace.error.not_enabled=§c这个多方块结构不支持在生存模式下自动搭建! structurelib.autoplace.error.no_simple_block=§c建议放置§r%s§c,但没有找到它 diff --git a/src/main/resources/assets/structurelib/textures/gui/channels.png b/src/main/resources/assets/structurelib/textures/gui/channels.png new file mode 100644 index 00000000..c76cdf33 Binary files /dev/null and b/src/main/resources/assets/structurelib/textures/gui/channels.png differ