From d1dec41c1171b81f0fab5740b8b4c605a24b1a6b Mon Sep 17 00:00:00 2001 From: Glease <4586901+Glease@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:28:26 +0800 Subject: [PATCH] add gui for customizing registries --- .../structurelib/ClientProxy.java | 15 +- .../structurelib/CommonProxy.java | 28 + .../structurelib/ConfigurationHandler.java | 26 +- .../structurelib/GuiEditRegistryConfig.java | 698 ++++++++++++++++++ .../structurelib/RegistryOrderEntry.java | 77 ++ .../structurelib/SortedRegistry.java | 50 +- .../structurelib/StructureLib.java | 2 + .../command/CommandRegistryDebug.java | 52 ++ .../item/ItemConstructableTrigger.java | 9 +- .../assets/structurelib/lang/en_US.lang | 16 + .../assets/structurelib/lang/zh_CN.lang | 16 + .../textures/gui/fourwayarrow2.png | Bin 0 -> 367 bytes src/main/resources/structurelib_at.cfg | 1 + 13 files changed, 981 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/gtnewhorizon/structurelib/GuiEditRegistryConfig.java create mode 100644 src/main/java/com/gtnewhorizon/structurelib/RegistryOrderEntry.java create mode 100644 src/main/java/com/gtnewhorizon/structurelib/command/CommandRegistryDebug.java create mode 100644 src/main/resources/assets/structurelib/textures/gui/fourwayarrow2.png create mode 100644 src/main/resources/structurelib_at.cfg diff --git a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java index 3ff4af93..733bbcf5 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java +++ b/src/main/java/com/gtnewhorizon/structurelib/ClientProxy.java @@ -5,6 +5,7 @@ import static com.gtnewhorizon.structurelib.StructureLib.RANDOM; import java.lang.ref.WeakReference; import java.util.*; +import cpw.mods.fml.client.config.GuiConfig; import net.minecraft.block.Block; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityClientPlayerMP; @@ -13,6 +14,7 @@ import net.minecraft.client.particle.EntityFX; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.culling.Frustrum; import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.resources.I18n; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; @@ -25,6 +27,7 @@ import net.minecraft.util.Vec3; import net.minecraft.world.World; import net.minecraftforge.client.event.RenderWorldLastEvent; import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.config.ConfigElement; import net.minecraftforge.event.entity.EntityJoinWorldEvent; import net.minecraftforge.event.world.WorldEvent; @@ -261,8 +264,10 @@ public class ClientProxy extends CommonProxy { FMLEventHandler.configSent = true; for (String s : SortedRegistry.ALL_REGISTRIES.keySet()) { Pair, List> registryOrder = ConfigurationHandler.INSTANCE.getRegistryOrder(s); - if (registryOrder == null || registryOrder.getKey().isEmpty() && registryOrder.getValue().isEmpty()) continue; - StructureLib.net.sendToServer(new RegistryOrderSyncMessage(s, registryOrder.getKey(), registryOrder.getValue())); + if (registryOrder == null || registryOrder.getKey().isEmpty() && registryOrder.getValue().isEmpty()) + continue; + StructureLib.net + .sendToServer(new RegistryOrderSyncMessage(s, registryOrder.getKey(), registryOrder.getValue())); } } @@ -281,6 +286,12 @@ public class ClientProxy extends CommonProxy { return EntityPlayer.func_146094_a(Minecraft.getMinecraft().getSession().func_148256_e()); } + public void displayConfigGUI(String category) { + ConfigElement element = new ConfigElement<>(ConfigurationHandler.INSTANCE.getConfig().getCategory(category)); + GuiConfig guiConfig = new GuiConfig(null, element.getChildElements(), StructureLibAPI.MOD_ID, null, false, false, I18n.format(element.getLanguageKey())); + Minecraft.getMinecraft().displayGuiScreen(guiConfig); + } + private static class HintGroup { private final List hints = new LinkedList<>(); diff --git a/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java b/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java index 3759da0e..cab04365 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java +++ b/src/main/java/com/gtnewhorizon/structurelib/CommonProxy.java @@ -12,14 +12,23 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.util.IChatComponent; import net.minecraft.util.IIcon; import net.minecraft.world.World; +import net.minecraftforge.common.MinecraftForge; import com.gtnewhorizon.structurelib.net.ErrorHintParticleMessage; import com.gtnewhorizon.structurelib.net.UpdateHintParticleMessage; + import cpw.mods.fml.common.event.FMLLoadCompleteEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.TickEvent; public class CommonProxy { + public CommonProxy() { + super(); + MinecraftForge.EVENT_BUS.register(new ForgeEventHandler()); + } + public void hintParticleTinted(World w, int x, int y, int z, IIcon[] icons, short[] RGBa) {} public void hintParticleTinted(World w, int x, int y, int z, Block block, int meta, short[] RGBa) {} @@ -98,4 +107,23 @@ public class CommonProxy { if (!forceUpdateLastSend) submap.put(throttleKey, now); } } + + public void displayConfigGUI(String category) { + } + + public static class ForgeEventHandler { + + private int ticksSinceLastPurge; + + @SubscribeEvent + public void onServerTickEnd(TickEvent.ServerTickEvent e) { + if (e.phase != TickEvent.Phase.END) return; + if (++ticksSinceLastPurge < 20 * 60 * 10) return; + ticksSinceLastPurge = 0; + MinecraftServer server = MinecraftServer.getServer(); + // it's hard to tell why server could be null in a server tick handler, but who knows? + if (server == null) return; + SortedRegistry.cleanup(server); + } + } } diff --git a/src/main/java/com/gtnewhorizon/structurelib/ConfigurationHandler.java b/src/main/java/com/gtnewhorizon/structurelib/ConfigurationHandler.java index 5fa1d03c..0f366e4a 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/ConfigurationHandler.java +++ b/src/main/java/com/gtnewhorizon/structurelib/ConfigurationHandler.java @@ -12,13 +12,15 @@ import net.minecraftforge.common.config.ConfigCategory; import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Property; +import org.apache.commons.lang3.tuple.Pair; + import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import cpw.mods.fml.client.event.ConfigChangedEvent; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import org.apache.commons.lang3.tuple.Pair; +import cpw.mods.fml.relauncher.FMLLaunchHandler; public enum ConfigurationHandler { @@ -102,13 +104,27 @@ public enum ConfigurationHandler { } void loadRegistryOrder() { + loadRegistryOrderImpl(); + setLanguageKeys(); + saveConfig(); + } + + private void loadRegistryOrderImpl() { registryOrders = new HashMap<>(); for (Map.Entry>> e : SortedRegistry.ALL_REGISTRIES.entrySet()) { SortedRegistry r = e.getValue().get(); if (r == null) continue; - Property pOrder = config - .get("registries." + e.getKey(), "ordering", Iterables.toArray(r.getCurrentOrdering(), String.class), "stuff not in this list will be automatically available after all entries listed here in their natural order, unless explicitly disabled in disabled config below."); - Property pDisable = config.get("registries." + e.getKey(), "disabled", new String[0], "stuff in this list will be disabled"); + String category = "registries." + e.getKey(); + if (FMLLaunchHandler.side().isClient()) { + // only meaningful on client, and would crash on server + config.setCategoryConfigEntryClass(category, RegistryOrderEntry.class); + } + Property pOrder = config.get( + category, + "ordering", + Iterables.toArray(r.getCurrentOrdering(), String.class), + "stuff not in this list will be automatically available after all entries listed here in their natural order, unless explicitly disabled in disabled config below."); + Property pDisable = config.get(category, "disabled", new String[0], "stuff in this list will be disabled"); List all = Lists.newArrayList(r.getCurrentOrdering()); List curVal = new ArrayList<>(Arrays.asList(pOrder.getStringList())); List disabled = new ArrayList<>(Arrays.asList(pDisable.getStringList())); @@ -164,7 +180,9 @@ public enum ConfigurationHandler { return registryOrders.get(name); } + Configuration getConfig() { return config; } + } diff --git a/src/main/java/com/gtnewhorizon/structurelib/GuiEditRegistryConfig.java b/src/main/java/com/gtnewhorizon/structurelib/GuiEditRegistryConfig.java new file mode 100644 index 00000000..149faecc --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/GuiEditRegistryConfig.java @@ -0,0 +1,698 @@ +/* + * Forge Mod Loader Copyright (c) 2012-2014 cpw. All rights reserved. This program and the accompanying materials are + * made available under the terms of the GNU Lesser Public License v2.1 which accompanies this distribution, and is + * available at http://www.gnu.org/licenses/old-licenses/gpl-2.0.html Contributors (this class): bspkrs - implementation + */ + +package com.gtnewhorizon.structurelib; + +import static cpw.mods.fml.client.config.GuiUtils.RESET_CHAR; +import static cpw.mods.fml.client.config.GuiUtils.UNDO_CHAR; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.WeakHashMap; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiListExtended; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.MathHelper; +import net.minecraft.util.ResourceLocation; + +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.GL11; + +import cpw.mods.fml.client.config.GuiButtonExt; +import cpw.mods.fml.client.config.GuiConfig; +import cpw.mods.fml.client.config.GuiUnicodeGlyphButton; +import cpw.mods.fml.client.config.GuiUtils; +import cpw.mods.fml.client.config.HoverChecker; +import cpw.mods.fml.client.config.IConfigElement; +import cpw.mods.fml.client.event.ConfigChangedEvent; +import cpw.mods.fml.client.event.ConfigChangedEvent.OnConfigChangedEvent; +import cpw.mods.fml.client.event.ConfigChangedEvent.PostConfigChangedEvent; +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.eventhandler.Event.Result; + +/** + * This class is the base GuiScreen for all config GUI screens. It can be extended by mods to provide the top-level + * config screen that will be called when the Config button is clicked from the Main Menu Mods list. + * + * @author bspkrs + */ +public class GuiEditRegistryConfig extends GuiScreen { + + public static final String OMITTED = ".."; + public static final int LIST_WINDOW_PADDING = 20; + public static final int MID_SECTION_WIDTH = 10; + /** + * A reference to the screen object that created this. Used for navigating between screens. + */ + public final GuiScreen parentScreen; + private final IConfigElement disabledElement; + private final String[] beforeDisabledValue; + private final IConfigElement orderElement; + private final String[] beforeOrderValue; + final Collection lists; + private final String registryName; + EntriesList disabledEntries; + EntriesList orderEntries; + public String title = "Config GUI"; + public String titleLine2; + private GuiButtonExt btnDefault; + private GuiButtonExt btnUndoAll; + public final String modID; + /** + * When set to a non-null value the OnConfigChanged and PostConfigChanged events will be posted when the Done button + * is pressed if any configElements were changed (includes child screens). If not defined, the events will be posted + * if the parent gui is null or if the parent gui is not an instance of GuiConfig. + */ + public final String configID; + public final boolean isWorldRunning; + public final boolean allRequireWorldRestart; + public final boolean allRequireMcRestart; + public boolean needsRefresh = true; + private HoverChecker undoHoverChecker; + private HoverChecker resetHoverChecker; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public GuiEditRegistryConfig(GuiScreen parentScreen, IConfigElement disabled, IConfigElement order, + String registryName, String modID, String configID, boolean allRequireWorldRestart, + boolean allRequireMcRestart, String title, String titleLine2) { + this.mc = parentScreen.mc; + this.lists = Collections.newSetFromMap(new WeakHashMap<>()); + this.disabledElement = disabled; + this.beforeDisabledValue = (String[]) this.disabledElement.getList(); + this.orderElement = order; + this.beforeOrderValue = (String[]) this.orderElement.getList(); + this.parentScreen = parentScreen; + this.allRequireWorldRestart = allRequireWorldRestart; + this.allRequireMcRestart = allRequireMcRestart; + this.registryName = registryName; + this.modID = modID; + this.configID = configID; + this.isWorldRunning = mc.theWorld != null; + if (title != null) this.title = title; + this.titleLine2 = titleLine2; + if (this.titleLine2 != null && this.titleLine2.startsWith(" > ")) + this.titleLine2 = this.titleLine2.replaceFirst(" > ", ""); + } + + /** + * Adds the buttons (and other controls) to the screen in question. + */ + @SuppressWarnings("unchecked") + @Override + public void initGui() { + Keyboard.enableRepeatEvents(true); + if (this.disabledEntries == null || this.needsRefresh) { + this.disabledEntries = new EntriesList( + (width - MID_SECTION_WIDTH) / 2 - LIST_WINDOW_PADDING, + LIST_WINDOW_PADDING, + I18n.format("structurelib.configgui.disabled")); + this.disabledEntries.initEntries(Arrays.asList(disabledElement.getList())); + } else { + this.disabledEntries.reinit((width - MID_SECTION_WIDTH) / 2 - LIST_WINDOW_PADDING, LIST_WINDOW_PADDING); + } + if (this.orderEntries == null || this.needsRefresh) { + this.orderEntries = new EntriesList( + (width - MID_SECTION_WIDTH) / 2 - LIST_WINDOW_PADDING, + (width + MID_SECTION_WIDTH) / 2, + I18n.format("structurelib.configgui.enabled")); + this.orderEntries.initEntries(Arrays.asList(orderElement.getList())); + } else { + this.orderEntries + .reinit((width - MID_SECTION_WIDTH) / 2 - LIST_WINDOW_PADDING, (width + MID_SECTION_WIDTH) / 2); + } + this.needsRefresh = false; + + int undoGlyphWidth = mc.fontRenderer.getStringWidth(UNDO_CHAR) * 2; + int resetGlyphWidth = mc.fontRenderer.getStringWidth(RESET_CHAR) * 2; + int doneWidth = Math.max(mc.fontRenderer.getStringWidth(I18n.format("gui.done")) + 20, 100); + int undoWidth = mc.fontRenderer.getStringWidth(" " + I18n.format("fml.configgui.tooltip.undoChanges")) + + undoGlyphWidth + + 20; + int resetWidth = mc.fontRenderer.getStringWidth(" " + I18n.format("fml.configgui.tooltip.resetToDefault")) + + resetGlyphWidth + + 20; + int buttonWidthHalf = (doneWidth + 5 + undoWidth + 5 + resetWidth + 5) / 2; + this.buttonList.add( + new GuiButtonExt( + 2000, + this.width / 2 - buttonWidthHalf, + this.height - 29, + doneWidth, + 20, + I18n.format("gui.done"))); + this.buttonList.add( + this.btnDefault = new GuiUnicodeGlyphButton( + 2001, + this.width / 2 - buttonWidthHalf + doneWidth + 5 + undoWidth + 5, + this.height - 29, + resetWidth, + 20, + " " + I18n.format("fml.configgui.tooltip.resetToDefault"), + RESET_CHAR, + 2.0F)); + this.buttonList.add( + btnUndoAll = new GuiUnicodeGlyphButton( + 2002, + this.width / 2 - buttonWidthHalf + doneWidth + 5, + this.height - 29, + undoWidth, + 20, + " " + I18n.format("fml.configgui.tooltip.undoChanges"), + UNDO_CHAR, + 2.0F)); + + this.undoHoverChecker = new HoverChecker(this.btnUndoAll, 800); + this.resetHoverChecker = new HoverChecker(this.btnDefault, 800); + } + + /** + * Called when the screen is unloaded. Used to disable keyboard repeat events + */ + @Override + public void onGuiClosed() { + if (this.configID != null && this.parentScreen instanceof GuiConfig) { + GuiConfig parentGuiConfig = (GuiConfig) this.parentScreen; + parentGuiConfig.needsRefresh = true; + parentGuiConfig.initGui(); + } + + if (!(this.parentScreen instanceof GuiConfig)) Keyboard.enableRepeatEvents(false); + } + + public boolean saveConfigElements() { + if (isChanged()) { + disabledElement.set(disabledEntries.entries.stream().map(e -> e.name).toArray(String[]::new)); + orderElement.set(orderEntries.entries.stream().map(e -> e.name).toArray(String[]::new)); + } + return false; + } + + @Override + protected void actionPerformed(GuiButton button) { + if (button.id == 2000) { + try { + if ((configID != null || this.parentScreen == null || !(this.parentScreen instanceof GuiConfig)) + && isChanged()) { + saveConfigElements(); + if (Loader.isModLoaded(modID)) { + ConfigChangedEvent event = new OnConfigChangedEvent(modID, configID, isWorldRunning, false); + FMLCommonHandler.instance().bus().post(event); + if (!event.getResult().equals(Result.DENY)) FMLCommonHandler.instance().bus() + .post(new PostConfigChangedEvent(modID, configID, isWorldRunning, false)); + + if (this.parentScreen instanceof GuiConfig) ((GuiConfig) this.parentScreen).needsRefresh = true; + } + } + } catch (Throwable e) { + e.printStackTrace(); + } + + this.mc.displayGuiScreen(this.parentScreen); + } else if (button.id == 2001) { + setToDefault(); + } else if (button.id == 2002) { + undoChanges(); + } + } + + public boolean isInitialized() { + return disabledEntries != null && orderEntries != null; + } + + public void undoChanges() { + disabledEntries.initEntries(Arrays.asList(beforeDisabledValue)); + orderEntries.initEntries(Arrays.asList(beforeOrderValue)); + } + + public void setToDefault() { + disabledEntries.initEntries(Arrays.asList(disabledElement.getList())); + orderEntries.initEntries(Arrays.asList(orderElement.getList())); + } + + public boolean isDefault() { + return !orderEntries.areContentEqual(orderElement.getDefaults()) + || !disabledEntries.areContentEqual(disabledElement.getDefaults()); + } + + public boolean isChanged() { + return !orderEntries.areContentEqual(beforeOrderValue) || !disabledEntries.areContentEqual(beforeDisabledValue); + } + + /** + * Called when the mouse is clicked. + */ + @Override + protected void mouseClicked(int x, int y, int mouseEvent) { + if (mouseEvent != 0 || !this.disabledEntries.func_148179_a(x, y, mouseEvent) + && !this.orderEntries.func_148179_a(x, y, mouseEvent)) { + super.mouseClicked(x, y, mouseEvent); + } + } + + /** + * Called when the mouse is moved or a mouse button is released. Signature: (mouseX, mouseY, which) which==-1 is + * mouseMove, which==0 or which==1 is mouseUp + */ + @Override + protected void mouseMovedOrUp(int x, int y, int mouseEvent) { + if (mouseEvent != 0 || !this.disabledEntries.func_148181_b(x, y, mouseEvent) + && !this.orderEntries.func_148181_b(x, y, mouseEvent)) { + super.mouseMovedOrUp(x, y, mouseEvent); + } + } + + /** + * Fired when a key is typed. This is the equivalent of KeyListener.keyTyped(KeyEvent e). + */ + @Override + protected void keyTyped(char eventChar, int eventKey) { + if (eventKey == Keyboard.KEY_ESCAPE) this.mc.displayGuiScreen(parentScreen); + } + + String trimStringToWidth(String str, int width) { + int strwidth = mc.fontRenderer.getStringWidth(str); + int elipsisWidth = mc.fontRenderer.getStringWidth(OMITTED); + if (strwidth > width - elipsisWidth && elipsisWidth > strwidth) { + return mc.fontRenderer.trimStringToWidth(str, width - elipsisWidth).trim() + OMITTED; + } + return str; + } + + /** + * Draws the screen and all the components in it. + */ + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + for (EntriesList list : lists) { + list.handleDrag(mouseX, mouseY, partialTicks); + } + this.drawDefaultBackground(); + for (EntriesList list : lists) { + list.drawScreen(mouseX, mouseY, partialTicks); + } + this.drawCenteredString(this.fontRendererObj, this.title, this.width / 2, 8, 16777215); + String title2 = this.titleLine2; + + if (title2 != null) { + title2 = trimStringToWidth(title2, width - 6); + this.drawCenteredString(this.fontRendererObj, title2, this.width / 2, 18, 16777215); + } + + this.btnUndoAll.enabled = isChanged(); + this.btnDefault.enabled = isDefault(); + super.drawScreen(mouseX, mouseY, partialTicks); + for (EntriesList list : lists) { + list.drawScreenPost(mouseX, mouseY, partialTicks); + } + if (this.undoHoverChecker.checkHover(mouseX, mouseY)) this.drawToolTip( + this.mc.fontRenderer.listFormattedStringToWidth(I18n.format("fml.configgui.tooltip.undoAll"), 300), + mouseX, + mouseY); + if (this.resetHoverChecker.checkHover(mouseX, mouseY)) this.drawToolTip( + this.mc.fontRenderer.listFormattedStringToWidth(I18n.format("fml.configgui.tooltip.resetAll"), 300), + mouseX, + mouseY); + } + + @SuppressWarnings("rawtypes") + public void drawToolTip(List stringList, int x, int y) { + this.func_146283_a(stringList, x, y); + } + + private class EntriesList extends GuiListExtended { + + final String heading; + final List entries = new ArrayList<>(); + int controlX; + + public EntriesList(int width, int left, String heading) { + super( + mc, + width, + GuiEditRegistryConfig.this.height, + titleLine2 != null ? 33 : 23, + GuiEditRegistryConfig.this.height - 32, + 20); + this.heading = heading; + if (heading != null) { + setHasListHeader(true, mc.fontRenderer.FONT_HEIGHT + 4); + } + setSlotXBoundsFromLeft(left); + this.controlX = this.getScrollBarX() - 20; + lists.add(this); + } + + @Override + public void drawScreen(int p_148128_1_, int p_148128_2_, float p_148128_3_) { + GL11.glEnable(GL11.GL_SCISSOR_TEST); + ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + GL11.glScissor((left - 4) * scaledresolution.getScaleFactor(), (int) ((scaledresolution.getScaledHeight_double() - (bottom + 4)) * scaledresolution.getScaleFactor()), (width + 8) * scaledresolution.getScaleFactor(), (height + 8) * scaledresolution.getScaleFactor()); + super.drawScreen(p_148128_1_, p_148128_2_, p_148128_3_); + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } + + @Override + public void overlayBackground(int p_148136_1_, int p_148136_2_, int p_148136_3_, int p_148136_4_) { + } + + @Override + protected void drawContainerBackground(Tessellator tessellator) { + GL11.glEnable(GL11.GL_BLEND); + OpenGlHelper.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, 0, 1); + GL11.glDisable(GL11.GL_ALPHA_TEST); + GL11.glShadeModel(GL11.GL_SMOOTH); + GL11.glDisable(GL11.GL_TEXTURE_2D); + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_I(0, 64); + tessellator.addVertex(left, bottom, 0); + tessellator.addVertex(right, bottom, 0); + tessellator.addVertex(right, top, 0); + tessellator.addVertex(left, top, 0); + tessellator.draw(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glDisable(GL11.GL_BLEND); + } + + @Override + protected void drawListHeader(int p_148129_1_, int p_148129_2_, Tessellator p_148129_3_) { + if (heading == null) { + return; + } + FontRenderer fr = mc.fontRenderer; + fr.drawString( + this.heading, + p_148129_1_ + this.width / 2 - fr.getStringWidth(this.heading) / 2, + Math.min(this.top + 3, p_148129_2_), + 0xffffff); + } + + void reinit(int width, int left) { + this.width = width; + this.bottom = GuiEditRegistryConfig.this.height - 32; + this.setSlotXBoundsFromLeft(left); + this.controlX = this.getScrollBarX() - 20; + } + + void initEntries(Iterable entries) { + this.controlX = this.getScrollBarX() - 20; + this.entries.clear(); + for (Object entry : entries) { + addEntry(new ListEntry(entry.toString())); + } + } + + void addEntry(ListEntry entry) { + entry.entriesList = this; + this.entries.add(entry); + this.controlX = Math.min(this.controlX, this.getScrollBarX() - entry.getControlSetWidth() - 2); + } + + @Override + public IGuiListEntry getListEntry(int i) { + return entries.get(i); + } + + @Override + protected int getSize() { + return entries.size(); + } + + int getLabelX() { + return left + 2; + } + + int getControlX() { + return controlX; + } + + @Override + public int getListWidth() { + return width; + } + + @Override + protected int getScrollBarX() { + return left + getListWidth() - 6; + } + + public void handleDrag(int mouseX, int mouseY, float partialTicks) { + for (int i = 0, entriesSize = entries.size(); i < entriesSize; i++) { + ListEntry entry = entries.get(i); + if (entry.handleDrag(i, mouseX, mouseY, partialTicks)) { + return; + } + } + } + + public void drawScreenPost(int mouseX, int mouseY, float partialTicks) { + for (ListEntry entry : entries) { + entry.drawToolTip(mouseX, mouseY); + } + } + + public boolean areContentEqual(Object[] vals) { + if (vals.length != entries.size()) return false; + for (int i = 0; i < entries.size(); i++) { + ListEntry entry = entries.get(i); + if (!entry.name.equals(vals[i])) return false; + } + return true; + } + } + + class ListEntry implements GuiListExtended.IGuiListEntry { + + private EntriesList entriesList; + private final HoverChecker hoverCheckerText = new HoverChecker(0, 0, 0, 0, 0); + final String name; + final String localizedName; + private final String tooltip; + public boolean modified = false; + + private final GuiButtonExt btnDrag = new GuiButtonDrag(1, 0, 0, 20, 20); + private final HoverChecker hoverCheckerBtnDrag = new HoverChecker(btnDrag, 800); + private boolean isDragging = false; + private long lastScroll = 0; + + ListEntry(String name) { + this.name = name; + String nameKey = "structurelib.sortedregistry." + registryName + "." + name; + this.localizedName = trans(nameKey, name); + + String tooltipKey = nameKey + ".tooltip"; + String tooltip = trans(tooltipKey, null); + StringBuilder tooltipBuilder = new StringBuilder(); + tooltipBuilder.append(EnumChatFormatting.GREEN); + tooltipBuilder.append(this.localizedName); + if (tooltip != null) { + tooltipBuilder.append('\n').append(EnumChatFormatting.YELLOW).append(tooltip); + } + tooltipBuilder.append('\n'); + tooltipBuilder.append(EnumChatFormatting.DARK_GRAY); + tooltipBuilder.append(this.name); + this.tooltip = tooltipBuilder.toString(); + } + + @Override + public void drawEntry(int slotIndex, int x, int y, int listWidth, int slotHeight, Tessellator tessellator, + int mouseX, int mouseY, boolean isSelected) { + drawOutlineBox(x, y, listWidth, slotHeight, tessellator); + drawLabel(y, slotHeight); + drawControlSet(y, slotHeight, mouseX, mouseY); + } + + private void drawOutlineBox(int x, int y, int listWidth, int slotHeight, Tessellator tessellator) { + if (isDragging) { + // left x, right x, top y, bottom y + int lx = x, rx = entriesList.getScrollBarX() - 4, ty = y, by = y + slotHeight; + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + GL11.glDisable(GL11.GL_TEXTURE_2D); + tessellator.startDrawingQuads(); + tessellator.setColorOpaque_I(0x808080); + tessellator.addVertexWithUV(lx - 4, by + 4, 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(rx + 4, by + 4, 0.0D, 1.0D, 1.0D); + tessellator.addVertexWithUV(rx + 4, ty - 4, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(lx - 4, ty - 4, 0.0D, 0.0D, 0.0D); + tessellator.setColorOpaque_I(0x202020); + tessellator.addVertexWithUV(lx - 2, by + 2, 0.0D, 0.0D, 1.0D); + tessellator.addVertexWithUV(rx + 2, by + 2, 0.0D, 1.0D, 1.0D); + tessellator.addVertexWithUV(rx + 2, ty - 2, 0.0D, 1.0D, 0.0D); + tessellator.addVertexWithUV(lx - 2, ty - 2, 0.0D, 0.0D, 0.0D); + tessellator.draw(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + } + + private void drawControlSet(int y, int slotHeight, int mouseX, int mouseY) { + btnDrag.xPosition = entriesList.getControlX(); + btnDrag.yPosition = y + (slotHeight - btnDrag.height) / 2; + btnDrag.drawButton(mc, mouseX, mouseY); + } + + private void drawLabel(int y, int slotHeight) { + String label = modified ? EnumChatFormatting.ITALIC + localizedName : localizedName; + int allowedWidth = entriesList.getControlX() - entriesList.getLabelX() - 4; + // probably not very wise to recompute this every render tick, but it doesn't matter for now + String toDraw = trimStringToWidth(label, allowedWidth); + hoverCheckerText.updateBounds( + y + slotHeight / 2 - mc.fontRenderer.FONT_HEIGHT / 2, + y + slotHeight / 2 + mc.fontRenderer.FONT_HEIGHT / 2, + entriesList.getLabelX(), + entriesList.getLabelX() + mc.fontRenderer.getStringWidth(toDraw)); + + mc.fontRenderer.drawString( + toDraw, + entriesList.getLabelX(), + y + slotHeight / 2 - mc.fontRenderer.FONT_HEIGHT / 2, + getLabelColor()); + } + + void drawToolTip(int mouseX, int mouseY) { + // ensure checkHover() is always called, or the hover threshold might fail under certain corner cases + if (hoverCheckerText.checkHover(mouseX, mouseY)) { + GuiEditRegistryConfig.this.drawToolTip( + mc.fontRenderer.listFormattedStringToWidth(tooltip, entriesList.getListWidth()), + mouseX, + mouseY); + } + if (hoverCheckerBtnDrag.checkHover(mouseX, mouseY) && !isDragging) { + GuiEditRegistryConfig.this.drawToolTip( + mc.fontRenderer.listFormattedStringToWidth( + I18n.format("structurelib.configgui.drag.tooltip"), + entriesList.getListWidth()), + mouseX, + mouseY); + } + } + + protected int getLabelColor() { + return isDragging ? 0x888888 : 0xffffff; + } + + int getControlSetWidth() { + return this.btnDrag.getButtonWidth(); + } + + public boolean handleDrag(int index, int mouseX, int mouseY, float partialTicks) { + if (!isDragging) { + return false; + } + for (EntriesList list : lists) { + if (list == entriesList) continue; + if (mouseX > list.left && mouseX < list.right && mouseY > list.top && mouseY < list.bottom) { + int l; + if (list.entries.isEmpty()) l = 0; + else l = list.func_148124_c(mouseX, mouseY); + if (l < 0) { + if (mouseY < (list.bottom + list.top) / 2) { + l = 0; + } else { + l = list.entries.size(); + } + } + entriesList.func_148143_b(true); + entriesList.entries.remove(index); + list.func_148143_b(false); + list.entries.add(l, this); + this.entriesList = list; + return true; + } + } + long now = System.currentTimeMillis(); + if (now - lastScroll > 20) { + if (mouseY < entriesList.top) { + entriesList.scrollBy(-3); + lastScroll = now; + } else if (mouseY > entriesList.bottom) { + entriesList.scrollBy(3); + lastScroll = now; + } + } + int l = entriesList.func_148124_c( + MathHelper.clamp_int(mouseX, entriesList.left, entriesList.right), + MathHelper.clamp_int(mouseY, entriesList.top, entriesList.bottom)); + if (l < 0 || l == index) return true; + entriesList.entries.remove(index); + entriesList.entries.add(l, this); + return true; + } + + @Override + public void mouseReleased(int index, int x, int y, int mouseEvent, int relativeX, int relativeY) { + isDragging = false; + } + + @Override + public boolean mousePressed(int index, int x, int y, int mouseEvent, int relativeX, int relativeY) { + if (btnDrag.mousePressed(mc, x, y)) { + isDragging = true; + return true; + } + return false; + } + } + + private static String trans(String transKey, String fallback) { + String translated = I18n.format(transKey); + return translated.equals(transKey) ? fallback : translated; + } + + private static class GuiButtonDrag extends GuiButtonExt { + + public GuiButtonDrag(int id, int xPos, int yPos, int width, int height) { + super(id, xPos, yPos, width, height, null); + } + + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + if (this.visible) { + this.field_146123_n = mouseX >= this.xPosition && mouseY >= this.yPosition + && mouseX < this.xPosition + this.width + && mouseY < this.yPosition + this.height; + int k = this.getHoverState(this.field_146123_n); + GuiUtils.drawContinuousTexturedBox( + buttonTextures, + this.xPosition, + this.yPosition, + 0, + 46 + k * 20, + this.width, + this.height, + 200, + 20, + 2, + 3, + 2, + 2, + this.zLevel); + mc.renderEngine.bindTexture(new ResourceLocation("structurelib", "textures/gui/fourwayarrow2.png")); + int x = this.xPosition + (this.width - 12) / 2; + int y = this.yPosition + (this.height - 12) / 2; + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.addVertexWithUV(x, y + 12, zLevel, 0, 1); + tessellator.addVertexWithUV(x + 12, y + 12, zLevel, 1, 1); + tessellator.addVertexWithUV(x + 12, y, zLevel, 1, 0); + tessellator.addVertexWithUV(x, y, zLevel, 0, 0); + tessellator.draw(); + } + } + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/RegistryOrderEntry.java b/src/main/java/com/gtnewhorizon/structurelib/RegistryOrderEntry.java new file mode 100644 index 00000000..22430704 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/RegistryOrderEntry.java @@ -0,0 +1,77 @@ +package com.gtnewhorizon.structurelib; + +import java.util.List; + +import net.minecraft.client.gui.GuiScreen; + +import cpw.mods.fml.client.config.GuiConfig; +import cpw.mods.fml.client.config.GuiConfigEntries; +import cpw.mods.fml.client.config.IConfigElement; + +@SuppressWarnings({ "unchecked" }) +public final class RegistryOrderEntry extends GuiConfigEntries.CategoryEntry { + + public RegistryOrderEntry(GuiConfig owningScreen, GuiConfigEntries owningEntryList, + IConfigElement configElement) { + super(owningScreen, owningEntryList, configElement); + } + + @Override + protected GuiScreen buildChildScreen() { + List> childElements = this.configElement.getChildElements(); + IConfigElement disabled = null, order = null; + for (IConfigElement e : childElements) { + if (e.getName().equals("disabled")) disabled = e; + else if (e.getName().equals("ordering")) order = e; + } + if (disabled == null || order == null) { + throw new IllegalArgumentException("Did not find expected config entires"); + } + return new GuiEditRegistryConfig( + this.owningScreen, + disabled, + order, + configElement.getName(), + this.owningScreen.modID, + null, + owningScreen.allRequireWorldRestart || this.configElement.requiresWorldRestart(), + owningScreen.allRequireMcRestart || this.configElement.requiresMcRestart(), + this.owningScreen.title, + ((this.owningScreen.titleLine2 == null ? "" : this.owningScreen.titleLine2) + " > " + this.name)); + } + + private GuiEditRegistryConfig getChildScreen() { + return (GuiEditRegistryConfig) childScreen; + } + + @Override + public boolean saveConfigElement() { + GuiEditRegistryConfig c = getChildScreen(); + if (c != null && c.isInitialized()) return c.saveConfigElements(); + return false; + } + + @Override + public boolean isChanged() { + GuiEditRegistryConfig c = getChildScreen(); + return c != null && c.isInitialized() && c.isChanged(); + } + + @Override + public boolean isDefault() { + GuiEditRegistryConfig c = getChildScreen(); + return c != null && c.isInitialized() && c.isDefault(); + } + + @Override + public void setToDefault() { + GuiEditRegistryConfig c = getChildScreen(); + if (c != null && c.isInitialized()) c.setToDefault(); + } + + @Override + public void undoChanges() { + GuiEditRegistryConfig c = getChildScreen(); + if (c != null && c.isInitialized()) c.undoChanges(); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/SortedRegistry.java b/src/main/java/com/gtnewhorizon/structurelib/SortedRegistry.java index 6ad47919..0c1c5fd3 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/SortedRegistry.java +++ b/src/main/java/com/gtnewhorizon/structurelib/SortedRegistry.java @@ -4,26 +4,32 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NavigableMap; +import java.util.Set; import java.util.Spliterator; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.server.MinecraftServer; + +import org.apache.commons.lang3.tuple.Pair; import com.gtnewhorizon.structurelib.net.RegistryOrderSyncMessage; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.PlayerEvent; -import org.apache.commons.lang3.tuple.Pair; public class SortedRegistry implements Iterable { @@ -60,6 +66,37 @@ public class SortedRegistry implements Iterable { FMLCommonHandler.instance().bus().register(this); } + public static Set getRegistryNames() { + return ALL_REGISTRIES.keySet(); + } + + public static SortedRegistry getRegistry(String name) { + WeakReference> ref = ALL_REGISTRIES.get(name); + return ref != null ? ref.get() : null; + } + + static void cleanup(MinecraftServer server) { + @SuppressWarnings("unchecked") + List players = server.getConfigurationManager().playerEntityList; + Set onlinePlayerIDs = players.stream().map(EntityPlayerMP::getUniqueID).collect(Collectors.toSet()); + int removed = 0; + for (WeakReference> ref : ALL_REGISTRIES.values()) { + SortedRegistry registry = ref.get(); + if (registry == null) continue; + for (Iterator iterator = registry.playerOrdering.keySet().iterator(); iterator.hasNext();) { + UUID uuid = iterator.next(); + if (!onlinePlayerIDs.contains(uuid)) { + iterator.remove(); + registry.playerBaked.remove(uuid); + removed++; + } + } + } + if (removed > 0) { + StructureLib.LOGGER.debug("Removed {} registry record for logged out players", removed); + } + } + @SubscribeEvent public void onPlayerLogOut(PlayerEvent.PlayerLoggedOutEvent e) { playerOrdering.remove(e.player.getUniqueID()); @@ -130,6 +167,17 @@ public class SortedRegistry implements Iterable { return playerPreference; } + public Stream getPlayerOrderingKeys(@Nullable EntityPlayerMP player) { + if (player == null) return store.keySet().stream(); + List playerOrder = playerBaked.get(player.getUniqueID()); + if (playerOrder == null) return store.keySet().stream(); + + // rebuild the order from the baked map + Map reverseMap = new IdentityHashMap<>(); + store.forEach((k, v) -> reverseMap.put(v, k)); + return playerOrder.stream().map(reverseMap::get); + } + Iterable getCurrentOrdering() { return store.keySet(); } diff --git a/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java b/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java index 404e087b..3f220256 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java +++ b/src/main/java/com/gtnewhorizon/structurelib/StructureLib.java @@ -12,6 +12,7 @@ import org.apache.logging.log4j.Logger; import com.gtnewhorizon.structurelib.block.BlockHint; import com.gtnewhorizon.structurelib.command.CommandConfigureChannels; +import com.gtnewhorizon.structurelib.command.CommandRegistryDebug; import com.gtnewhorizon.structurelib.item.ItemBlockHint; import com.gtnewhorizon.structurelib.item.ItemConstructableTrigger; import com.gtnewhorizon.structurelib.item.ItemFrontRotationTool; @@ -127,6 +128,7 @@ public class StructureLib { @Mod.EventHandler public void serverStarting(FMLServerStartingEvent e) { e.registerServerCommand(new CommandConfigureChannels()); + e.registerServerCommand(new CommandRegistryDebug()); } public static void addClientSideChatMessages(String... messages) { diff --git a/src/main/java/com/gtnewhorizon/structurelib/command/CommandRegistryDebug.java b/src/main/java/com/gtnewhorizon/structurelib/command/CommandRegistryDebug.java new file mode 100644 index 00000000..73e67923 --- /dev/null +++ b/src/main/java/com/gtnewhorizon/structurelib/command/CommandRegistryDebug.java @@ -0,0 +1,52 @@ +package com.gtnewhorizon.structurelib.command; + +import java.util.List; + +import net.minecraft.command.CommandBase; +import net.minecraft.command.ICommandSender; +import net.minecraft.command.WrongUsageException; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.util.ChatComponentText; + +import com.gtnewhorizon.structurelib.SortedRegistry; + +public class CommandRegistryDebug extends CommandBase { + + @Override + public String getCommandName() { + return "sl_registry_debug"; + } + + @Override + public String getCommandUsage(ICommandSender p_71518_1_) { + return "/sl_registry_debug "; + } + + @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_, + SortedRegistry.getRegistryNames().toArray(new String[0])); + return null; + } + + @Override + public void processCommand(ICommandSender sender, String[] args) { + if (args.length != 1) throw new WrongUsageException(getCommandUsage(sender)); + + if (!(sender instanceof EntityPlayerMP)) throw new WrongUsageException("must be a player"); + + EntityPlayerMP player = (EntityPlayerMP) sender; + + SortedRegistry registry = SortedRegistry.getRegistry(args[0]); + if (registry == null) throw new WrongUsageException("registry not found"); + + player.addChatMessage(new ChatComponentText("Current order:")); + registry.getPlayerOrderingKeys(player).map(ChatComponentText::new).forEach(player::addChatMessage); + } +} diff --git a/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java b/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java index 3656df34..6aedef93 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java +++ b/src/main/java/com/gtnewhorizon/structurelib/item/ItemConstructableTrigger.java @@ -28,8 +28,13 @@ public class ItemConstructableTrigger extends Item { @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); + if (world.isRemote && getMovingObjectPositionFromPlayer(world, player, true) == null) { + if (player.isSneaking()) { + StructureLib.instance().proxy().displayConfigGUI("registries"); + } else { + player.openGui(StructureLib.instance(), 0, world, player.inventory.currentItem, 0, 0); + } + } return super.onItemRightClick(stack, world, player); } diff --git a/src/main/resources/assets/structurelib/lang/en_US.lang b/src/main/resources/assets/structurelib/lang/en_US.lang index c3c0dcde..6375d6e8 100644 --- a/src/main/resources/assets/structurelib/lang/en_US.lang +++ b/src/main/resources/assets/structurelib/lang/en_US.lang @@ -172,3 +172,19 @@ structurelib.config.client.hologram.maxCoexisting=Max Coexisting Holograms structurelib.config.client.hologram.maxCoexisting.tooltip=Number of holograms that can coexist. Usually all hints generated by one right click belongs to one hologram. An attempt will be made to prune old holograms when a new hologram is about to be projected and this limit would be exceeded. structurelib.config.client.hologram.removeColliding=Remove Colliding Holograms structurelib.config.client.hologram.removeColliding.tooltip=Whether an attempt will be made to remove any existing hologram that collides with a new hologram. + +structurelib.config.registries=Sorted Registries +structurelib.config.registries.tooltip=Sorted Registry is how StructureLib internally keep track of certain features. Each individual elements may be reordered, disabled and enabled at user's command. Changes will be synced to server if needed.\nThis gui can also be accessed in game by shift right clicking air with Multiblock Structure Hologram Projector. +structurelib.config.registries.stackextractors=Stack Extractors +structurelib.config.registries.stackextractors.tooltip=Controls which inventory-from-stack functions are enabled during survival autoplace +structurelib.config.registries.inventoryproviders=Inventory Providers +structurelib.config.registries.inventoryproviders.tooltip=Controls which inventories functions are enabled during survival autoplace + +structurelib.configgui.enabled=Enabled Entry +structurelib.configgui.disabled=Disabled Entry +structurelib.configgui.drag.tooltip=Drag this button to reorder this element. + +structurelib.sortedregistry.inventoryproviders.5000-main-inventory=Main Player Inventory +structurelib.sortedregistry.inventoryproviders.5000-main-inventory.tooltip=36 slots of item in your main inventory. Armor slots excluded. +structurelib.sortedregistry.inventoryproviders.7000-ender-inventory=Ender Chest Inventory +structurelib.sortedregistry.inventoryproviders.7000-ender-inventory.tooltip=Links to your vanilla ender chest inventory. Require at least one "ender backpack like" item in main inventory to work. diff --git a/src/main/resources/assets/structurelib/lang/zh_CN.lang b/src/main/resources/assets/structurelib/lang/zh_CN.lang index 5454a9bc..be461bd1 100644 --- a/src/main/resources/assets/structurelib/lang/zh_CN.lang +++ b/src/main/resources/assets/structurelib/lang/zh_CN.lang @@ -172,3 +172,19 @@ structurelib.config.client.hologram.maxCoexisting=最多可以同时存在的全 structurelib.config.client.hologram.maxCoexisting.tooltip=这决定最多可以同时存在多少台机器的全息投影。一般一次使用全息投影仪右键生成的所有机器结构细节被视为一个全息投影。当同时存在多个全息投影,并超过此数量时,再次使用全息投影仪会移除旧有的全息投影。 structurelib.config.client.hologram.removeColliding=移除冲突的全息投影 structurelib.config.client.hologram.removeColliding.tooltip=当新的全息投影与已存在的全息投影冲突时,开启此选项时会移除已存在的全息投影。 + +structurelib.config.registries=注册表 +structurelib.config.registries.tooltip=StructureLib 使用注册表来管理其部分功能。每个注册表中的每一个元素都可以被单独排序、禁用。需要时,注册表的内容将被同步至服务端。 +structurelib.config.registries.stackextractors=物品提取器 +structurelib.config.registries.stackextractors.tooltip=该注册表控制在生存模式自动放置时,如何从物品(如林业背包)提取库存。 +structurelib.config.registries.inventoryproviders=物品栏定位 +structurelib.config.registries.inventoryproviders.tooltip=该注册表控制在生存模式自动放置时,可用的物品栏来源。 + +structurelib.configgui.enabled=已启用 +structurelib.configgui.disabled=已禁用 +structurelib.configgui.drag.tooltip=拖动该按钮以重排。 + +structurelib.sortedregistry.inventoryproviders.5000-main-inventory=玩家物品栏 +structurelib.sortedregistry.inventoryproviders.5000-main-inventory.tooltip=不包括护甲栏以外的36格。 +structurelib.sortedregistry.inventoryproviders.7000-ender-inventory=末影箱 +structurelib.sortedregistry.inventoryproviders.7000-ender-inventory.tooltip=当前玩家的末影箱。需要在主物品栏中存在类似于末影背包的东西才会实际生效。 diff --git a/src/main/resources/assets/structurelib/textures/gui/fourwayarrow2.png b/src/main/resources/assets/structurelib/textures/gui/fourwayarrow2.png new file mode 100644 index 0000000000000000000000000000000000000000..a75a35bc6b5a17dfcff400f8ffa659b8efcf9a69 GIT binary patch literal 367 zcmV-#0g(QQP)(S>!c17<=Kmsom7^oXXXp7Dt`_k^h9J0H zd7dghil3bjc;l_(eqZ zzlZiCG8&RXZ+Ud}sX