From fd1c5b455ac0b5386396c7a32a1dd4a57a9ddc8f Mon Sep 17 00:00:00 2001 From: McHorse Date: Fri, 30 Jun 2023 11:13:24 +0100 Subject: [PATCH] Add title and lore editor with formatting --- .../java/mchorse/mappet/EventHandler.java | 9 +- .../scriptedItem/GuiFormattedTextElement.java | 213 +++++ .../scriptedItem/GuiScriptedItemScreen.java | 113 ++- .../gui/scripts/scriptedItem/util/Pair.java | 39 + .../scriptedItem/util/StringGroup.java | 33 + .../scriptedItem/util/StringGroupMatcher.java | 85 ++ .../scripts/scriptedItem/util/Textbox.java | 765 ++++++++++++++++++ .../items/ClientHandlerScriptedItemInfo.java | 5 + .../common/items/PacketScriptedItemInfo.java | 18 +- .../items/ServerHandlerScriptedItemInfo.java | 7 +- .../resources/assets/mappet/lang/en_US.lang | 4 + 11 files changed, 1270 insertions(+), 21 deletions(-) create mode 100644 src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/GuiFormattedTextElement.java create mode 100644 src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/Pair.java create mode 100644 src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/StringGroup.java create mode 100644 src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/StringGroupMatcher.java create mode 100644 src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/Textbox.java diff --git a/src/main/java/mchorse/mappet/EventHandler.java b/src/main/java/mchorse/mappet/EventHandler.java index 81c42e18..a61c3cbe 100644 --- a/src/main/java/mchorse/mappet/EventHandler.java +++ b/src/main/java/mchorse/mappet/EventHandler.java @@ -1226,11 +1226,10 @@ public void onKeyInput(InputEvent.KeyInputEvent event) EntityPlayer player = Minecraft.getMinecraft().player; if ( - player.isRiding() && - player.getRidingEntity() instanceof EntityNpc - && ((EntityNpc) player.getRidingEntity()).getState().canBeSteered - ) - { + player.isRiding() && + player.getRidingEntity() instanceof EntityNpc && + ((EntityNpc) player.getRidingEntity()).getState().canBeSteered + ) { float jumpPower = ((EntityNpc) player.getRidingEntity()).getState().jumpPower; Dispatcher.sendToServer(new PacketNpcJump(player.getRidingEntity().getEntityId(), jumpPower)); } diff --git a/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/GuiFormattedTextElement.java b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/GuiFormattedTextElement.java new file mode 100644 index 00000000..fcc2c035 --- /dev/null +++ b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/GuiFormattedTextElement.java @@ -0,0 +1,213 @@ +package mchorse.mappet.client.gui.scripts.scriptedItem; + +import mchorse.mappet.client.gui.scripts.scriptedItem.util.Textbox; +import mchorse.mclib.client.gui.framework.elements.GuiElement; +import mchorse.mclib.client.gui.framework.elements.IFocusedGuiElement; +import mchorse.mclib.client.gui.framework.elements.utils.GuiContext; +import mchorse.mclib.client.gui.utils.Area; +import mchorse.mclib.utils.ColorUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiScreen; +import org.lwjgl.input.Keyboard; + +import java.util.function.Consumer; + +public class GuiFormattedTextElement extends GuiElement implements IFocusedGuiElement +{ + private static String[] formattingCodes = new String[] { + "\u00A70", "\u00A71", "\u00A72", "\u00A73", "\u00A74", "\u00A75", "\u00A76", "\u00A77", "\u00A78", "\u00A79", "\u00A7a", "\u00A7b", "\u00A7c", "\u00A7d", "\u00A7e", "\u00A7f", "\u00A7k", "\u00A7l", "\u00A7m", "\u00A7n", "\u00A7o" + }; + + private static int[] colors = new int[] { + 0x000000, 0x0000AA, 0x00AA00, 0x00AAAA, 0xAA0000, 0xAA00AA, 0xFFAA00, 0xAAAAAA, 0x555555, 0x5555FF, 0x55FF55, 0x55FFFF, 0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF + }; + + public Textbox text; + + private Consumer callback; + + public GuiFormattedTextElement(Minecraft mc, Consumer consumer) + { + super(mc); + + this.callback = consumer; + + this.text = new Textbox(consumer); + this.text.setFont(mc.fontRenderer); + this.text.setBorder(true); + } + + @Override + public void resize() + { + super.resize(); + + this.text.area.set(this.area.x, this.area.ey() - 20, this.area.w, 20); + } + + @Override + public boolean mouseClicked(GuiContext context) + { + int cell = this.area.h - 20; + + if (this.area.isInside(context) && context.mouseY < this.area.y + cell) + { + int index = (context.mouseX - this.area.x) / cell; + + if (index >= formattingCodes.length || !this.text.isSelected()) + { + return super.mouseClicked(context); + } + + String formattingCode = formattingCodes[index]; + String text = this.text.getText(); + int cursor = this.text.getCursor(); + int selection = this.text.getSelection(); + int beginning = Math.min(cursor, selection); + int end = Math.max(cursor, selection); + + this.text.setText(text.substring(0, beginning) + formattingCode + this.text.getSelectedText() + "\u00A7r" + text.substring(end)); + this.text.moveCursorTo(beginning); + this.text.setSelection(end + 4); + + if (this.callback != null) + { + this.callback.accept(this.text.getText()); + } + + return true; + } + + if (super.mouseClicked(context)) + { + return true; + } + + boolean wasFocused = this.text.isFocused(); + + this.text.mouseClicked(context.mouseX, context.mouseY, context.mouseButton); + + if (wasFocused != this.text.isFocused()) + { + context.focus(wasFocused ? null : this); + } + + return this.text.area.isInside(context); + } + + @Override + public void mouseReleased(GuiContext context) + { + this.text.mouseReleased(context.mouseX, context.mouseY, context.mouseButton); + + super.mouseReleased(context); + } + + @Override + public boolean keyTyped(GuiContext context) + { + if (this.isFocused()) + { + if (context.keyCode == Keyboard.KEY_TAB) + { + context.focus(this, -1, GuiScreen.isShiftKeyDown() ? -1 : 1); + + return true; + } + + if (context.keyCode == Keyboard.KEY_ESCAPE) + { + context.unfocus(); + + return true; + } + } + + return this.text.keyPressed(context) || this.text.textInput(context.typedChar) || super.keyTyped(context); + } + + @Override + public void draw(GuiContext context) + { + this.area.draw(0xff111111); + + int x = this.area.x; + int y = this.area.y; + int cell = this.area.h - 20; + + for (int i = 0; i < formattingCodes.length; i++) + { + Area.SHARED.set(x, y, cell, cell); + + int a = Area.SHARED.isInside(context) ? 0x88000000 : 0xFF000000; + float aa = Area.SHARED.isInside(context) ? 0.5F : 1F; + + if (i < colors.length) + { + Gui.drawRect(x, y, x + cell, y + cell, colors[i] | a); + } + else if (i == 16) + { + this.drawCenteredString(context.font, formattingCodes[i] + "W", x + cell / 2, y + cell / 2 - context.font.FONT_HEIGHT / 2, ColorUtils.multiplyColor(0xffffff, aa)); + } + else if (i == 17) + { + this.drawCenteredString(context.font, formattingCodes[i] + "B", x + cell / 2, y + cell / 2 - context.font.FONT_HEIGHT / 2, ColorUtils.multiplyColor(0xffffff, aa)); + } + else if (i == 18) + { + this.drawCenteredString(context.font, formattingCodes[i] + "S", x + cell / 2, y + cell / 2 - context.font.FONT_HEIGHT / 2, ColorUtils.multiplyColor(0xffffff, aa)); + } + else if (i == 19) + { + this.drawCenteredString(context.font, formattingCodes[i] + "U", x + cell / 2, y + cell / 2 - context.font.FONT_HEIGHT / 2, ColorUtils.multiplyColor(0xffffff, aa)); + } + else if (i == 20) + { + this.drawCenteredString(context.font, formattingCodes[i] + "I", x + cell / 2, y + cell / 2 - context.font.FONT_HEIGHT / 2, ColorUtils.multiplyColor(0xffffff, aa)); + } + + x += cell; + } + + this.text.render(context); + + super.draw(context); + } + + /* Focus implementation */ + + @Override + public boolean isFocused() + { + return this.text.isFocused(); + } + + @Override + public void focus(GuiContext guiContext) + { + this.text.setFocused(true); + Keyboard.enableRepeatEvents(true); + } + + @Override + public void unfocus(GuiContext guiContext) + { + this.text.setFocused(false); + Keyboard.enableRepeatEvents(false); + } + + @Override + public void selectAll(GuiContext guiContext) + { + this.text.moveCursorToEnd(); + this.text.setSelection(0); + } + + @Override + public void unselect(GuiContext guiContext) + { + this.text.deselect(); + } +} \ No newline at end of file diff --git a/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/GuiScriptedItemScreen.java b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/GuiScriptedItemScreen.java index c5823fd8..b03032d5 100644 --- a/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/GuiScriptedItemScreen.java +++ b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/GuiScriptedItemScreen.java @@ -8,17 +8,29 @@ import mchorse.mappet.network.common.items.PacketScriptedItemInfo; import mchorse.mappet.utils.NBTUtils; import mchorse.mclib.client.gui.framework.GuiBase; +import mchorse.mclib.client.gui.framework.elements.GuiElement; import mchorse.mclib.client.gui.framework.elements.GuiScrollElement; +import mchorse.mclib.client.gui.framework.elements.buttons.GuiIconElement; +import mchorse.mclib.client.gui.framework.elements.context.GuiSimpleContextMenu; import mchorse.mclib.client.gui.framework.elements.list.GuiLabelListElement; import mchorse.mclib.client.gui.framework.elements.utils.GuiLabel; import mchorse.mclib.client.gui.utils.Elements; +import mchorse.mclib.client.gui.utils.Icons; import mchorse.mclib.client.gui.utils.Label; import mchorse.mclib.client.gui.utils.keys.IKey; import net.minecraft.client.Minecraft; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; +import net.minecraftforge.common.util.Constants; public class GuiScriptedItemScreen extends GuiBase { + public GuiFormattedTextElement title; + public GuiElement lore; + public GuiIconElement addLore; + public GuiScrollElement editor; public GuiLabelListElement triggers; @@ -27,39 +39,78 @@ public class GuiScriptedItemScreen extends GuiBase private String lastTrigger = "interact_with_air"; + private final ItemStack stack; private final ScriptedItemProps props; - private final Minecraft mc; public GuiScriptedItemScreen(Minecraft mc, ItemStack stack) { - this.mc = mc; + this.stack = stack; + this.props = NBTUtils.getScriptedItemProps(stack); + /* Labels */ GuiLabel triggersLabel = Elements.label(IKey.lang("mappet.gui.scripted_item.title")).anchor(0, 0.5F).background(); + GuiLabel titleLabel = Elements.label(IKey.lang("mappet.gui.scripted_item.item_title")).anchor(0, 0.5F).background(); + GuiLabel loreLabel = Elements.label(IKey.lang("mappet.gui.scripted_item.item_lore")).anchor(0, 0.5F).background(); - triggersLabel.flex().relative(this.viewport).x(0.5F, 10).y(10).wh(120, 20); + /* UI components */ + this.title = new GuiFormattedTextElement(mc, stack::setStackDisplayName); + this.title.flex().relative(this.viewport).x(10).w(0.5F, -20).y(35).h(32); + + this.lore = Elements.column(mc, 5); + this.lore.flex().relative(loreLabel).y(1F, 8).w(1F).hTo(this.viewport, 1F, -10); + + this.addLore = new GuiIconElement(mc, Icons.ADD, (b) -> this.addLore(mc, "")); + this.addLore.flex().relative(loreLabel).x(1F, 3).y(-3).anchorX(1F); + this.addLore.tooltip(IKey.lang("mappet.gui.scripted_item.item_lore_add")); this.editor = new GuiScrollElement(mc); this.editor.flex().relative(this.viewport).x(0.5F).y(281).w(0.5F).h(1F, -311).column(5).scroll().stretch().padding(10); - this.triggers = new GuiLabelListElement<>(mc, (l) -> this.fillTrigger(l.get(0), false)); + this.triggers = new GuiLabelListElement<>(mc, (l) -> this.fillTrigger(mc, l.get(0), false)); this.triggers.background().flex().relative(this.viewport).x(0.5F, 10).y(35).w(0.5F, -20).h(246); this.trigger = new GuiTriggerElement(mc).onClose(this::updateCurrentTrigger); this.trigger.flex().relative(this.viewport).x(1F, -10).y(1F, -10).wh(120, 20).anchor(1F, 1F); - // Initialize properties - this.props = NBTUtils.getScriptedItemProps(stack); + triggersLabel.flex().relative(this.viewport).x(0.5F, 10).y(10).wh(120, 20); + titleLabel.flex().relative(this.viewport).x(10).y(10).wh(120, 20); + loreLabel.flex().relative(this.title).y(1F, 25).w(1F).h( 20); + + this.fill(mc); + + this.root.add(this.title); + this.root.add(this.triggers, this.editor, this.trigger, triggersLabel, titleLabel, loreLabel, this.lore, this.addLore); + } + + private void addLore(Minecraft mc, String lore) + { + GuiFormattedTextElement textElement = new GuiFormattedTextElement(mc, null); + + textElement.text.setText(lore); + textElement.context(() -> + { + GuiSimpleContextMenu menu = new GuiSimpleContextMenu(this.mc); + + menu.action(Icons.REMOVE, IKey.lang("mappet.gui.scripted_item.item_lore_remove"), () -> + { + textElement.removeFromParent(); + this.lore.resize(); + }); + + return menu; + }); - this.fill(); + textElement.flex().h(32); - this.root.add(this.triggers, this.editor, this.trigger, triggersLabel); + this.lore.add(textElement); + this.lore.resize(); } - private void fillTrigger(Label trigger, boolean select) + private void fillTrigger(Minecraft mc, Label trigger, boolean select) { this.editor.removeAll(); - this.editor.add(new GuiText(this.mc).text(IKey.lang("mappet.gui.scripted_item." + trigger.value))); - this.editor.add(new GuiText(this.mc).text(IKey.lang("mappet.gui.scripted_item.descriptions." + trigger.value))); + this.editor.add(new GuiText(mc).text(IKey.lang("mappet.gui.scripted_item." + trigger.value))); + this.editor.add(new GuiText(mc).text(IKey.lang("mappet.gui.scripted_item.descriptions." + trigger.value))); this.trigger.set(this.props.registered.get(trigger.value)); @@ -94,7 +145,7 @@ public IKey createTooltip(String key, Trigger trigger) return IKey.comp(title, count); } - public void fill() + public void fill(Minecraft mc) { this.triggers.clear(); @@ -106,9 +157,24 @@ public void fill() this.triggers.sort(); this.triggers.setCurrentValue(this.lastTrigger); - this.fillTrigger(this.triggers.getCurrentFirst(), true); + this.fillTrigger(mc, this.triggers.getCurrentFirst(), true); this.triggers.resize(); + + /* Fill display name and lore */ + this.title.text.setText(this.stack.getDisplayName()); + + NBTTagCompound tag = this.stack.getTagCompound(); + + if (tag != null && tag.hasKey("display", Constants.NBT.TAG_COMPOUND) && tag.getCompoundTag("display").hasKey("Lore", Constants.NBT.TAG_LIST)) + { + NBTTagList lore = tag.getCompoundTag("display").getTagList("Lore", Constants.NBT.TAG_STRING); + + for (int i = 0; i < lore.tagCount(); i++) + { + this.addLore(mc, lore.getStringTagAt(i)); + } + } } @Override @@ -125,7 +191,26 @@ protected void closeScreen() // Reset 'pickedUp' property to false each time properties are edited this.props.pickedUp = false; - Dispatcher.sendToServer(new PacketScriptedItemInfo(this.props.toNBT(), 0)); + NBTTagList lore = new NBTTagList(); + + for (GuiFormattedTextElement element : this.lore.getChildren(GuiFormattedTextElement.class)) + { + String text = element.text.getText(); + + if (!text.isEmpty()) + { + lore.appendTag(new NBTTagString(text)); + } + } + + this.stack.getOrCreateSubCompound("display").setTag("Lore", lore); + + if (this.title.text.getText().trim().isEmpty()) + { + this.stack.getOrCreateSubCompound("display").removeTag("Name"); + } + + Dispatcher.sendToServer(new PacketScriptedItemInfo(this.props.toNBT(), this.stack.getTagCompound(), 0)); } @Override diff --git a/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/Pair.java b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/Pair.java new file mode 100644 index 00000000..ae480281 --- /dev/null +++ b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/Pair.java @@ -0,0 +1,39 @@ +package mchorse.mappet.client.gui.scripts.scriptedItem.util; + +import java.util.Objects; + +public class Pair +{ + public A a; + public B b; + + public Pair(A a, B b) + { + this.a = a; + this.b = b; + } + + @Override + public boolean equals(Object obj) + { + if (super.equals(obj)) + { + return true; + } + + if (obj instanceof Pair) + { + Pair pair = (Pair) obj; + + return Objects.equals(this.a, pair.b) && Objects.equals(this.b, pair.b); + } + + return false; + } + + @Override + public int hashCode() + { + return Objects.hash(this.a, this.b); + } +} \ No newline at end of file diff --git a/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/StringGroup.java b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/StringGroup.java new file mode 100644 index 00000000..81f819fb --- /dev/null +++ b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/StringGroup.java @@ -0,0 +1,33 @@ +package mchorse.mappet.client.gui.scripts.scriptedItem.util; + +import java.util.regex.Pattern; + +public enum StringGroup +{ + SPACE("[\\s]"), ALPHANUMERIC("[\\w\\d]"), OTHER("[^\\w\\d\\s]"); + + private Pattern regex; + + public static StringGroup get(String character) + { + for (StringGroup group : values()) + { + if (group.match(character)) + { + return group; + } + } + + return OTHER; + } + + private StringGroup(String regex) + { + this.regex = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + } + + public boolean match(String character) + { + return this.regex.matcher(character).matches(); + } +} diff --git a/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/StringGroupMatcher.java b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/StringGroupMatcher.java new file mode 100644 index 00000000..a331fba5 --- /dev/null +++ b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/StringGroupMatcher.java @@ -0,0 +1,85 @@ +package mchorse.mappet.client.gui.scripts.scriptedItem.util; + +import mchorse.mclib.utils.MathUtils; + +public class StringGroupMatcher +{ + private StringGroup lastGroup; + + /** + * Find a group (two cursors) at given cursor + */ + public Pair findGroup(int direction, String line, int offset) + { + if (line.isEmpty()) + { + return null; + } + + int first = direction < 0 && offset > 0 ? offset - 1 : offset; + + first = MathUtils.clamp(first, 0, line.length() - 1); + + String character = String.valueOf(line.charAt(first)); + StringGroup group = StringGroup.get(character); + + int min = offset; + int max = offset; + + this.lastGroup = null; + + if (direction <= 0) + { + while (min > 0) + { + if (this.matchSelectGroup(group, String.valueOf(line.charAt(min - 1)))) + { + min -= 1; + } + else + { + break; + } + } + } + + this.lastGroup = null; + + if (direction >= 0) + { + while (max < line.length()) + { + if (this.matchSelectGroup(group, String.valueOf(line.charAt(max)))) + { + max += 1; + } + else + { + break; + } + } + } + + return new Pair(min, max); + } + + private boolean matchSelectGroup(StringGroup group, String character) + { + if (group.match(character)) + { + return this.lastGroup == null; + } + + if (group == StringGroup.SPACE) + { + if (this.lastGroup == null) + { + this.lastGroup = StringGroup.get(character); + } + + return StringGroup.get(character) == this.lastGroup; + } + + return false; + } +} \ No newline at end of file diff --git a/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/Textbox.java b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/Textbox.java new file mode 100644 index 00000000..3d367acd --- /dev/null +++ b/src/main/java/mchorse/mappet/client/gui/scripts/scriptedItem/util/Textbox.java @@ -0,0 +1,765 @@ +package mchorse.mappet.client.gui.scripts.scriptedItem.util; + +import mchorse.mclib.McLib; +import mchorse.mclib.client.gui.framework.elements.utils.GuiContext; +import mchorse.mclib.client.gui.framework.elements.utils.GuiDraw; +import mchorse.mclib.client.gui.utils.Area; +import mchorse.mclib.client.gui.utils.keys.IKey; +import mchorse.mclib.utils.ColorUtils; +import mchorse.mclib.utils.MathUtils; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.ChatAllowedCharacters; +import org.lwjgl.input.Keyboard; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class Textbox +{ + private String text = ""; + private Consumer callback; + + private int cursor; + private int selection = -1; + private int left; + private int right; + + private Predicate validator; + private int length = 100; + private boolean focused; + private boolean enabled = true; + private boolean visible = true; + + private IKey placeholder = IKey.EMPTY; + private boolean background = true; + private int color = 0xffffffff; + private boolean border; + + private boolean holding; + private int lastX; + + public Area area = new Area(); + public FontRenderer font; + + private int lastW; + + private long lastClick; + + public Textbox(Consumer callback) + { + this.callback = callback; + } + + public void setFont(FontRenderer font) + { + this.font = font; + + this.updateBounds(false); + } + + public FontRenderer getFont() + { + return this.font; + } + + public void setPlaceholder(IKey placeholder) + { + this.placeholder = placeholder; + } + + public void setBorder(boolean border) + { + this.border = border; + } + + /* Text */ + + public String getSelectedText() + { + if (this.isSelected()) + { + int min = Math.min(this.cursor, this.selection); + int max = Math.max(this.cursor, this.selection); + + return this.text.substring(min, max); + } + + return ""; + } + + public String getText() + { + return this.text; + } + + public void setText(String text) + { + if (text.length() > this.length) + { + text = text.substring(0, this.length); + } + + this.text = text; + + this.moveCursorToStart(); + this.deselect(); + this.updateBounds(false); + } + + public void acceptText() + { + if (this.callback != null) + { + this.callback.accept(this.text); + } + } + + public void insert(String text) + { + this.deleteSelection(); + + text = text.replaceAll("\n", ""); + + int i = this.text.length() + text.length(); + + if (i >= this.length) + { + text = text.substring(0, this.length - this.text.length()); + } + + if (text.isEmpty()) + { + return; + } + + String newText = this.text; + + if (this.cursor == 0) + { + newText = text + newText; + } + else if (this.cursor >= newText.length()) + { + newText += text; + } + else + { + newText = newText.substring(0, this.cursor) + text + newText.substring(this.cursor); + } + + this.text = newText; + this.moveCursorTo(this.cursor + text.length()); + + this.updateBounds(false); + } + + public void deleteCharacter() + { + if (this.cursor > 0) + { + this.text = this.text.substring(0, this.cursor - 1) + this.text.substring(this.cursor); + this.moveCursorBy(-1); + } + } + + public void setValidator(Predicate validator) + { + this.validator = validator; + } + + public int getLength() + { + return this.length; + } + + public void setLength(int length) + { + if (this.text.length() > length) + { + this.text = this.text.substring(0, length); + this.updateBounds(false); + } + + this.length = length; + } + + /* Cursor, selection and offsets */ + + public int getCursor() + { + return this.cursor; + } + + public int getSelection() + { + return this.selection; + } + + public boolean selectGroup(int direction, boolean select) + { + Pair groups = this.findGroup(direction, this.cursor); + + if (groups == null) + { + return false; + } + + int min = groups.a; + int max = groups.b; + + if (select) + { + if (direction == 0) + { + this.cursor = max; + this.selection = min; + } + else + { + if (!this.isSelected()) + { + this.selection = this.cursor; + } + + this.cursor = direction < 0 ? min : max; + } + } + else + { + this.deselect(); + this.cursor = direction < 0 ? min : max; + } + + this.updateBounds(false); + + return true; + } + + /** + * Find a group (two cursors) at given offset + */ + public Pair findGroup(int direction, int offset) + { + StringGroupMatcher matcher = new StringGroupMatcher(); + + return matcher.findGroup(direction, this.text, offset); + } + + public void moveCursorTo(int cursor) + { + this.cursor = cursor; + this.cursor = MathUtils.clamp(this.cursor, 0, this.text.length()); + + this.updateBounds(false); + } + + public void moveCursorToStart() + { + this.moveCursorTo(0); + } + + public void moveCursorToEnd() + { + this.moveCursorTo(this.text.length()); + } + + private void moveCursorBy(int i) + { + this.moveCursorTo(this.cursor + (int) Math.copySign(1, i)); + } + + public boolean isSelected() + { + return this.selection != this.cursor && this.selection >= 0; + } + + public void setSelection(int selection) + { + this.selection = selection; + + this.updateBounds(true); + } + + public void deselect() + { + this.selection = -1; + } + + public void deleteSelection() + { + if (this.cursor == this.selection) + { + this.deselect(); + } + + if (!this.isSelected()) + { + return; + } + + int min = Math.min(this.cursor, this.selection); + int max = Math.max(this.cursor, this.selection); + + this.text = this.text.substring(0, min) + this.text.substring(max); + + this.deselect(); + + this.cursor = min; + + this.updateBounds(false); + this.clamp(); + } + + private void updateBounds(boolean selection) + { + int cursor = selection ? this.selection : this.cursor; + int length = this.text.length(); + int offset = this.background ? 10 : 0; + int max = this.area.w - offset; + + if (this.font.getStringWidth(this.text) < max) + { + this.left = 0; + this.right = length; + + return; + } + + if (cursor < this.left) + { + int bound = this.getBound(max, cursor, 1); + + if (bound == cursor) + { + bound = this.getBound(max, length - 1, -1); + + this.left = bound; + this.right = length; + } + else + { + this.left = cursor; + this.right = MathUtils.clamp(bound + 1, 0, length); + } + } + else if (cursor >= this.right) + { + int bound = this.getBound(max, MathUtils.clamp(cursor, 0, length - 1), -1); + + if (bound == cursor) + { + bound = this.getBound(max, 0, 1); + + this.left = 0; + this.right = bound; + } + else + { + this.left = bound; + this.right = cursor; + } + } + + this.left = MathUtils.clamp(this.left, 0, length); + this.right = MathUtils.clamp(this.right, 0, length); + } + + private int getBound(int max, int start, int direction) + { + int w = 0; + + for (int i = start; i >= 0 && i < this.text.length(); i += direction) + { + int sw = this.font.getCharWidth(this.text.charAt(i)); + + if (w < max && w + sw >= max) + { + return i; + } + + w += sw; + } + + return start; + } + + private void clamp() + { + this.cursor = MathUtils.clamp(this.cursor, 0, this.text.length()); + this.selection = MathUtils.clamp(this.selection, -1, this.text.length()); + } + + private String getWrappedText() + { + int length = this.text.length(); + + return this.text.substring( + MathUtils.clamp(this.left, 0, length), + MathUtils.clamp(this.right, 0, length) + ); + } + + /* Visual */ + + public boolean hasBackground() + { + return this.background; + } + + public void setBackground(boolean background) + { + this.background = background; + } + + public int getColor() + { + return this.color; + } + + public void setColor(int color) + { + this.color = color; + } + + public boolean isVisible() + { + return this.visible; + } + + public void setVisible(boolean visible) + { + this.visible = visible; + } + + /* Input handling */ + + public boolean isEnabled() + { + return this.enabled; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + public boolean isFocused() + { + return this.focused; + } + + public void setFocused(boolean focused) + { + this.focused = focused; + } + + public void mouseClicked(int x, int y, int button) + { + if (button == 0 && this.area.isInside(x, y)) + { + if (System.currentTimeMillis() < this.lastClick) + { + this.selectGroup(0, true); + this.lastClick -= 500; + } + else + { + int lastSelection = this.selection; + + this.deselect(); + + if (GuiScreen.isShiftKeyDown()) + { + this.selection = lastSelection < 0 ? this.cursor : lastSelection; + } + + this.focused = true; + this.lastX = x; + this.holding = true; + this.moveCursorTo(this.getIndexAt(x)); + + this.lastClick = System.currentTimeMillis() + 200; + } + } + else + { + this.focused = false; + } + } + + public void mouseReleased(int x, int y, int button) + { + if (button == 0) + { + this.holding = false; + } + } + + private int getIndexAt(int x) + { + x -= this.area.x; + + if (this.background) + { + x -= 4; + } + + if (x >= 0) + { + String wrappedText = this.getWrappedText(); + int w = this.font.getStringWidth(wrappedText); + + if (x >= w) + { + return this.right; + } + else + { + w = 0; + + for (int i = 0, c = wrappedText.length(); i < c; i++) + { + char character = wrappedText.charAt(i); + + if (character == '\u00A7') + { + i += 1; + + continue; + } + + int string = this.font.getCharWidth(character); + + if (x >= w && x < w + string) + { + return this.left + i; + } + + w += string; + } + } + } + + return this.left; + } + + public boolean keyPressed(GuiContext context) + { + if (!this.focused || !this.enabled || !this.visible) + { + return false; + } + + boolean selecting = this.isSelected(); + boolean ctrl = GuiScreen.isCtrlKeyDown(); + boolean shift = GuiScreen.isShiftKeyDown(); + + if (ctrl && (context.keyCode == Keyboard.KEY_C || context.keyCode == Keyboard.KEY_X)) + { + if (selecting) + { + GuiScreen.setClipboardString(this.getSelectedText()); + + if (context.keyCode == Keyboard.KEY_X) + { + this.deleteSelection(); + } + + return true; + } + } + else if (ctrl && context.keyCode == Keyboard.KEY_V) + { + String clipboard = GuiScreen.getClipboardString(); + + if (!clipboard.isEmpty()) + { + this.insert(clipboard); + this.acceptText(); + } + + return true; + } + else if (ctrl && context.keyCode == Keyboard.KEY_A) + { + this.selection = 0; + this.cursor = this.text.length(); + this.updateBounds(false); + + return true; + } + else if (context.keyCode == Keyboard.KEY_HOME) + { + this.handleShift(shift); + this.moveCursorToStart(); + } + else if (context.keyCode == Keyboard.KEY_END) + { + this.handleShift(shift); + this.moveCursorToEnd(); + } + else if (context.keyCode == Keyboard.KEY_LEFT || context.keyCode == Keyboard.KEY_RIGHT) + { + int offset = context.keyCode == Keyboard.KEY_LEFT ? -1 : 1; + + if (ctrl) + { + if (!this.selectGroup(offset, shift)) + { + this.handleShift(shift); + this.moveCursorBy(offset); + } + } + else + { + this.handleShift(shift); + this.moveCursorBy(offset); + } + } + else if (context.keyCode == Keyboard.KEY_BACK || context.keyCode == Keyboard.KEY_DELETE) + { + if (this.isSelected()) + { + this.deleteSelection(); + this.acceptText(); + + return true; + } + else if (context.keyCode == Keyboard.KEY_DELETE && this.cursor < this.text.length()) + { + this.moveCursorBy(1); + this.deleteCharacter(); + this.acceptText(); + + return true; + } + else if (context.keyCode == Keyboard.KEY_BACK) + { + this.deleteCharacter(); + this.acceptText(); + + return true; + } + } + + return false; + } + + public boolean textInput(char character) + { + if (!this.focused || !this.enabled || !this.visible) + { + return false; + } + + if (ChatAllowedCharacters.isAllowedCharacter(character)) + { + String text = String.valueOf(character); + + if (this.validator != null && !this.validator.test(text)) + { + return false; + } + + this.insert(text); + this.acceptText(); + + return true; + } + + return false; + } + + private void handleShift(boolean shift) + { + if (shift) + { + if (this.selection == -1) + { + this.selection = cursor; + } + } + else + { + this.deselect(); + } + } + + /* Rendering */ + + public void render(GuiContext context) + { + if (!this.visible) + { + return; + } + + int mouseX = context.mouseX; + int mouseY = context.mouseY; + + if (this.lastW != this.area.w) + { + this.lastW = this.area.w; + this.updateBounds(false); + } + + if (this.area.isInside(mouseX, mouseY) && this.holding && Math.abs(mouseX - this.lastX) > 2) + { + this.moveCursorTo(this.getIndexAt(mouseX)); + } + + int x = this.area.x; + int y = this.area.y; + + if (this.background) + { + this.area.draw(0xff000000); + + if (this.border) + { + int borderColor = this.focused ? 0xff000000 + McLib.primaryColor.get() : 0xffaaaaaa; + + GuiDraw.drawOutline(this.area.x, this.area.y, this.area.ex(), this.area.ey(), borderColor); + } + + x = this.area.x + 4; + y = this.area.my() - this.font.FONT_HEIGHT / 2; + } + + boolean empty = !this.focused && this.text.isEmpty(); + String text = empty ? this.placeholder.get() : this.getWrappedText(); + int length = text.length(); + int color = empty ? 0xaaaaaa : this.color; + + if (!empty && this.isSelected()) + { + int min = MathUtils.clamp(Math.min(this.cursor, this.selection) - this.left, 0, length); + int max = MathUtils.clamp(Math.max(this.cursor, this.selection) - this.left, 0, length); + + int offset = this.font.getStringWidth(text.substring(0, min)); + int sx = x + offset; + int sw = this.font.getStringWidth(text.substring(min, max)); + + Gui.drawRect(sx, y - 2, sx + sw, y + this.font.FONT_HEIGHT + 2, 0x88000000 + McLib.primaryColor.get()); + } + + context.font.drawStringWithShadow(text, x, y, color); + + if (this.focused) + { + int relativeIndex = this.cursor - this.left; + + if (relativeIndex >= 0 && relativeIndex <= length) + { + x += this.font.getStringWidth(text.substring(0, relativeIndex)); + + float alpha = (float) Math.sin(context.partialTicks / 2D); + int c = ColorUtils.setAlpha(0xffffff, alpha * 0.5F + 0.5F); + + Gui.drawRect(x, y - 1, x + 1, y + this.font.FONT_HEIGHT + 1, c); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/mchorse/mappet/network/client/items/ClientHandlerScriptedItemInfo.java b/src/main/java/mchorse/mappet/network/client/items/ClientHandlerScriptedItemInfo.java index a697a35f..f651e108 100644 --- a/src/main/java/mchorse/mappet/network/client/items/ClientHandlerScriptedItemInfo.java +++ b/src/main/java/mchorse/mappet/network/client/items/ClientHandlerScriptedItemInfo.java @@ -29,6 +29,11 @@ public void run(EntityPlayerSP player, PacketScriptedItemInfo message) return; } + if (message.stackTag != null) + { + stack.setTagCompound(message.stackTag); + } + NBTUtils.saveScriptedItemProps(stack, message.tag); } } diff --git a/src/main/java/mchorse/mappet/network/common/items/PacketScriptedItemInfo.java b/src/main/java/mchorse/mappet/network/common/items/PacketScriptedItemInfo.java index 455e0323..ea868c86 100644 --- a/src/main/java/mchorse/mappet/network/common/items/PacketScriptedItemInfo.java +++ b/src/main/java/mchorse/mappet/network/common/items/PacketScriptedItemInfo.java @@ -9,6 +9,7 @@ public class PacketScriptedItemInfo implements IMessage { public NBTTagCompound tag; + public NBTTagCompound stackTag; public int entity; public PacketScriptedItemInfo() @@ -16,9 +17,10 @@ public PacketScriptedItemInfo() this.tag = new NBTTagCompound(); } - public PacketScriptedItemInfo(NBTTagCompound tag, int entity) + public PacketScriptedItemInfo(NBTTagCompound tag, NBTTagCompound stackTag, int entity) { this.tag = tag; + this.stackTag = stackTag; this.entity = entity; } @@ -26,6 +28,12 @@ public PacketScriptedItemInfo(NBTTagCompound tag, int entity) public void fromBytes(ByteBuf buf) { this.tag = NBTUtils.readInfiniteTag(buf); + + if (buf.readBoolean()) + { + this.stackTag = NBTUtils.readInfiniteTag(buf); + } + this.entity = buf.readInt(); } @@ -33,6 +41,14 @@ public void fromBytes(ByteBuf buf) public void toBytes(ByteBuf buf) { ByteBufUtils.writeTag(buf, this.tag); + + buf.writeBoolean(this.stackTag != null); + + if (this.stackTag != null) + { + ByteBufUtils.writeTag(buf, this.stackTag); + } + buf.writeInt(this.entity); } } diff --git a/src/main/java/mchorse/mappet/network/server/items/ServerHandlerScriptedItemInfo.java b/src/main/java/mchorse/mappet/network/server/items/ServerHandlerScriptedItemInfo.java index cb4c9112..b02b9c62 100644 --- a/src/main/java/mchorse/mappet/network/server/items/ServerHandlerScriptedItemInfo.java +++ b/src/main/java/mchorse/mappet/network/server/items/ServerHandlerScriptedItemInfo.java @@ -21,9 +21,14 @@ public void run(EntityPlayerMP player, PacketScriptedItemInfo message) ItemStack stack = player.getHeldItemMainhand(); + if (message.stackTag != null) + { + stack.setTagCompound(message.stackTag); + } + if (NBTUtils.saveScriptedItemProps(stack, message.tag)) { - IMessage packet = new PacketScriptedItemInfo(message.tag, player.getEntityId()); + IMessage packet = new PacketScriptedItemInfo(message.tag, message.stackTag, player.getEntityId()); Dispatcher.sendTo(packet, player); Dispatcher.sendToTracked(player, packet); } diff --git a/src/main/resources/assets/mappet/lang/en_US.lang b/src/main/resources/assets/mappet/lang/en_US.lang index 54c9f602..104cd717 100644 --- a/src/main/resources/assets/mappet/lang/en_US.lang +++ b/src/main/resources/assets/mappet/lang/en_US.lang @@ -418,6 +418,10 @@ mappet.gui.npc_tool.npc=NPC mappet.gui.npc_tool.state=State mappet.gui.scripted_item.title=Scripted item configuration +mappet.gui.scripted_item.item_title=Display name +mappet.gui.scripted_item.item_lore=Lore +mappet.gui.scripted_item.item_lore_add=Add a line of lore +mappet.gui.scripted_item.item_lore_remove=Remove this line of lore mappet.gui.scripted_item.interact_with_air=Player: On air interaction mappet.gui.scripted_item.interact_with_entity=Player: On entity interaction mappet.gui.scripted_item.interact_with_block=Player: On block interaction