add channel desc and channel indicator item (#39)

Add channel desc and channel indicator item. Its meaning and development
examples has been added to overview page in javadoc.
Also removes gt_no_hatch and show_error special button from GUI as they
are now a candidate in the dropdown menu.

one day we will move this to MUI2, but for now let's stick to existing
infra that is known to work



https://github.com/user-attachments/assets/814edbb6-c4b4-4309-8798-787d9ed1aeef

Note: channels in demo video are from GT5. if you boot the pack without
the modified gt5 you won't see most of the channels.

---------

Signed-off-by: Glease <4586901+Glease@users.noreply.github.com>
This commit is contained in:
Glease authored and GitHub committed 2025-04-30 15:55:19 +02:00
1 parent 538459eab5
commit 7cda901ae5
16 files changed
+706 -134

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
// Add your dependencies here
dependencies {
api("com.github.GTNewHorizons:GTNHLib:0.6.13:dev")
runtimeOnly("com.github.GTNewHorizons:NotEnoughItems:2.7.29-GTNH:dev")
api("com.github.GTNewHorizons:GTNHLib:0.6.17:dev")
implementation("com.github.GTNewHorizons:NotEnoughItems:2.7.29-GTNH:dev")
testImplementation(platform('org.junit:junit-bom:5.9.2'))
testImplementation('org.junit.jupiter:junit-jupiter')
@@ -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<Set<ChannelDescription>> itemToChannels = new ItemStackMap<>(true);
private static final Map<String, ChannelDescription> registry = new HashMap<>();
private final String channel;
private final Map<String, String> descriptions = new HashMap<>();
private final ItemStackMap<Integer> 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<Map.Entry<String, Integer>> iterate(final ItemStack block) {
Set<ChannelDescription> 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<String, ChannelDescription> 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<String, String> getDescriptions() {
return Collections.unmodifiableMap(descriptions);
}
public Map<ItemStack, Integer> getItems() {
return Collections.unmodifiableMap(items);
}
}
@@ -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);
}
}
@@ -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;
}
@@ -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);
}
@@ -11,6 +11,7 @@ import net.minecraft.util.IChatComponent;
import net.minecraft.util.IIcon;
import net.minecraft.world.World;
import com.gtnewhorizon.gtnhlib.util.AnimatedTooltipHandler;
import com.gtnewhorizon.structurelib.alignment.IAlignment;
import com.gtnewhorizon.structurelib.alignment.IAlignmentProvider;
import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing;
@@ -43,6 +44,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<Object> instrument = new ThreadLocal<>();
/**
@@ -350,4 +352,37 @@ 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,
AnimatedTooltipHandler.translatedText("structurelib.tooltip.channelvalue", channelValue, channel));
AnimatedTooltipHandler.addItemTooltip(
stack,
AnimatedTooltipHandler.translatedText("structurelib.tooltip.indicator_dnd", channelValue, channel));
}
}
@@ -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;
}
}
@@ -1,56 +1,82 @@
package com.gtnewhorizon.structurelib.gui;
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 java.util.stream.StreamSupport;
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.base.Splitter;
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 static final int INFO_BOX_SIZE = 11;
private final ItemStack trigger;
private final GuiChannelsList list;
private final GuiAutoCompleteList autoCompleteList;
private final Map<String, List<String>> tooltipSplitCache = new HashMap<>();
private final TIntObjectMap<Map<String, String>> trimCache = new TIntObjectHashMap<>();
private GuiTextField key, value;
protected int guiTop, guiLeft;
private List<String> info;
public GuiScreenConfigureChannels(Container container, ItemStack trigger) {
super(container);
public GuiScreenConfigureChannels(ItemStack trigger) {
this.trigger = trigger;
list = new GuiChannelsList(152, 100, 12, 12, 14);
list = new GuiChannelsList(152, 91, 12, 21, 14);
list.addSelectionListener((list, selectedIndex) -> {
Entry<String, Integer> e = list.getElementAt(selectedIndex);
key.setText(e.getKey());
value.setText(e.getValue().toString());
updateButtons();
});
autoCompleteList = new GuiAutoCompleteList(151 + 12 - 45 - 2, 50, 45, 119 + 12, 14);
}
@Override
@@ -136,6 +162,7 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen
};
list.onGuiInit(this);
autoCompleteList.onGuiInit(this);
addButton(
new GuiButton(
@@ -162,21 +189,16 @@ 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();
info = StreamSupport
.stream(
Splitter.on("\\n").split(
StatCollector.translateToLocal("item.structurelib.constructableTrigger.gui.info"))
.spliterator(),
false)
.flatMap(line -> fontRendererObj.listFormattedStringToWidth(line, width * 3 / 5).stream())
.collect(Collectors.toList());
}
@Override
@@ -196,7 +218,7 @@ public class GuiScreenConfigureChannels extends GuiScreen implements IGuiScreen
@Override
public int getYSize() {
return 211;
return 188;
}
@Override
@@ -252,13 +274,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 +294,33 @@ 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);
value.setText("1");
value.setSelectionPos(0);
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 +341,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 +366,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,18 +391,17 @@ 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.title"),
guiLeft + 12,
guiTop + 9,
0);
fontRendererObj.drawString(
I18n.format("item.structurelib.constructableTrigger.gui.key"),
guiLeft + 12,
@@ -401,6 +416,221 @@ 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);
}
if (mouseX > xSize + guiLeft - INFO_BOX_SIZE - 12 && mouseX < xSize + guiLeft - 12
&& mouseY > guiTop + 7
&& mouseY < guiTop + INFO_BOX_SIZE + 7) {
drawHoveringText(info, mouseX, mouseY, fontRendererObj);
}
}
private void drawChannelDescriptionTooltip(String candidate) {
List<String> tooltip = tooltipSplitCache.computeIfAbsent(candidate, this::getChannelDescriptionTooltip);
drawHoveringText(tooltip, guiLeft + 151 + 12, guiTop + 122, fontRendererObj);
}
private List<String> getChannelDescriptionTooltip(String candidate) {
List<String> tooltip = new ArrayList<>();
int maxLine = width - (guiLeft + 151 + 12) - 12;
for (Entry<String, String> 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<String, String> 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<Integer> getItemSpawnSlots(GuiContainer gui, ItemStack item) {
return Collections.emptyList();
}
@Override
public List<TaggedInventoryArea> 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<Entry<String, Integer>> 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<String, Integer> 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<String> {
private List<Entry<String, String>> cache; // <key, display string>
public GuiAutoCompleteList(int width, int height, int originX, int originY, int slotHeight) {
super(width, height, originX, originY, slotHeight);
margin = 2;
}
private List<Entry<String, String>> getOptions() {
if (cache == null) {
cache = search();
}
return cache;
}
private List<Entry<String, String>> search() {
List<Entry<String, String>> cache = new ArrayList<>();
Set<String> 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<Entry<String, Integer>> {
private List<Entry<String, Integer>> cache;
@@ -468,22 +698,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;
}
}
}
@@ -16,7 +16,7 @@ import org.lwjgl.opengl.GL11;
* <p>
* 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
*
* <p>
* Major edits
* <ul>
* <li>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<T> {
}
}
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<T> {
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<T> {
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<T> {
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);
@@ -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);
@@ -395,7 +395,7 @@ public interface IStructureDefinition<T> {
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);
}
@@ -614,35 +614,35 @@ 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.
* <p>
* (*): 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)
* <p>
* 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}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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
* <p>
* There is yet no TileEntity counterpart of this utility. Feel free to submit a PR to add it.
* <p>
* 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:
* (*): 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.
* <p>
* <h3>WARNING</h3> <b>You SHOULD NOT return notSet from your tierExtractor.</b> If you do so, we will have this
* chain of events:
* <ul>
* <li>at check start, tier is reset to notSet
* <li>at first occurrence, ofBlocksTiered finds your current tier is notSet, and the block's tier is notSet, so
* it's accepted
* <li>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
* <li>at 2nd, 3rd..., same thing in 2 happens
* <li>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
@@ -650,10 +650,10 @@ public class StructureUtility {
* doing its business
* <li>Player enjoy a (probably) hilariously shaped multi and (probably) reduced build cost.
* </ul>
* <h3>Example Implementation</h3>
* <p>
* <b>Example:</b>
* <p>
* 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
*
* <pre>
* public class Tile extends TileEntity {
@@ -677,6 +677,18 @@ public class StructureUtility {
* }
* </pre>
*
* Important bits of this example implementation:
* <ul>
* <li>The supplied ITierConverter returns null for an invalid block</li>
* <li>Possible tier returned by ITierConverter is Integer object 0 to 15.</li>
* <li>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)}</li>
* <li>notSet is never a tier returned by ITierConverter.</li>
* </ul>
* <p>
* As a friendly reminder, the above characteristics are only for this simply demo, not as a general restriction
* imposed by ofBlocksTiered.
*
* @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 +698,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
*/
+74
View File
@@ -178,6 +178,80 @@ StructureLib API documentation.
Just give me a better name and I'd do the renaming.
<h3>Channel Descriptions</h3>
<p>
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.
</p>
<p>
Here is an example of sending an IMC message to register two channels and their description.
In this example, we assume your modid is <code>my_mod</code> and your mod will ship with localization entries for
localization key <code>channels.my_mod.channel_1</code> and <code>channels.my_mod.channel_2</code>
</p>
<pre>
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);
</pre>
<p>
There is no strict rules over whether you should do direct API method call or send IMC messages.
You can choose whichever you prefer.
</p>
<h3>Channel Indicator Items</h3>
<p>
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.
</p>
<p>
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 <code>my_mod</code>.
</p>
<pre>
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);
</pre>
<p>
There is no strict rules over whether you should do direct API method call or send IMC messages.
You can choose whichever you prefer.
</p>
<h3>Examples</h3>
<h4>For Structure Definition Author</h4>
@@ -30,16 +30,20 @@ item.structurelib.constructableTrigger.desc.4=Quantity of schematics in item sta
item.structurelib.constructableTrigger.desc.5=Hold §l[LSHIFT]§r to display channel data
item.structurelib.constructableTrigger.desc.lshift.0=Contain %d sub channel.
item.structurelib.constructableTrigger.too_fast=Please wait %d milliseconds before another autoplace...
item.structurelib.constructableTrigger.gui.title=Sub Channels
item.structurelib.constructableTrigger.gui.key=Key:
item.structurelib.constructableTrigger.gui.value=Value:
item.structurelib.constructableTrigger.gui.add=Add
item.structurelib.constructableTrigger.gui.set=Set
item.structurelib.constructableTrigger.gui.unset=Unset
item.structurelib.constructableTrigger.gui.wipe=§4Wipe
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
item.structurelib.constructableTrigger.gui.info=Many multiblocks support some degree of variation in its structure and use sub channels to indicate which variant to use.\n Sub channels are has a sequence of lower case characters as key and a positive integer as value. Most known sub channels should be available as typeahead completions.\nYou can also drag and drop items from NEI to set the corresponding sub channel value. These items all have a tooltip of §oIndicates value X for sub channel Y§r.
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.tooltip.indicator_dnd=Drop into GUI to select this value for this channel
structurelib.autoplace.built_stat=Built %s blocks
structurelib.autoplace.complete=§aThis multiblock has completed!
@@ -30,16 +30,20 @@ item.structurelib.constructableTrigger.desc.4=此物品堆叠数量会影响显
item.structurelib.constructableTrigger.desc.5=按下 §l[左SHIFT]§r 以显示子信道
item.structurelib.constructableTrigger.desc.lshift.0=包含 %d 个子信道
item.structurelib.constructableTrigger.too_fast=请在下一次自动搭建前等待 %d 毫秒...
item.structurelib.constructableTrigger.gui.title=子信道
item.structurelib.constructableTrigger.gui.key=信道名:
item.structurelib.constructableTrigger.gui.value=值:
item.structurelib.constructableTrigger.gui.add=添加
item.structurelib.constructableTrigger.gui.set=替换
item.structurelib.constructableTrigger.gui.unset=删除
item.structurelib.constructableTrigger.gui.wipe=§4全部清空
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
item.structurelib.constructableTrigger.gui.info=许多多方块的结构都是可变的,在自动放置时,可以使用子信道来向多方块指明该使用哪一个变体。\n子信道由一个字符串标识、并拥有一个正整数作为值。这个GUI中的信道名支持自动补全到绝大多数已知的子信道。\n你也可以将部分物品从NEI中拖拽至GUI内来设置子信道值为该物品表示的值。这些物品的物品说明中都带有§o表示子信道 X 为 Y§r。
channels.structurelib.show_errors=添加后,在投影时会高亮标出必然错误的部分
structurelib.tooltip.channelvalue=§7表示子信道 §6%2$s§r§7 为 §6%d§r
structurelib.tooltip.indicator_dnd=拖入GUI中来选择该值
structurelib.autoplace.built_stat=已放置 %s 个方块
structurelib.autoplace.complete=§a这个多方块结构已经完成自动搭建!
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB