From df96277bd7d9be92d9e4500cfe5785457da539f5 Mon Sep 17 00:00:00 2001 From: Glease <4586901+Glease@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:32:10 +0800 Subject: [PATCH] add channel desc and channel indicator item Signed-off-by: Glease <4586901+Glease@users.noreply.github.com> --- .../structurelib/ChannelDescription.java | 94 ++++ .../structurelib/DevelopHelper.java | 48 ++ .../gtnewhorizon/structurelib/GuiHandler.java | 10 +- .../structurelib/StructureLib.java | 66 ++- .../structurelib/StructureLibAPI.java | 31 ++ .../structurelib/gui/EmptyContainer.java | 18 + .../gui/GuiScreenConfigureChannels.java | 459 +++++++++++++++--- .../structurelib/gui/GuiScrollableList.java | 27 +- .../item/ItemConstructableTrigger.java | 7 +- .../structure/IStructureDefinition.java | 2 +- .../structure/StructureUtility.java | 74 +-- src/main/javadoc/overview.html | 74 +++ .../assets/structurelib/lang/en_US.lang | 9 +- .../assets/structurelib/lang/zh_CN.lang | 9 +- .../structurelib/textures/gui/channels.png | Bin 2825 -> 6738 bytes 15 files changed, 788 insertions(+), 140 deletions(-) create mode 100644 src/main/java/com/gtnewhorizon/structurelib/ChannelDescription.java create mode 100644 src/main/java/com/gtnewhorizon/structurelib/DevelopHelper.java create mode 100644 src/main/java/com/gtnewhorizon/structurelib/gui/EmptyContainer.java diff --git a/src/main/java/com/gtnewhorizon/structurelib/ChannelDescription.java b/src/main/java/com/gtnewhorizon/structurelib/ChannelDescription.java new file mode 100644 index 00000000..91c24461 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/ChannelDescription.java @@ -0,0 +1,94 @@ +package com.gtnewhorizon.structurelib; + +import java.util.AbstractMap; +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.Set; +import java.util.stream.Collectors; + +import net.minecraft.item.ItemStack; + +import com.gtnewhorizon.gtnhlib.util.map.ItemStackMap; + +import cpw.mods.fml.common.Loader; + +// this is not an API! go check StructureLibAPI instead! +public class ChannelDescription { + + private static final ItemStackMap> itemToChannels = new ItemStackMap<>(true); + private static final Map registry = new HashMap<>(); + private final String channel; + private final Map descriptions = new HashMap<>(); + private final ItemStackMap items = new ItemStackMap<>(true); + + public static ChannelDescription get(final String channel) { + if (StructureLibAPI.isDebugEnabled() && !channel.toLowerCase(Locale.ROOT).equals(channel)) + throw new IllegalArgumentException("Channel name can be lower case ONLY"); + return registry.computeIfAbsent(channel, ChannelDescription::new); + } + + public static boolean has(final String channel) { + if (StructureLibAPI.isDebugEnabled() && !channel.toLowerCase(Locale.ROOT).equals(channel)) + throw new IllegalArgumentException("Channel name can be lower case ONLY"); + return registry.containsKey(channel); + } + + public static Collection> iterate(final ItemStack block) { + Set channels = itemToChannels.get(block); + if (channels == null) return Collections.emptySet(); + return channels.stream().map(d -> new AbstractMap.SimpleImmutableEntry<>(d.channel, d.items.get(block))) + .collect(Collectors.toList()); + } + + public static void set(final String channel, final String modid, final String description) { + if (StructureLibAPI.isDebugEnabled() && !channel.toLowerCase(Locale.ROOT).equals(channel)) + throw new IllegalArgumentException("Channel name can be lower case ONLY"); + registry.computeIfAbsent(channel, ChannelDescription::new).add(modid, description); + } + + public static void item(final String channel, int channelValue, ItemStack stack) { + if (StructureLibAPI.isDebugEnabled()) { + if (!channel.toLowerCase(Locale.ROOT).equals(channel)) { + throw new IllegalArgumentException("Channel name can be lower case ONLY"); + } + if (channelValue <= 0) { + throw new IllegalArgumentException("Channel value must be greater than 0"); + } + } + ChannelDescription inst = registry.computeIfAbsent(channel, ChannelDescription::new); + inst.items.put(stack, channelValue); + itemToChannels.computeIfAbsent(stack, s -> new HashSet<>()).add(inst); + } + + public static Map getAll() { + return Collections.unmodifiableMap(registry); + } + + private ChannelDescription(String channel) { + this.channel = channel; + } + + public String getChannel() { + return channel; + } + + public ChannelDescription add(String modid, String description) { + if (Loader.instance().getIndexedModList().get(modid) == null) { + throw new IllegalArgumentException("Mod not found: " + modid); + } + descriptions.put(modid, description); + return this; + } + + public Map getDescriptions() { + return Collections.unmodifiableMap(descriptions); + } + + public Map getItems() { + return Collections.unmodifiableMap(items); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/DevelopHelper.java b/src/main/java/com/gtnewhorizon/structurelib/DevelopHelper.java new file mode 100644 index 00000000..db49d097 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/DevelopHelper.java @@ -0,0 +1,48 @@ +package com.gtnewhorizon.structurelib; + +import net.minecraft.init.Blocks; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; + +import cpw.mods.fml.common.event.FMLInterModComms; + +public class DevelopHelper { + + private static final boolean DEBUG = Boolean.getBoolean("structurelib.develop_helper"); + + public static void onPreInit() { + if (!DEBUG) return; + StructureLib.LOGGER.info("Running develop helper"); + StructureLibAPI.registerChannelDescription("sandstone", StructureLibAPI.MOD_ID, "variants of Sandstone"); + StructureLibAPI.registerChannelItem("sandstone", StructureLibAPI.MOD_ID, 1, new ItemStack(Blocks.sandstone)); + StructureLibAPI + .registerChannelItem("sandstone", StructureLibAPI.MOD_ID, 2, new ItemStack(Blocks.sandstone, 1, 1)); + StructureLibAPI + .registerChannelItem("sandstone", StructureLibAPI.MOD_ID, 3, new ItemStack(Blocks.sandstone, 1, 2)); + StructureLibAPI.registerChannelDescription("sandbricks", StructureLibAPI.MOD_ID, "variants of Sandbricks"); + StructureLibAPI.registerChannelDescription("apple", StructureLibAPI.MOD_ID, "apple"); + StructureLibAPI.registerChannelDescription("bright", StructureLibAPI.MOD_ID, "bright"); + StructureLibAPI.registerChannelDescription("apple_blight", StructureLibAPI.MOD_ID, "apple_blight"); + StructureLibAPI.registerChannelDescription("app_eagle", StructureLibAPI.MOD_ID, "app_eagle"); + } + + public void x() { + NBTTagCompound command = new NBTTagCompound(); + NBTTagList items = new NBTTagList(); + int[] metas = { 0, 1, 2 }; + for (int meta : metas) { + NBTTagCompound tag = new NBTTagCompound(); + tag.setTag("Item", new ItemStack(Blocks.sandstone, 1, meta).writeToNBT(new NBTTagCompound())); + NBTTagList channels = new NBTTagList(); + NBTTagCompound channel = new NBTTagCompound(); + channel.setString("Channel", "sandstone"); + channel.setInteger("Value", meta + 1); + channels.appendTag(channel); + tag.setTag("Channels", channels); + items.appendTag(tag); + } + command.setTag("Items", items); + FMLInterModComms.sendMessage("structurelib", "register_channel_item", command); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/GuiHandler.java b/src/main/java/com/gtnewhorizon/structurelib/GuiHandler.java index 91ed3a59..c5e0f5c7 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/GuiHandler.java +++ b/src/main/java/com/gtnewhorizon/structurelib/GuiHandler.java @@ -3,6 +3,7 @@ package com.gtnewhorizon.structurelib; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.world.World; +import com.gtnewhorizon.structurelib.gui.EmptyContainer; import com.gtnewhorizon.structurelib.gui.GuiScreenConfigureChannels; import cpw.mods.fml.common.network.IGuiHandler; @@ -11,14 +12,19 @@ public class GuiHandler implements IGuiHandler { @Override public Object getServerGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) { - return null; + switch (ID) { + case 0: + return new EmptyContainer(player); + default: + 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)); + return new GuiScreenConfigureChannels(new EmptyContainer(player), 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 0d8504e0..a36ad3f7 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java +++ b/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java @@ -1,11 +1,18 @@ package com.gtnewhorizon.structurelib; +import static com.gtnewhorizon.structurelib.StructureLibAPI.CHANNEL_SHOW_ERROR; +import static com.gtnewhorizon.structurelib.StructureLibAPI.MOD_ID; + import net.minecraft.block.Block; import net.minecraft.creativetab.CreativeTabs; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.Item; import net.minecraft.item.ItemBlock; +import net.minecraft.item.ItemStack; import net.minecraft.launchwrapper.Launch; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraftforge.common.util.Constants; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -24,9 +31,9 @@ import com.gtnewhorizon.structurelib.net.UpdateHintParticleMessage; import com.gtnewhorizon.structurelib.util.InventoryUtility; import com.gtnewhorizon.structurelib.util.XSTR; -import cpw.mods.fml.common.Loader; import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.SidedProxy; +import cpw.mods.fml.common.event.FMLInterModComms; import cpw.mods.fml.common.event.FMLLoadCompleteEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.event.FMLServerStartingEvent; @@ -40,7 +47,7 @@ import cpw.mods.fml.relauncher.SideOnly; * This class does not contain a stable API. Refrain from using this class. */ @Mod( - modid = StructureLibAPI.MOD_ID, + modid = MOD_ID, name = "StructureLib", version = Tags.VERSION, acceptableRemoteVersions = "*", @@ -57,7 +64,7 @@ public class StructureLib { clientSide = "com.gtnewhorizon.structurelib.ClientProxy") static CommonProxy proxy; - static final SimpleNetworkWrapper net = NetworkRegistry.INSTANCE.newSimpleChannel(StructureLibAPI.MOD_ID); + static final SimpleNetworkWrapper net = NetworkRegistry.INSTANCE.newSimpleChannel(MOD_ID); static { net.registerMessage( @@ -84,7 +91,7 @@ public class StructureLib { @Mod.Instance static StructureLib INSTANCE; - + @Mod.Instance(STRUCTURECOMPAT_MODID) static Object COMPAT; static Block blockHint; @@ -100,8 +107,6 @@ public class StructureLib { } }; - public static boolean isGTLoaded; - @Mod.EventHandler public void preInit(FMLPreInitializationEvent e) { ConfigurationHandler.INSTANCE.init(e.getSuggestedConfigurationFile()); @@ -117,11 +122,10 @@ public class StructureLib { NetworkRegistry.INSTANCE.registerGuiHandler(instance(), new GuiHandler()); InventoryUtility.init(); - if (Loader.isModLoaded(STRUCTURECOMPAT_MODID)) { - COMPAT = Loader.instance().getIndexedModList().get(STRUCTURECOMPAT_MODID).getMod(); - } - isGTLoaded = Loader.isModLoaded("gregtech"); + ChannelDescription.set(CHANNEL_SHOW_ERROR, MOD_ID, "channels.structurelib.show_errors"); + + DevelopHelper.onPreInit(); } @Mod.EventHandler @@ -135,6 +139,48 @@ public class StructureLib { e.registerServerCommand(new CommandRegistryDebug()); } + @Mod.EventHandler + public void handleIMC(FMLInterModComms.IMCEvent event) { + for (FMLInterModComms.IMCMessage message : event.getMessages()) { + switch (message.key) { + case "register_channel": + processRegisterChannel(message); + break; + case "register_channel_item": + processRegisterChannelItem(message); + break; + } + } + } + + private void processRegisterChannel(FMLInterModComms.IMCMessage message) { + NBTTagList tags = message.getNBTValue().getTagList("Channels", Constants.NBT.TAG_COMPOUND); + for (int i = 0; i < tags.tagCount(); i++) { + NBTTagCompound tag = tags.getCompoundTagAt(i); + StructureLibAPI.registerChannelDescription( + tag.getString("Channel"), + message.getSender(), + tag.getString("Description")); + } + } + + private void processRegisterChannelItem(FMLInterModComms.IMCMessage message) { + NBTTagList items = message.getNBTValue().getTagList("Items", Constants.NBT.TAG_COMPOUND); + for (int i = 0; i < items.tagCount(); i++) { + NBTTagCompound itemTag = items.getCompoundTagAt(i); + ItemStack item = ItemStack.loadItemStackFromNBT(itemTag.getCompoundTag("Item")); + NBTTagList tags = itemTag.getTagList("Channels", Constants.NBT.TAG_COMPOUND); + for (int j = 0; j < tags.tagCount(); j++) { + NBTTagCompound tag = tags.getCompoundTagAt(j); + StructureLibAPI.registerChannelItem( + tag.getString("Channel"), + message.getSender(), + tag.getInteger("Value"), + item); + } + } + } + public static void addClientSideChatMessages(String... messages) { proxy.addClientSideChatMessages(messages); } diff --git a/src/main/java/com/gtnewhorizon/structurelib/StructureLibAPI.java b/src/main/java/com/gtnewhorizon/structurelib/StructureLibAPI.java index cec674bc..37414ffd 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/StructureLibAPI.java +++ b/src/main/java/com/gtnewhorizon/structurelib/StructureLibAPI.java @@ -2,6 +2,7 @@ package com.gtnewhorizon.structurelib; import static com.gtnewhorizon.structurelib.StructureLib.proxy; +import com.gtnewhorizon.gtnhlib.util.AnimatedTooltipHandler; import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; @@ -9,6 +10,7 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.util.IChatComponent; import net.minecraft.util.IIcon; +import net.minecraft.util.StatCollector; import net.minecraft.world.World; import com.gtnewhorizon.structurelib.alignment.IAlignment; @@ -43,6 +45,7 @@ public class StructureLibAPI { public static final int HINT_BLOCK_META_AIR = 13; public static final int HINT_BLOCK_META_NOT_AIR = 14; public static final int HINT_BLOCK_META_ERROR = 15; + public static final String CHANNEL_SHOW_ERROR = "show_error"; static final ThreadLocal instrument = new ThreadLocal<>(); /** @@ -350,4 +353,32 @@ public class StructureLibAPI { short intervalRequired, boolean forceUpdateLastSend) { proxy.addThrottledChat(throttleKey, player, text, intervalRequired, forceUpdateLastSend); } + + /** + * Register a channel description here. The description should describe how a channel will be used inside your mod. + * + * @param channel channel key + * @param modid your modid + * @param description localization key for your description + */ + public static void registerChannelDescription(final String channel, final String modid, final String description) { + ChannelDescription.set(channel, modid, description); + } + + /** + * Register an item stack as an indicator for a (channel, value) pair. + * + * @param channel channel key + * @param modid your modid + * @param channelValue value + * @param stack item stack. having a metadata of + * {@link net.minecraftforge.oredict.OreDictionary#WILDCARD_VALUE} will match without comparing + * NBT or metadata value. having a tag with {@code tag.getBoolean("*") == true} will match + * without comparing NBT. + */ + public static void registerChannelItem(final String channel, final String modid, final int channelValue, + final ItemStack stack) { + ChannelDescription.item(channel, channelValue, stack); + AnimatedTooltipHandler.addItemTooltip(stack, () -> StatCollector.translateToLocalFormatted("structurelib.tooltip.channelvalue", channelValue, channel)); + } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/gui/EmptyContainer.java b/src/main/java/com/gtnewhorizon/structurelib/gui/EmptyContainer.java new file mode 100644 index 00000000..bb2a930b --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/gui/EmptyContainer.java @@ -0,0 +1,18 @@ +package com.gtnewhorizon.structurelib.gui; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.Container; + +public class EmptyContainer extends Container { + + private final EntityPlayer player; + + public EmptyContainer(EntityPlayer player) { + this.player = player; + } + + @Override + public boolean canInteractWith(EntityPlayer player) { + return player == this.player; + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScreenConfigureChannels.java b/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScreenConfigureChannels.java index ea6d69ef..3030e014 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScreenConfigureChannels.java +++ b/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScreenConfigureChannels.java @@ -1,48 +1,82 @@ package com.gtnewhorizon.structurelib.gui; +import static org.lwjgl.opengl.ARBImaging.*; +import static org.lwjgl.opengl.ARBImaging.GL_SEPARABLE_2D; +import static org.lwjgl.opengl.GL11.*; +import static org.lwjgl.opengl.GL11.GL_ALPHA_TEST_FUNC; +import static org.lwjgl.opengl.GL12.GL_RESCALE_NORMAL; +import static org.lwjgl.opengl.GL12.GL_TEXTURE_3D; +import static org.lwjgl.opengl.GL13.*; +import static org.lwjgl.opengl.GL14.*; +import static org.lwjgl.opengl.GL14.GL_BLEND_SRC_RGB; +import static org.lwjgl.opengl.GL15.GL_FOG_COORD_ARRAY; +import static org.lwjgl.opengl.GL20.*; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Map.Entry; +import java.util.Set; 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.gui.inventory.GuiContainer; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.resources.I18n; +import net.minecraft.inventory.Container; import net.minecraft.item.ItemStack; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; import org.apache.commons.lang3.StringUtils; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; +import com.google.common.collect.ImmutableMap; +import com.gtnewhorizon.structurelib.ChannelDescription; import com.gtnewhorizon.structurelib.StructureLib; import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; import com.gtnewhorizon.structurelib.gui.GuiScrollableList.IGuiScreen; -public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen { +import codechicken.nei.VisiblityData; +import codechicken.nei.api.INEIGuiHandler; +import codechicken.nei.api.TaggedInventoryArea; +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.Optional; +import gnu.trove.map.TIntObjectMap; +import gnu.trove.map.hash.TIntObjectHashMap; + +@Optional.Interface(iface = "codechicken.nei.api.INEIGuiHandler", modid = "NotEnoughItems") +public class GuiScreenConfigureChannels extends GuiContainer implements IGuiScreen, INEIGuiHandler { private static final String I18N_PREFIX = "item.structurelib.constructableTrigger.gui."; private static final int ADD_BTN = 0; private static final int UNSET_BTN = 1; private static final int WIPE_BTN = 2; - private static final int SHOW_ERROR_BTN = 3; - private static final int GT_NO_HATCH_BTN = 4; - - private static final String SHOW_ERROR_CHANNEL = "show_error"; - private static final String GT_NO_HATCH_CHANNEL = "gt_no_hatch"; private static final int KEY_MAX_WIDTH = 50; private final ItemStack trigger; private final GuiChannelsList list; + private final GuiAutoCompleteList autoCompleteList; + private final Map> tooltipSplitCache = new HashMap<>(); + private final TIntObjectMap> trimCache = new TIntObjectHashMap<>(); private GuiTextField key, value; protected int guiTop, guiLeft; - public GuiScreenConfigureChannels(ItemStack trigger) { + public GuiScreenConfigureChannels(Container container, ItemStack trigger) { + super(container); + this.trigger = trigger; list = new GuiChannelsList(152, 100, 12, 12, 14); list.addSelectionListener((list, selectedIndex) -> { @@ -51,6 +85,7 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen value.setText(e.getValue().toString()); updateButtons(); }); + autoCompleteList = new GuiAutoCompleteList(151 + 12 - 45 - 2, 50, 45, 119 + 12, 14); } @Override @@ -136,6 +171,7 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen }; list.onGuiInit(this); + autoCompleteList.onGuiInit(this); addButton( new GuiButton( @@ -162,21 +198,8 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen 20, I18n.format("item.structurelib.constructableTrigger.gui.wipe"))); - addButton( - new GuiButton( - SHOW_ERROR_BTN, - StructureLib.isGTLoaded ? guiLeft + 12 : guiLeft + 52, - guiTop + 180, - 73, - 20, - "")); - - // only show GT hatch button if GT is loaded - if (StructureLib.isGTLoaded) { - addButton(new GuiButton(GT_NO_HATCH_BTN, guiLeft + 92, guiTop + 180, 73, 20, "")); - } - updateButtons(); + tooltipSplitCache.clear(); } @Override @@ -196,7 +219,7 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen @Override public int getYSize() { - return 211; + return 188; } @Override @@ -252,13 +275,14 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen @Override protected void mouseClicked(int mX, int mY, int button) { + super.mouseClicked(mX, mY, button); key.mouseClicked(mX, mY, button); value.mouseClicked(mX, mY, button); - super.mouseClicked(mX, mY, button); } @Override protected void keyTyped(char aChar, int aKey) { + GuiScrollableList l = key.isFocused() ? autoCompleteList : list; switch (aKey) { case Keyboard.KEY_TAB: if (key.isFocused()) { @@ -271,15 +295,31 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen return; case Keyboard.KEY_RETURN: case Keyboard.KEY_NUMPADENTER: - GuiButton add = getButtonList().get(0); + if (key.isFocused() && autoCompleteList.getSelectedElement() != null) { + key.setText(autoCompleteList.getSelectedElement()); + key.setFocused(false); + value.setFocused(true); + return; + } + GuiButton add = getButtonList().get(ADD_BTN); if (add.enabled) doActionPerformed(add); return; case Keyboard.KEY_UP: - if (list.selectedIndex > 0) list.setSelection(list.selectedIndex - 1); + if (l.selectedIndex > 0) l.setSelection(l.selectedIndex - 1); return; case Keyboard.KEY_DOWN: - if (list.selectedIndex < list.getNumElements() - 1) list.setSelection(list.selectedIndex + 1); + if (l.selectedIndex < l.getNumElements() - 1) l.setSelection(l.selectedIndex + 1); return; + case Keyboard.KEY_ESCAPE: + if (key.isFocused()) { + if (autoCompleteList.getSelectedElement() != null) { + autoCompleteList.reset(); + } else { + key.setFocused(false); + } + return; + } + break; } if (key.textboxKeyTyped(aChar, aKey)) { updateButtons(); @@ -300,20 +340,7 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen && Integer.parseInt(value.getText()) > 0; getButtonList().get(UNSET_BTN).enabled = existing && !StringUtils.isBlank(value.getText()); - if (ChannelDataAccessor.hasSubChannel(trigger, SHOW_ERROR_CHANNEL)) { - getButtonList().get(SHOW_ERROR_BTN).displayString = I18n.format(I18N_PREFIX + "error.hide"); - } else { - getButtonList().get(SHOW_ERROR_BTN).displayString = I18n.format(I18N_PREFIX + "error.show"); - } - - // this button only exists if GT is loaded. - if (StructureLib.isGTLoaded) { - if (ChannelDataAccessor.hasSubChannel(trigger, GT_NO_HATCH_CHANNEL)) { - getButtonList().get(GT_NO_HATCH_BTN).displayString = I18n.format(I18N_PREFIX + "gt_no_hatch.disable"); - } else { - getButtonList().get(GT_NO_HATCH_BTN).displayString = I18n.format(I18N_PREFIX + "gt_no_hatch.enable"); - } - } + autoCompleteList.reset(); } private int getValue() { @@ -338,20 +365,8 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen break; case WIPE_BTN: ChannelDataAccessor.wipeChannelData(trigger); - break; - case SHOW_ERROR_BTN: - if (ChannelDataAccessor.hasSubChannel(trigger, SHOW_ERROR_CHANNEL)) { - ChannelDataAccessor.unsetChannelData(trigger, SHOW_ERROR_CHANNEL); - } else { - ChannelDataAccessor.setChannelData(trigger, SHOW_ERROR_CHANNEL, 1); - } - break; - case GT_NO_HATCH_BTN: - if (ChannelDataAccessor.hasSubChannel(trigger, GT_NO_HATCH_CHANNEL)) { - ChannelDataAccessor.unsetChannelData(trigger, GT_NO_HATCH_CHANNEL); - } else { - ChannelDataAccessor.setChannelData(trigger, GT_NO_HATCH_CHANNEL, 1); - } + key.setText(""); + this.value.setText(""); break; } @@ -375,17 +390,11 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen } @Override - public boolean doesGuiPauseGame() { - return false; - } - - @Override - public void drawScreen(int mX, int mY, float partialTick) { + public void drawGuiContainerBackgroundLayer(float partialTick, int mX, int mY) { 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"), @@ -401,6 +410,315 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen value.drawTextBox(); } + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + if (key.isFocused() && autoCompleteList.getNumElements() > 0) { + autoCompleteList.drawScreen(mouseX, mouseY, 0); + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawScreen(mouseX, mouseY, partialTicks); + String highlight = null; + if (key.isFocused() && autoCompleteList.getNumElements() > 0) { + highlight = autoCompleteList.getSelectedElement(); + } + if (highlight == null && ChannelDescription.has(key.getText())) { + highlight = key.getText(); + } + if (highlight != null) { + drawChannelDescriptionTooltip(highlight); + } + } + + private void drawChannelDescriptionTooltip(String candidate) { + List tooltip = tooltipSplitCache.computeIfAbsent(candidate, this::getChannelDescriptionTooltip); + drawHoveringText(tooltip, guiLeft + 151 + 12, guiTop + 122, fontRendererObj); + } + + public static void debugPrintGLEnable() { + Map functions = ImmutableMap.builder().put(GL11.GL_NEVER, "GL_NEVER") + .put(GL11.GL_LESS, "GL_LESS").put(GL11.GL_EQUAL, "GL_EQUAL").put(GL11.GL_LEQUAL, "GL_LEQUAL") + .put(GL11.GL_GREATER, "GL_GREATER").put(GL11.GL_NOTEQUAL, "GL_NOTEQUAL") + .put(GL11.GL_GEQUAL, "GL_GEQUAL").put(GL11.GL_ALWAYS, "GL_ALWAYS").put(GL11.GL_ZERO, "GL_ZERO") + .put(GL11.GL_ONE, "GL_ONE").put(GL11.GL_SRC_COLOR, "GL_SRC_COLOR") + .put(GL11.GL_ONE_MINUS_SRC_COLOR, "GL_ONE_MINUS_SRC_COLOR").put(GL11.GL_DST_COLOR, "GL_DST_COLOR") + .put(GL11.GL_ONE_MINUS_DST_COLOR, "GL_ONE_MINUS_DST_COLOR").put(GL11.GL_SRC_ALPHA, "GL_SRC_ALPHA") + .put(GL11.GL_ONE_MINUS_SRC_ALPHA, "GL_ONE_MINUS_SRC_ALPHA").put(GL11.GL_DST_ALPHA, "GL_DST_ALPHA") + .put(GL11.GL_ONE_MINUS_DST_ALPHA, "GL_ONE_MINUS_DST_ALPHA") + .put(GL11.GL_CONSTANT_COLOR, "GL_CONSTANT_COLOR") + .put(GL11.GL_ONE_MINUS_CONSTANT_COLOR, "GL_ONE_MINUS_CONSTANT_COLOR") + .put(GL11.GL_CONSTANT_ALPHA, "GL_CONSTANT_ALPHA") + .put(GL11.GL_ONE_MINUS_CONSTANT_ALPHA, "GL_ONE_MINUS_CONSTANT_ALPHA") + .put(GL11.GL_SRC_ALPHA_SATURATE, "GL_SRC_ALPHA_SATURATE").build(); + System.out.println("GL_ALPHA_TEST: " + GL11.glIsEnabled(GL_ALPHA_TEST)); + System.out.println("GL_AUTO_NORMAL: " + GL11.glIsEnabled(GL_AUTO_NORMAL)); + System.out.println("GL_BLEND: " + GL11.glIsEnabled(GL_BLEND)); + System.out.println("GL_COLOR_ARRAY: " + GL11.glIsEnabled(GL_COLOR_ARRAY)); + System.out.println("GL_COLOR_LOGIC_OP: " + GL11.glIsEnabled(GL_COLOR_LOGIC_OP)); + System.out.println("GL_COLOR_MATERIAL: " + GL11.glIsEnabled(GL_COLOR_MATERIAL)); + System.out.println("GL_COLOR_SUM: " + GL11.glIsEnabled(GL_COLOR_SUM)); + System.out.println("GL_COLOR_TABLE: " + GL11.glIsEnabled(GL_COLOR_TABLE)); + System.out.println("GL_CONVOLUTION_1D: " + GL11.glIsEnabled(GL_CONVOLUTION_1D)); + System.out.println("GL_CONVOLUTION_2D: " + GL11.glIsEnabled(GL_CONVOLUTION_2D)); + System.out.println("GL_CULL_FACE: " + GL11.glIsEnabled(GL_CULL_FACE)); + System.out.println("GL_DEPTH_TEST: " + GL11.glIsEnabled(GL_DEPTH_TEST)); + System.out.println("GL_DITHER: " + GL11.glIsEnabled(GL_DITHER)); + System.out.println("GL_EDGE_FLAG_ARRAY: " + GL11.glIsEnabled(GL_EDGE_FLAG_ARRAY)); + System.out.println("GL_FOG: " + GL11.glIsEnabled(GL_FOG)); + System.out.println("GL_FOG_COORD_ARRAY: " + GL11.glIsEnabled(GL_FOG_COORD_ARRAY)); + System.out.println("GL_HISTOGRAM: " + GL11.glIsEnabled(GL_HISTOGRAM)); + System.out.println("GL_INDEX_ARRAY: " + GL11.glIsEnabled(GL_INDEX_ARRAY)); + System.out.println("GL_INDEX_LOGIC_OP: " + GL11.glIsEnabled(GL_INDEX_LOGIC_OP)); + System.out.println("GL_LIGHTING: " + GL11.glIsEnabled(GL_LIGHTING)); + System.out.println("GL_LINE_SMOOTH: " + GL11.glIsEnabled(GL_LINE_SMOOTH)); + System.out.println("GL_LINE_STIPPLE: " + GL11.glIsEnabled(GL_LINE_STIPPLE)); + System.out.println("GL_MAP1_COLOR_4: " + GL11.glIsEnabled(GL_MAP1_COLOR_4)); + System.out.println("GL_MAP1_INDEX: " + GL11.glIsEnabled(GL_MAP1_INDEX)); + System.out.println("GL_MAP1_NORMAL: " + GL11.glIsEnabled(GL_MAP1_NORMAL)); + System.out.println("GL_MAP1_TEXTURE_COORD_1: " + GL11.glIsEnabled(GL_MAP1_TEXTURE_COORD_1)); + System.out.println("GL_MAP1_TEXTURE_COORD_2: " + GL11.glIsEnabled(GL_MAP1_TEXTURE_COORD_2)); + System.out.println("GL_MAP1_TEXTURE_COORD_3: " + GL11.glIsEnabled(GL_MAP1_TEXTURE_COORD_3)); + System.out.println("GL_MAP1_TEXTURE_COORD_4: " + GL11.glIsEnabled(GL_MAP1_TEXTURE_COORD_4)); + System.out.println("GL_MAP2_COLOR_4: " + GL11.glIsEnabled(GL_MAP2_COLOR_4)); + System.out.println("GL_MAP2_INDEX: " + GL11.glIsEnabled(GL_MAP2_INDEX)); + System.out.println("GL_MAP2_NORMAL: " + GL11.glIsEnabled(GL_MAP2_NORMAL)); + System.out.println("GL_MAP2_TEXTURE_COORD_1: " + GL11.glIsEnabled(GL_MAP2_TEXTURE_COORD_1)); + System.out.println("GL_MAP2_TEXTURE_COORD_2: " + GL11.glIsEnabled(GL_MAP2_TEXTURE_COORD_2)); + System.out.println("GL_MAP2_TEXTURE_COORD_3: " + GL11.glIsEnabled(GL_MAP2_TEXTURE_COORD_3)); + System.out.println("GL_MAP2_TEXTURE_COORD_4: " + GL11.glIsEnabled(GL_MAP2_TEXTURE_COORD_4)); + System.out.println("GL_MAP2_VERTEX_3: " + GL11.glIsEnabled(GL_MAP2_VERTEX_3)); + System.out.println("GL_MAP2_VERTEX_4: " + GL11.glIsEnabled(GL_MAP2_VERTEX_4)); + System.out.println("GL_MINMAX: " + GL11.glIsEnabled(GL_MINMAX)); + System.out.println("GL_MULTISAMPLE: " + GL11.glIsEnabled(GL_MULTISAMPLE)); + System.out.println("GL_NORMAL_ARRAY: " + GL11.glIsEnabled(GL_NORMAL_ARRAY)); + System.out.println("GL_NORMALIZE: " + GL11.glIsEnabled(GL_NORMALIZE)); + System.out.println("GL_POINT_SMOOTH: " + GL11.glIsEnabled(GL_POINT_SMOOTH)); + System.out.println("GL_POINT_SPRITE: " + GL11.glIsEnabled(GL_POINT_SPRITE)); + System.out.println("GL_POLYGON_SMOOTH: " + GL11.glIsEnabled(GL_POLYGON_SMOOTH)); + System.out.println("GL_POLYGON_OFFSET_FILL: " + GL11.glIsEnabled(GL_POLYGON_OFFSET_FILL)); + System.out.println("GL_POLYGON_OFFSET_LINE: " + GL11.glIsEnabled(GL_POLYGON_OFFSET_LINE)); + System.out.println("GL_POLYGON_OFFSET_POINT: " + GL11.glIsEnabled(GL_POLYGON_OFFSET_POINT)); + System.out.println("GL_POLYGON_STIPPLE: " + GL11.glIsEnabled(GL_POLYGON_STIPPLE)); + System.out.println("GL_POST_COLOR_MATRIX_COLOR_TABLE: " + GL11.glIsEnabled(GL_POST_COLOR_MATRIX_COLOR_TABLE)); + System.out.println("GL_POST_CONVOLUTION_COLOR_TABLE: " + GL11.glIsEnabled(GL_POST_CONVOLUTION_COLOR_TABLE)); + System.out.println("GL_RESCALE_NORMAL: " + GL11.glIsEnabled(GL_RESCALE_NORMAL)); + System.out.println("GL_SAMPLE_ALPHA_TO_COVERAGE: " + GL11.glIsEnabled(GL_SAMPLE_ALPHA_TO_COVERAGE)); + System.out.println("GL_SAMPLE_ALPHA_TO_ONE: " + GL11.glIsEnabled(GL_SAMPLE_ALPHA_TO_ONE)); + System.out.println("GL_SAMPLE_COVERAGE: " + GL11.glIsEnabled(GL_SAMPLE_COVERAGE)); + System.out.println("GL_SCISSOR_TEST: " + GL11.glIsEnabled(GL_SCISSOR_TEST)); + System.out.println("GL_SECONDARY_COLOR_ARRAY: " + GL11.glIsEnabled(GL_SECONDARY_COLOR_ARRAY)); + System.out.println("GL_SEPARABLE_2D: " + GL11.glIsEnabled(GL_SEPARABLE_2D)); + System.out.println("GL_STENCIL_TEST: " + GL11.glIsEnabled(GL_STENCIL_TEST)); + System.out.println("GL_TEXTURE_1D: " + GL11.glIsEnabled(GL_TEXTURE_1D)); + System.out.println("GL_TEXTURE_2D: " + GL11.glIsEnabled(GL_TEXTURE_2D)); + System.out.println("GL_TEXTURE_3D: " + GL11.glIsEnabled(GL_TEXTURE_3D)); + System.out.println("GL_TEXTURE_COORD_ARRAY: " + GL11.glIsEnabled(GL_TEXTURE_COORD_ARRAY)); + System.out.println("GL_TEXTURE_CUBE_MAP: " + GL11.glIsEnabled(GL_TEXTURE_CUBE_MAP)); + System.out.println("GL_TEXTURE_GEN_Q: " + GL11.glIsEnabled(GL_TEXTURE_GEN_Q)); + System.out.println("GL_TEXTURE_GEN_R: " + GL11.glIsEnabled(GL_TEXTURE_GEN_R)); + System.out.println("GL_TEXTURE_GEN_S: " + GL11.glIsEnabled(GL_TEXTURE_GEN_S)); + System.out.println("GL_TEXTURE_GEN_T: " + GL11.glIsEnabled(GL_TEXTURE_GEN_T)); + System.out.println("GL_VERTEX_ARRAY: " + GL11.glIsEnabled(GL_VERTEX_ARRAY)); + System.out.println("GL_VERTEX_PROGRAM_POINT_SIZE: " + GL11.glIsEnabled(GL_VERTEX_PROGRAM_POINT_SIZE)); + System.out.println("GL_VERTEX_PROGRAM_TWO_SIDE: " + GL11.glIsEnabled(GL_VERTEX_PROGRAM_TWO_SIDE)); + System.out.println("Depth Function: " + functions.get(GL11.glGetInteger(GL_DEPTH_FUNC))); + System.out.printf( + "Blend Function: DST: Alpha %s RGB %s, SRC: Alpha %s RGB %s\n", + functions.get(GL11.glGetInteger(GL_BLEND_DST_ALPHA)), + functions.get(GL11.glGetInteger(GL_BLEND_DST_RGB)), + functions.get(GL11.glGetInteger(GL_BLEND_SRC_ALPHA)), + functions.get(GL11.glGetInteger(GL_BLEND_SRC_RGB))); + System.out.println( + "Alpha Test Function: " + functions.get(GL11.glGetInteger(GL_ALPHA_TEST_FUNC)) + + ", Reference: " + + GL11.glGetDouble(GL11.GL_ALPHA_TEST_REF)); + } + + private List getChannelDescriptionTooltip(String candidate) { + List tooltip = new ArrayList<>(); + int maxLine = width - (guiLeft + 151 + 12) - 12; + for (Entry e : ChannelDescription.get(candidate).getDescriptions().entrySet()) { + tooltip.addAll( + fontRendererObj.listFormattedStringToWidth( + StatCollector.translateToLocalFormatted( + "item.structurelib.constructableTrigger.gui.channels.from", + Loader.instance().getIndexedModList().get(e.getKey()).getName()), + maxLine)); + tooltip.addAll( + fontRendererObj.listFormattedStringToWidth(StatCollector.translateToLocal(e.getValue()), maxLine)); + } + return tooltip; + } + + public String trim(String str, int keyMaxWidth) { + Map cache = trimCache.get(keyMaxWidth); + if (cache == null) { + trimCache.put(keyMaxWidth, cache = new HashMap<>()); + } + return cache.computeIfAbsent(str, e -> { + 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; + }); + } + + @Override + @Optional.Method(modid = "NotEnoughItems") + public VisiblityData modifyVisiblity(GuiContainer gui, VisiblityData currentVisibility) { + return null; + } + + @Override + public Iterable getItemSpawnSlots(GuiContainer gui, ItemStack item) { + return Collections.emptyList(); + } + + @Override + public List getInventoryAreas(GuiContainer gui) { + return Collections.emptyList(); + } + + @Override + public boolean handleDragNDrop(GuiContainer gui, int mousex, int mousey, ItemStack draggedStack, int button) { + if (button != 0 || mousex <= guiLeft + || mousey <= guiTop + || mousex >= guiLeft + getXSize() + || mousey >= guiTop + getYSize()) { + return false; + } + Collection> channels = ChannelDescription.iterate(draggedStack); + switch (channels.size()) { + case 0: + return false; + case 1: + default: // TODO implement a GUI to select which pair to use, instead of blindly using the first + Entry e = channels.iterator().next(); + ChannelDataAccessor.setChannelData(trigger, e.getKey(), e.getValue()); + key.setText(e.getKey()); + value.setText(e.getValue().toString()); + draggedStack.stackSize = 0; + return true; + } + } + + @Override + public boolean hideItemPanelSlot(GuiContainer gui, int x, int y, int w, int h) { + return false; + } + + private class GuiAutoCompleteList extends GuiScrollableList { + + private List> cache; // + + public GuiAutoCompleteList(int width, int height, int originX, int originY, int slotHeight) { + super(width, height, originX, originY, slotHeight); + margin = 2; + } + + private List> getOptions() { + if (cache == null) { + cache = search(); + } + return cache; + } + + private List> search() { + List> cache = new ArrayList<>(); + Set added = new HashSet<>(); + String term = key.getText(); + String[] subjects = ChannelDescription.getAll().keySet().toArray(new String[0]); + Arrays.sort(subjects); + if (StringUtils.isBlank(term)) { + // nothing. just gives out everything + for (String subject : subjects) { + cache.add(new AbstractMap.SimpleImmutableEntry<>(subject, EnumChatFormatting.GRAY + subject)); + } + return cache; + } + // prefix match + for (String s : subjects) { + if (s.startsWith(term)) { + cache.add( + new AbstractMap.SimpleImmutableEntry<>( + s, + term + EnumChatFormatting.GRAY + s.substring(term.length()))); + added.add(s); + } + } + // subsequence match + for (String subject : subjects) { + if (!added.contains(subject) && subject.length() > term.length()) { + int i = 0, j = 0; + StringBuilder sb = new StringBuilder(); + boolean mode = true; + sb.append(EnumChatFormatting.WHITE); + while (i < term.length() && j < subject.length()) { + if (term.codePointAt(i) == subject.codePointAt(j)) { + if (!mode) { + sb.append(EnumChatFormatting.WHITE); + mode = true; + } + sb.appendCodePoint(term.codePointAt(i)); + i++; + j++; + } else { + if (mode) { + sb.append(EnumChatFormatting.GRAY); + mode = false; + } + sb.appendCodePoint(subject.codePointAt(j)); + j++; + } + } + if (i == term.length()) { + if (j < subject.length()) { + sb.append(EnumChatFormatting.GRAY); + sb.append(subject, j, subject.length()); + } + cache.add(new AbstractMap.SimpleImmutableEntry<>(subject, sb.toString())); + added.add(subject); + } + } + } + return cache; + } + + public void reset() { + cache = search(); + selectedIndex = cache.isEmpty() ? -1 : 0; + resetScroll(); + } + + @Override + public String getElementAt(int index) { + return index >= 0 && index < cache.size() ? getOptions().get(index).getKey() : null; + } + + @Override + public int getNumElements() { + return getOptions().size(); + } + + @Override + protected void drawElement(int elementIndex, int x, int y, int height, Tessellator tessellator, + boolean isHovering) { + int color = elementIndex == selectedIndex ? 0xffffff : 0xcccccc; + fontRendererObj.drawString(getOptions().get(elementIndex).getValue(), x + margin, y + margin / 2, color); + } + } + private class GuiChannelsList extends GuiScrollableList> { private List> cache; @@ -468,22 +786,5 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen // 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 index ae6aa50b..b39ea3e7 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScrollableList.java +++ b/src/main/java/com/gtnewhorizon/structurelib/gui/GuiScrollableList.java @@ -16,7 +16,7 @@ import org.lwjgl.opengl.GL11; *

* 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 @@ -167,10 +167,17 @@ public abstract class GuiScrollableList { } } + protected void resetScroll() { + amountScrolled = 0; + } + /** * 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) { + GL11.glPushMatrix(); + GL11.glLoadIdentity(); + GL11.glTranslatef(0.0F, 0.0F, -2000.0F); this.mouseX = mX; this.mouseY = mY; @@ -199,13 +206,26 @@ public abstract class GuiScrollableList { int contentYOffset = this.minY + margin - amountScrolled; boolean hoveringPrecondition = mX >= minX && mX <= maxX && mY >= minY && mY <= maxY; + int slotHeight = this.slotHeight - margin; + + if (selectedIndex != -1) { + int elementY = contentYOffset + selectedIndex * this.slotHeight; + if (elementY + slotHeight > maxY) { + this.amountScrolled = (selectedIndex + 1) * this.slotHeight - this.height + margin; + amountScrolled = (int) this.amountScrolled; + contentYOffset = this.minY + margin - amountScrolled; + } else if (elementY < minY) { + this.amountScrolled = selectedIndex * this.slotHeight; + amountScrolled = (int) this.amountScrolled; + contentYOffset = this.minY + margin - amountScrolled; + } + } 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) { @@ -264,6 +284,7 @@ public abstract class GuiScrollableList { GL11.glShadeModel(GL11.GL_FLAT); GL11.glEnable(GL11.GL_ALPHA_TEST); GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); } protected void prepareDrawElements() {} @@ -412,7 +433,7 @@ public abstract class GuiScrollableList { GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); GL11.glDisable(GL11.GL_TEXTURE_2D); tess.startDrawingQuads(); - tess.setColorOpaque_I(2105376); + tess.setColorOpaque_I(0x202020); tess.addVertex(minX, maxY, 0.0D); tess.addVertex(maxX, maxY, 0.0D); tess.addVertex(maxX, minY, 0.0D); diff --git a/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java b/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java index 118c954f..5246da82 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java +++ b/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java @@ -17,9 +17,6 @@ import org.lwjgl.input.Keyboard; import com.gtnewhorizon.structurelib.StructureLib; import com.gtnewhorizon.structurelib.alignment.constructable.ChannelDataAccessor; import com.gtnewhorizon.structurelib.alignment.constructable.ConstructableUtility; -import com.gtnewhorizon.structurelib.gui.GuiScreenConfigureChannels; - -import cpw.mods.fml.common.FMLCommonHandler; public class ItemConstructableTrigger extends Item { @@ -31,11 +28,11 @@ public class ItemConstructableTrigger extends Item { @Override public ItemStack onItemRightClick(ItemStack stack, World world, EntityPlayer player) { - if (world.isRemote && getMovingObjectPositionFromPlayer(world, player, true) == null) { + if (!world.isRemote && getMovingObjectPositionFromPlayer(world, player, true) == null) { if (player.isSneaking()) { StructureLib.instance().proxy().displayConfigGUI("registries"); } else { - FMLCommonHandler.instance().showGuiScreen(new GuiScreenConfigureChannels(stack)); + player.openGui(StructureLib.instance(), 0, world, player.inventory.currentItem, 0, 0); } } return super.onItemRightClick(stack, world, player); diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java index 2b6b7732..d9cba10d 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/IStructureDefinition.java @@ -395,7 +395,7 @@ public interface IStructureDefinition { basePositionC, ignoreBlockUnloaded((e, w, x, y, z, a, b, c) -> { e.spawnHint(object, world, x, y, z, trigger); - if (ChannelDataAccessor.hasSubChannel(trigger, "show_error") + if (ChannelDataAccessor.hasSubChannel(trigger, StructureLibAPI.CHANNEL_SHOW_ERROR) && !e.couldBeValid(object, world, x, y, z, trigger)) { StructureLibAPI.markHintParticleError(StructureLib.getCurrentPlayer(), world, x, y, z); } diff --git a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java index 03bb9e6b..71e379d0 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java +++ b/src/main/java/com/gtnewhorizon/structurelib/structure/StructureUtility.java @@ -614,46 +614,33 @@ public class StructureUtility { /** * Element representing a component with different tiers. Your multi will accept any of them (as long as player does - * not mix them (*)), but would like to know which is used. Player can use more blueprint to get hints denoting more + * not mix tier (*)), but would like to know which is used. Player can use more blueprint to get hints denoting more * advanced components. *

    - * (*): 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) - *

    - * Above all else, you need to decide what tier you are going to use. For simpler cases, Integer is a good choice. - * You can also use an existing Enum. Tier can never be null though. It can also be a String or a + * Above all else, you need to decide what type of tier you are going to use. For simpler cases, Integer is a good + * choice. You can also use an existing Enum. It can also be a String or a * {@link net.minecraft.util.ResourceLocation}, or basically anything. It doesn't even have to implement * {@link Comparable}. *

    - * This assumes you will reset the backing storage, and on the first occurrence getter would return notSet. You can - * also make getter to return other value to forcefully select a certain tier, but you're probably better off using - * block adders or {@link #ofBlock(Block, int, Block, int)} and its overloads + * If your tier extractor function returns null on any (Block, meta) pair, then it means this block is rejected. + * This also implies null can never be a valid tier. *

    - * Implementation wise, this allows a block only when we don't yet have a tier, or the block at that location has - * the same tier as we already have. - *

    - * Notice that if your tier extractor function returns null on any (Block, meta) pair, then it means this block is - * rejected, since null can never be a valid tier. + * This structure element require you will reset the backing storage of getter in your own code, such that in every + * round of structure checking, the first call to getter would always return notSet. You can also make getter to + * return other value on that occasion to forcefully select a certain tier, but then you'd probably be better off + * using block adders or {@link #ofBlock(Block, int, Block, int)} and its overloads *

    * There is yet no TileEntity counterpart of this utility. Feel free to submit a PR to add it. *

    - * For most typical use cases, you will NOT want to return notSet from your tierExtractor. If you do so, we will - * have this kind of chain of events: - *

      - *
    • at check start, tier is reset to notSet - *
    • at first occurrence, ofBlocksTiered finds your current tier is notSet, and the block's tier is notSet, so - * it's accepted - *
    • at 2nd, 3rd..., same thing in 2 happens - *
    • at last occurrence, player placed a valid block with a tier not notSet. ofBlocksTiered now sets your current - * tier to this tier, and accept the block - *
    • check got notified structure is sound, and tier is not notSet. it marks the structure as complete and start - * doing its business - *
    • Player enjoy a (probably) hilariously shaped multi and (probably) reduced build cost. - *
    + * (*): 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). If there are multiple (block, tier) tuple for a + * given tier, then player will be allowed to use multiple kinds of blocks, but their tier is guaranteed to be the + * same. + *
    + * Example Implementation: *

    - * Example: - *

    - * Assume you have 16 tier, each map to one block's 16 different meta. You will usually want something like this + * Assume you have 16 tier, each map to one particular block's 16 different meta. You will usually want something + * like this * *

          * public class Tile extends TileEntity {
    @@ -677,6 +664,29 @@ public class StructureUtility {
          * }
          * 
    * + * Important bits: + *
      + *
    • The supplied ITierConverter returns null for an invalid block
    • + *
    • Possible tier returned by ITierConverter is Integer object 0 to 15.
    • + *
    • notSet is set to -1. tier got reset to this value by caller before calling into + * {@link IStructureDefinition#check(Object, String, World, ExtendedFacing, int, int, int, int, int, int, boolean)}
    • + *
    • notSet is never a tier returned by ITierConverter.
    • + *
    + *
    + *

    + * You SHOULD NOT return notSet from your tierExtractor. If you do so, we will have this chain of events: + *

      + *
    • at check start, tier is reset to notSet + *
    • the tier extractor returns notSet for first block, so since ofBlocksTiered finds your current tier is notSet, + * and the new block's tier is the same as that, so it's accepted + *
    • at 2nd, 3rd..., same thing in 2 happens + *
    • at last occurrence, player placed a valid block with a tier not notSet. ofBlocksTiered now sets your current + * tier to this tier, and accept the block + *
    • check got notified structure is sound, and tier is not notSet. it marks the structure as complete and start + * doing its business + *
    • Player enjoy a (probably) hilariously shaped multi and (probably) reduced build cost. + *
    + * * @param tierExtractor a function to extract tier info from a block. This function can return null and will never * be passed a null block or an invalid block meta. If this function returns null, then the * block would be considered invalid. @@ -686,8 +696,8 @@ public class StructureUtility { * construction time, use {@link #lazy(Supplier)} or its overloads to delay a bit. This list is * only for hint particle spawning and survival build. The hint/autoplace code will choose 1st * pair to spawn/place if trigger item has master channel data of 1, 2nd pair if 2, and so on. - * @param notSet The value returned from {@code getter} when there were no tier info found in T yet. Can be - * null. + * @param notSet The value returned from {@code getter} when there were no tier info found in T yet. Cannot + * be null. * @param getter a function to retrieve the current tier from context object * @param setter a function to set the current tier into context object */ diff --git a/src/main/javadoc/overview.html b/src/main/javadoc/overview.html index dfa1f9fa..deb993f2 100644 --- a/src/main/javadoc/overview.html +++ b/src/main/javadoc/overview.html @@ -178,6 +178,80 @@ StructureLib API documentation. Just give me a better name and I'd do the renaming. +

    Channel Descriptions

    +

    + Each channel can have one description associated with it per mod. + Channel description should briefly describe how a specific channel is used for inside a specific mod. + They will be displayed to player in channel editor GUI that comes with StructureLib. + The registration process can be done either via direct method call to {@link com.gtnewhorizon.structurelib.StructureLibAPI#registerChannelDescription(String, String, String)}, + or by sending an IMC message. + After registration, the channel will also show up as autocomplete options when players are editing channels. +

    + +

    + Here is an example of sending an IMC message to register two channels and their description. + In this example, we assume your modid is my_mod and your mod will ship with localization entries for + localization key channels.my_mod.channel_1 and channels.my_mod.channel_2 +

    + +
    +        NBTTagCompound command = new NBTTagCompound();
    +        NBTTagList tags = new NBTTagList();
    +        String[] channels = {"channel_1", "channel_2"};
    +        for (String channel : channels) {
    +            NBTTagCompound tag = new NBTTagCompound();
    +            tag.setString("Channel", channel);
    +            tag.setString("Description", "channels.my_mod." + channel);
    +            tags.appendTag(tag);
    +        }
    +        command.setTag("Channels", tags);
    +        FMLInterModComms.sendMessage("structurelib", "register_channel", command);
    +
    + +

    + There is no strict rules over whether you should do direct API method call or send IMC messages. + You can choose whichever you prefer. +

    + +

    Channel Indicator Items

    +

    + You can associate an ItemStack with a given (channel, value) pair. + It is usually (but not required to be) the ItemBlock for the blocks to be placed by this (channel, value) pair. + Player will be able to drag and drop this item to the builtin channel configuration GUI. + The registration process can be done either via direct method call to {@link com.gtnewhorizon.structurelib.StructureLibAPI#registerChannelItem(String, String, int, net.minecraft.item.ItemStack)}, + or by sending an IMC message. + After registration, the channel will also show up as autocomplete options when players are editing channels. +

    + +

    + Here is an example of sending an IMC message to register 3 vanilla sandstone as different indicator for channel "sandstone", with block metadata plus 1 as channel value. + In this example, we assume your modid is my_mod. +

    + +
    +        NBTTagCompound command = new NBTTagCompound();
    +        NBTTagList items = new NBTTagList();
    +        int[] metas = {0, 1, 2};
    +        for (int meta : metas) {
    +            NBTTagCompound tag = new NBTTagCompound();
    +            tag.setTag("Item", new ItemStack(Blocks.sandstone, 1, meta).writeToNBT(new NBTTagCompound()));
    +            NBTTagList channels = new NBTTagList();
    +            NBTTagCompound channel = new NBTTagCompound();
    +            channel.setString("Channel", "sandstone");
    +            channel.setInteger("Value", meta + 1);
    +            channels.appendTag(channel);
    +            tag.setTag("Channels", channels);
    +            items.appendTag(tag);
    +        }
    +        command.setTag("Items", items);
    +        FMLInterModComms.sendMessage("structurelib", "register_channel_item", command);
    +
    + +

    + There is no strict rules over whether you should do direct API method call or send IMC messages. + You can choose whichever you prefer. +

    +

    Examples

    For Structure Definition Author

    diff --git a/src/main/resources/assets/structurelib/lang/en_US.lang b/src/main/resources/assets/structurelib/lang/en_US.lang index e188dac1..c2b7b238 100644 --- a/src/main/resources/assets/structurelib/lang/en_US.lang +++ b/src/main/resources/assets/structurelib/lang/en_US.lang @@ -36,10 +36,11 @@ item.structurelib.constructableTrigger.gui.add=Add item.structurelib.constructableTrigger.gui.set=Set item.structurelib.constructableTrigger.gui.unset=Unset item.structurelib.constructableTrigger.gui.wipe=§4Wipe -item.structurelib.constructableTrigger.gui.error.show=Show Errors -item.structurelib.constructableTrigger.gui.error.hide=Hide Errors -item.structurelib.constructableTrigger.gui.gt_no_hatch.enable=No Hatch -item.structurelib.constructableTrigger.gui.gt_no_hatch.disable=Hatches +item.structurelib.constructableTrigger.gui.channels.from=§rMod: §9§o%s + +channels.structurelib.show_errors=When set, will highlight parts of the multiblock that is definitely wrong + +structurelib.tooltip.channelvalue=§7Indicates value §6%d§r§7 for sub channel §6%s 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 26dc2ee9..01010415 100644 --- a/src/main/resources/assets/structurelib/lang/zh_CN.lang +++ b/src/main/resources/assets/structurelib/lang/zh_CN.lang @@ -36,10 +36,11 @@ item.structurelib.constructableTrigger.gui.add=添加 item.structurelib.constructableTrigger.gui.set=替换 item.structurelib.constructableTrigger.gui.unset=删除 item.structurelib.constructableTrigger.gui.wipe=§4全部清空 -item.structurelib.constructableTrigger.gui.error.show=显示结构错误 -item.structurelib.constructableTrigger.gui.error.hide=隐藏结构错误 -item.structurelib.constructableTrigger.gui.gt_no_hatch.enable=跳过舱室 -item.structurelib.constructableTrigger.gui.gt_no_hatch.disable=放置舱室 +item.structurelib.constructableTrigger.gui.channels.from=§r模组: §9§o%s + +channels.structurelib.show_errors=添加后,在投影时会高亮标出必然错误的部分 + +structurelib.tooltip.channelvalue=§7表示子信道 §6%2$s§r§7 为 §6%d§r structurelib.autoplace.built_stat=已放置 %s 个方块 structurelib.autoplace.complete=§a这个多方块结构已经完成自动搭建! diff --git a/src/main/resources/assets/structurelib/textures/gui/channels.png b/src/main/resources/assets/structurelib/textures/gui/channels.png index 4726863dbf86452e2de318a3937175b7f9999f02..c76cdf33446316a04eb6393364377cd34ad275a3 100644 GIT binary patch literal 6738 zcmeHMcT`hZx4(e&ViXHajiCr=dO`^VDWQo`1QZY~DK`)xBqo7G6a*>8Kjco1T(cgc_;c zrL#b_C<3G#<+qYcS{Be{ZYU;SHmNHd(pN-E?I$zl zB2@#Cz4@qs`@R{SCu)4=D&MIK+Nz8A%g#J~yJWsf;>#TaRxXl-@+eKSrq=CA7o{_l zmM`B_p0R004-go6-ZP!31!v#rR?)++K4NdtcCTbnif=&8nt*X$KtSAcT?|o4MSafP zf%*N96i*#eTR6&pKl@F}?MzL)hP4et52hMLI#?EumX1q##_$`@L65y^(qSh`@7o=! z!3xxj8#=oUG&X0+KS4-XI@&5Kz(solqfrBfmR$N0Kyq!!%`6fQ%jikii=ClbVs_hy z;jXnM0mc^N9Ny)ESQw6>?_#7?hJjwVUp$ntb}M(f($$D&@nsTg|bakf%3;g(zc zjx^hwl9SC=?CneQn`|3TpFdLqTecXv{@h>YMaB6lMB@T?f9Laa&O9iV94m<-{b7&H zl+^D!KyC7E=UMiqCT|qhBbs~X6Gq^v>$&ifjU#ns>HgZ)jRldDZp*Hlf zq-|P*kFNK<_>PZmg@<+|%Ud0Wb>HrW1l zI2l>A{vOi&L9%9aiMhVBgJOytDf`u71^-)b)cXRJ)~QlMIl2>eyEo=OG<_|(w|@ur ze6@Sm`4?jj1rMGUHI0rh{d-`!n zg?YjTIlILh-&x%LSRZ#O@7CSK83_t`)ftaTLrOLe{=8dyyzSmJg>5kMB_e3hu-f>N zvu5)Pd_k)oL1F9OS2q<`RVMj|G>RN=28j*!T!YOYthI{S=V&CItd?dTOrl`zbctJik zZmZL5|GkZ{_P$HF2*(s=czkp3u>1#cyH?%j%Td~0$BLwelC-^Ak}Tt#EmXh zkqk2wEmAsrrKBda$07ipR#iC8R;rMijm9Wdh`a_@%i7|jaVjb3gO_^{l7#pBK06em zYir0W3r1YDg$r7Pthxp~InU=j_lVrSDyiJYfeN30K|#-2l+;WfSVOxWxI|#zmO-(J zc6~;CCvH9&42L}d-`=)izRO(va0~O@R^h&- zBU=PC`d`l}8Z(DiF-=DvnRG8s;tyD;-_p4mUFLo{H2Z9#>OiZ_fw0gZQ@Q?#+y>)} z*6>QL8`H0s+k~Fg7%AtkQ}1+2|D>~LTRo?L_po+H=H`=qSlFthl!$=W4m2HrGx@h*pxnKS%mo2l*(9EeDszRAe{ zf?ccocj{>5<1k#&0(-zZ0y7Nbs3H(b<>f0FyB$$EutmxdzqK7DBGI@sUq!S$^; zj;>tyGUlpGsIR;&@a5<0*Q7V&O&|7H)?B!Ax2I#>qP&xy3_tOwL2cV)QUA4w#jw_1CH`V9ejc|HaY98#Q;UNIefD3*ov8A}daaxBs^IUM7#Kgp=ReiaXC zHCpNy!P|7V7Cw*G~_@~n<_d_%$> z^l7!vzSCeXu=B|&O|4${>kR^>on+gsm8pYEMg-f^<;sIG)rPQJSgPet%j5z}>aIb% zO$nMGcjU}GQso3|7)J!ufO_Nx!(ifJ3Gxl zUE&lM{OQ`n6H%E9n_uIuW*qx@ZsSKCxv?wuSJ?iKrH4A~jKV}$wOzcNH%ol@}n*%F}NHQjmf2hC^3fzoyh>e zcBz<0V}yc2I2{aPhm#QlSE>+jHj|9-B~md|o)Z|%c8lVJ-cc)j7*U}NYbIi;otmwf z1OaeBAq_6(goO)8VlrY9mjtzCVl)CisUi#|BmAhIa3?MwgcDE%6b9)mW=G->c4}~2 zK9fcAqPTp4fO=#^uu#Y&q0u6d2qnU!xcneA*4o+{jlrRDI3%Qj6l@L`(!|JcfuRgy z3WEX)7<@KQ$mWK_WtcQNH$q58AfR#h*Z4R*D)k$DxZn#55FcnUjfcjfFlY`3{k?@i z=o|@wd4A40e3>O-H55Z)7v*$(d!zR;VGSFZc$bnP^kXP&v zE?wQJp5H8F6a=w3yh$sF>>o6RY}Vgo{ScdMWHO!a1A)xH;r^igHTOwnNQ+7(QMil< zS$OUgGD7B`#N;yAOwweN#bR1pFj!0^k;t$_5-^}O(h6ruK;l`JM5YDHl0dY=eFx

    wAXw0_-$AV8v!SY_ zg?%5D42lUsfh;B-WHLY`9*?s^66iP@(uzUABJoxPJOCQeGr1zc-$OoZ4(KhU$@s)tSYR<$ zmKK&oEXEp-or-!AaSg~9K(#1C#bQwSDN1A$LxR$QsHMp&6#|%qg|Zk%{o?J|D_~f!6(HK)H2xyTYb23f{YifuQObZVJ zq5b1a!G4vq|4lJ4=?pB5&a_5a5}-ODV62%?JuwMLBAtjMU@Zw&L~H9G(FI(VP(HG(;FZ1vp^Z-Hsa`IFB{-Wy_T|dRZPbvSZu3vQh6azn{ z{HwbD&*)P7`(X+UhfY8u=uydg-;f>jn59JbaHRkfvQKtp!G1`h%5(D<0D!u->?;Qx z$<%>_%0hRlv+{G5*_v90T*pmg03iR;o#NmFy%>L3$*IM_VsA4xRvVVS)oYhG_x2D- zJYFikAx?9Mox7}_(6WJ}pn7dY&nYp`K)4`y_KJ6Blr9*OUzayqORo+TN*B(z+u>I< zUT~ssWnqj9ymzcsbkK|bcm&luO8>xazQn<5#CyNqFAq?W6X%vFy?c5bs8Z6HIQUE? z4@?7KfW7Qh{=YP`gOm-kLF-=AJe>rfwdnQxkFDcmWeO1d$4PZts&~`in|S~nh=uY1 zRQ|ih#LfwYY5Q3(UG$MYX0|EY}Mg3;3F?p!tEK2 z7>}~kR~w=mNTX6EuPg2bQ07TXuj;4|^+d1h5tHgaRoqQhk&8`#cz9`gf%jip0BM`@ ZTRAsJ-=S^=*eh8K?#?SH#f}@d{};Q?{`vp_ literal 2825 zcmeHJeNYo;8ei&11++S2MXJuYtXB_uZn9q?*_D7oXoxisffgLmy4l@?t;ue>yFh}X zRBOGq<1y{k)}9D+WmNicw%V~D(kUJYSFdOBhN+HYp<3)wu#^uE(edlv4GE<)&e;EZ znKzsFefIbK-skx}&-+eZbmiJ-#Vv?~AZV5&$CeL4F<=@4&6o~;N|Z(qSnLq9vRsa= zEZC*U9?>U2P;$qfSDeYVHH+G|kzf4oC>HNbke}O=l60hVPRcV28n&RHjkV7-4ZhjE z?^JvK`N!fHFWj+nw`pDcx&yD(yn0~Iyd$sF-K}cy#4}O2P ze237x|77v%eQ)mX?5~ggcjlBGHm&otGk5pWx0!S)wsCanJ4dg7Te=GUbm>&;QWDvF zdFT9{H_wgGZA~q#x;8JiKCXgRR$khCVBw=%*VGQvjYMlMfiGK>aF&?oEBxToAB$V6 zS_V?SeErtN)h`yjbZ*|d^V@Hzd7~}uk8ZA9)kj=NeA%Al(+HeWqEaDXJbY$3?2JERKKfktZb^O zCcwUN``zrs;QXq0-#fN=#ntJx>t8o<4}N9->uA!U<;RU*e=+l5=TIf`EftTRJA*yl zKj5wJS)Q}1Xl~I5AJ5+Z$f*sDBR{zWd0XA^%_Y}nH6hAL`m3WxP?gv~&RP|M;^*q$ z80asxi4Zg`Q!Fgjik+(&PWBsEUUmzHpg#c65R{e?46xiLL4)0bSCp*CU{f;!i@X&n zGC474AWJ9}b1D^~pfb0RtK7uVJd%+XMZ&}knCoUE0*4{m zCM#0xbirA&BEXb^GGJ(SP%I}9Kmx;Q3h!a^ZI4Gtz{-l0YFdCX8Y?O)3>Bn7R=h@> zrfDNa7zqLe2viM88XH6k!myIhd8B~f>toA3>kDZX$P zJZBVqf*-(Cup2(HmEGZVO`!Fxy`n!5_5cb`pfu6*vo^noM}HFzKe{<^o8bEeGMc9j z1Y$5YnJd%daoDVgzA3}YoX9ibsmH_9sho#LO(xEaQkX!a7Q#%Sq{nRHQ$1$NWFewF z4oTHmi4$}lz-$l!i3Jdt2s43tD3(MimUW{n&Rb9qL1HXHQdBC7NB#2@5wsrbn;ehM z!vh|{!;=Ef2`EVt7L;-mENbB>93?FjiIE&nlDvSxJjXmPD}EM~O!Tu}!5EOd2&^m1 ztaLf72w}h?OP7z;JOB#%TavAT44YATLd%&BG*j?p9z!VD8VB`7NB zEu9R<3?#^2SDpdE0|{BZ?SN}IB_>M|SWQ+6W!Yy%bZ1!i8Gq*lhl6J|*2ZcA;KB%k z!7L1c7ZNywQw(YP16X2Fu*{2|(EXs{qXdIb+IU@aL>25FnlMKWNP+O|BvZub6X9^L zGb}frg36W)JQ6W-rL5!?z-0jXa2zJyqt?8e!&x^^qh<<}nZjru^a@X*Cbx;8a5H5w z(R9>bl|5Pos|YK-Ks6u^XuGgDaB`#`DN$rasi2Du1VS+jI!Pb|HcrYT&8Q#t$+@K& z?1CK0kO-VdjcKHF?{&3Q0=KlN*&m_OKepTc1H=6U}Iui1ye>qs=?+cl3 zm)F{hw;xi+e!O~ZWJIY927~l`qHgwg4WYT|pMTx2_5Ug3ANX?}&sJ4gySlrVP6-TO z$8W5Ct@RT2w{usH^qsVJ&VZJ+@4aii+$;y_8v zioMYNM%#*I|Gd(+f=fD6{4e!zzZ;I(xaLkmwYsNhN*wg^NXB7ElBD*9FT5HvG!tr> z`m4uhv#Ya1x6`YenwmN*@2H{0(6TeZKX+{2+ib8y@aq4M->L$>(d%?NGqC8PU1C#L z-A9)iKnHV)y_o#pEz>rRWOWo+Yp#q|c0XU!@&4VL!4R~q>%kMm-do{6D))b!$XI)N z$lT>p8=`rX5pYtx|2 p|D4Kb?;L3MLUV?D$?BVZzpv}4*>J3NYqh?iBRki2ICJCn{{hghkO2Sy