diff --git a/src/main/java/ch/njol/skript/expressions/ExprLore.java b/src/main/java/ch/njol/skript/expressions/ExprLore.java index 7b4c26361cf..ad30682acb4 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprLore.java +++ b/src/main/java/ch/njol/skript/expressions/ExprLore.java @@ -1,236 +1,181 @@ package ch.njol.skript.expressions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.event.Event; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.jetbrains.annotations.Nullable; - import ch.njol.skript.Skript; -import ch.njol.skript.SkriptConfig; import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.bukkitutil.ItemUtils; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.classes.Changer.ChangerUtils; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; +import ch.njol.skript.doc.*; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.registrations.EventValues; import ch.njol.util.Kleenean; -import ch.njol.util.Math2; -import ch.njol.util.StringUtils; import ch.njol.util.coll.CollectionUtils; +import org.bukkit.event.Event; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; /** * TODO make a 'line %number% of %text%' expression and figure out how to deal with signs (4 lines, delete = empty, etc...) - * - * @author joeuguce99 */ @Name("Lore") @Description("An item's lore.") -@Examples("set the 1st line of the item's lore to \"<orange>Excalibur 2.0\"") +@Example(""" + set {_item} to diamond named "Bling Diamond" with lore "&bThe blingiest of diamonds" + add "With extra steps" to lore of {_item} + remove line 1 of lore of {_item} from lore of {_item} + set line 3 of lore of {_item} to "-----" + """) @Since("2.1") public class ExprLore extends SimpleExpression { + private static final boolean IS_RUNNING_1_21_5 = Skript.isRunningMinecraft(1,21,5); + static { - Skript.registerExpression(ExprLore.class, String.class, ExpressionType.PROPERTY, - "[the] lore of %itemstack/itemtype%", "%itemstack/itemtype%'[s] lore", - "[the] line %number% of [the] lore of %itemstack/itemtype%", - "[the] line %number% of %itemstack/itemtype%'[s] lore", - "[the] %number%(st|nd|rd|th) line of [the] lore of %itemstack/itemtype%", - "[the] %number%(st|nd|rd|th) line of %itemstack/itemtype%'[s] lore"); + Skript.registerExpression(ExprLore.class, String.class, ExpressionType.PROPERTY, + "[the] lore of %itemstack/itemtype%", + "%itemstack/itemtype%'[s] lore", + "line %number% of [the] lore of %itemstack/itemtype%", + "line %number% of %itemstack/itemtype%'[s] lore", + "[the] %number%(st|nd|rd|th) line of [the] lore of %itemstack/itemtype%", + "[the] %number%(st|nd|rd|th) line of %itemstack/itemtype%'[s] lore"); } - @Nullable - private Expression lineNumber; - - @SuppressWarnings("null") + private @Nullable Expression lineNumber; private Expression item; - @SuppressWarnings({"unchecked", "null"}) @Override - public boolean init(final Expression[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parseResult) { + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + //noinspection unchecked lineNumber = exprs.length > 1 ? (Expression) exprs[0] : null; item = exprs[exprs.length - 1]; return true; } @Override - @Nullable - protected String[] get(final Event e) { - final Object i = item.getSingle(e); - final Number n = lineNumber != null ? lineNumber.getSingle(e) : null; - if (n == null && lineNumber != null) - return null; - if (i == null || i instanceof ItemStack && ((ItemStack) i).getType() == Material.AIR) + protected String @Nullable [] get(Event event) { + if (!validateItem(item.getSingle(event))) return new String[0]; - final ItemMeta meta = i instanceof ItemStack ? ((ItemStack) i).getItemMeta() : (ItemMeta) ((ItemType) i).getItemMeta(); - if (meta == null || !meta.hasLore()) + ItemStack itemStack = ItemUtils.asItemStack(item.getSingle(event)); + assert itemStack != null; // Validated in validateItem + ItemMeta itemMeta = itemStack.getItemMeta(); + //noinspection deprecation + List itemLore = itemMeta.getLore(); + if (itemLore == null || itemLore.isEmpty()) return new String[0]; - final List lore = meta.getLore(); - assert lore != null; // hasLore() called before - if (n == null) - return lore.toArray(new String[0]); - final int l = n.intValue() - 1; - if (l < 0 || l >= lore.size()) + + if (lineNumber == null) { + return itemLore.toArray(String[]::new); + } + + int loreIndex = this.lineNumber.getOptionalSingle(event).orElse(0).intValue() -1; + if (loreIndex < 0 || loreIndex >= itemLore.size()) { return new String[0]; - return new String[]{lore.get(l)}; + } + return new String[]{itemLore.get(loreIndex)}; } @Override - @Nullable - public Class[] acceptChange(final ChangeMode mode) { - boolean acceptsMany = lineNumber == null; - switch (mode) { - case REMOVE: - case REMOVE_ALL: - case DELETE: - acceptsMany = false; - case SET: - case ADD: - if (ChangerUtils.acceptsChange(item, ChangeMode.SET, ItemStack.class, ItemType.class)) { - return CollectionUtils.array(acceptsMany ? String[].class : String.class); - } - return null; - case RESET: - default: - return null; + public Class @Nullable [] acceptChange(ChangeMode mode) { + if (this.item.getTime() != EventValues.TIME_NOW && !ChangerUtils.acceptsChange(item, ChangeMode.SET, ItemStack.class, ItemType.class)) { + // Allows past/future expressions only if they accept the set change mode + return null; } + boolean acceptsMany = this.lineNumber == null; + return switch (mode) { + case SET -> CollectionUtils.array(acceptsMany ? String[].class : String.class); + case DELETE -> CollectionUtils.array(); + case ADD, REMOVE, REMOVE_ALL -> { + if (!acceptsMany) { + Skript.error("You cannot add to or remove from the lore from a single line."); + yield null; + } + yield CollectionUtils.array(String[].class); + } + default -> null; + }; } @Override - public void change(final Event e, final @Nullable Object[] delta, final ChangeMode mode) throws UnsupportedOperationException { - Object i = item.getSingle(e); - - String[] stringDelta = delta == null ? null : Arrays.copyOf(delta, delta.length, String[].class); - - // air is just nothing, it can't have a lore - if (i == null || i instanceof ItemStack && ((ItemStack) i).getType() == Material.AIR) - return; - - ItemMeta meta = i instanceof ItemStack ? ((ItemStack) i).getItemMeta() : (ItemMeta) ((ItemType) i).getItemMeta(); - if (meta == null) - meta = Bukkit.getItemFactory().getItemMeta(Material.STONE); - - Number lineNumber = this.lineNumber != null ? this.lineNumber.getSingle(e) : null; - List lore = meta.hasLore() ? new ArrayList<>(meta.getLore()) : new ArrayList<>(); - - if (lineNumber == null) { - // if the condition below is true, the pattern with the line %number% expression was used, - // but the line number turned out to be null at runtime, meaning we should ignore it - if (this.lineNumber != null) { - return; - } - + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + Object item = this.item.getSingle(event); + if (!validateItem(item)) + return; // Validates to ensure it is a valid item and has item meta. + ItemStack modifiedItem = ItemUtils.asItemStack(item); + assert modifiedItem != null; // validateItem has already run a check against this + ItemMeta itemMeta = modifiedItem.getItemMeta(); + //noinspection deprecation + List modifiedLore = itemMeta.hasLore() ? itemMeta.getLore() : new ArrayList<>(); + assert modifiedLore != null; // lore can never be null here, if it's unset we create an empty list + + if (this.lineNumber == null) { switch (mode) { - case SET: - assert stringDelta != null; - List newLore = new ArrayList<>(); - for (String line : stringDelta) { - if (line.contains("\n")) { - Collections.addAll(newLore, line.split("\n")); - continue; - } - newLore.add(line); - } - lore = newLore; - break; - case ADD: - assert stringDelta != null; - List addLore = new ArrayList<>(); - for (String line : stringDelta) { - if (line.contains("\n")) { - Collections.addAll(addLore, line.split("\n")); - continue; - } - addLore.add(line); + case SET, DELETE -> modifiedLore = (delta != null) ? List.of((String[]) delta) : null; + case ADD -> { + assert delta != null; + modifiedLore.addAll(List.of((String[]) delta)); + } + case REMOVE, REMOVE_ALL -> { + boolean isAll = mode == ChangeMode.REMOVE_ALL; + if (isAll) { + //noinspection DataFlowIssue + modifiedLore.removeAll(List.of((String[]) delta)); + } else { + //noinspection DataFlowIssue + for (String string : (String[]) delta) + modifiedLore.remove(string); } - lore.addAll(addLore); - break; - case DELETE: - lore = null; - break; - case REMOVE: - case REMOVE_ALL: - assert stringDelta != null; - lore = Arrays.asList(handleRemove( - StringUtils.join(lore, "\n"), stringDelta[0], mode == ChangeMode.REMOVE_ALL).split("\n")); - break; - case RESET: - assert false; - return; + } } } else { - // Note: line number is changed from one-indexed to zero-indexed here - int lineNum = Math2.fit(0, lineNumber.intValue() - 1, 99); // TODO figure out the actual maximum - - // Fill in the empty lines above the line being set with empty strings (avoids index out of bounds) - while (lore.size() <= lineNum) - lore.add(""); + int loreIndex = this.lineNumber.getOptionalSingle(event).orElse(0).intValue() -1; + if (loreIndex < 0) + return; // Cannot change anything in lore if it's negative, therefor we return switch (mode) { - case SET: - assert stringDelta != null; - lore.set(lineNum, stringDelta[0]); - break; - case ADD: - assert stringDelta != null; - lore.set(lineNum, lore.get(lineNum) + stringDelta[0]); - break; - case DELETE: - lore.remove(lineNum); - break; - case REMOVE: - case REMOVE_ALL: - assert stringDelta != null; - lore.set(lineNum, handleRemove(lore.get(lineNum), stringDelta[0], mode == ChangeMode.REMOVE_ALL)); - break; - case RESET: - assert false; - return; + case SET -> { + for (int line = modifiedLore.size()-1; line < loreIndex; line++) { + modifiedLore.add(""); + } + //noinspection DataFlowIssue + modifiedLore.set(loreIndex, (String) delta[0]); + } + case DELETE -> { + if (loreIndex > modifiedLore.size() || !itemMeta.hasLore()) + return; // Cannot change anything in lore, therefor we return + modifiedLore.remove(loreIndex); + } } } - meta.setLore(lore == null || lore.size() == 0 ? null : lore); - if (i instanceof ItemStack) { - ((ItemStack) i).setItemMeta(meta); - } else { - ((ItemType) i).setItemMeta(meta); + if (modifiedLore != null && !modifiedLore.isEmpty()) { + if (IS_RUNNING_1_21_5) { + // The maximum amount of lore an item can have is 256 + // this change was made in 1.21.5 + // source: https://minecraft.wiki/w/Data_component_format#lore + modifiedLore = modifiedLore.stream().limit(256).toList(); + } else { + modifiedLore = modifiedLore.stream().limit(99).toList(); + } } - - if (ChangerUtils.acceptsChange(item, ChangeMode.SET, i.getClass())) { - Object[] itemDelta = i instanceof ItemStack ? new ItemStack[]{(ItemStack) i} : new ItemType[]{(ItemType) i}; - item.change(e, itemDelta, ChangeMode.SET); - } else { - Object[] itemDelta = i instanceof ItemStack ? new ItemType[]{new ItemType((ItemStack) i)} : - new ItemStack[]{((ItemType) i).getRandom()}; - item.change(e, itemDelta, ChangeMode.SET); + //noinspection deprecation + itemMeta.setLore(modifiedLore); + if (item instanceof ItemType itemType) { + itemType.setItemMeta(itemMeta); + } else if (item instanceof ItemStack itemStack) { + itemStack.setItemMeta(itemMeta); } } - private String handleRemove(String input, String toRemove, boolean all) { - if (SkriptConfig.caseSensitive.value()) { - if (all) { - return input.replace(toRemove, ""); - } else { - // .replaceFirst requires the regex to be quoted, .replace does it internally - return input.replaceFirst(Pattern.quote(toRemove), ""); - } - } else { - final Matcher m = Pattern.compile(Pattern.quote(toRemove), - Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE).matcher(input); - return all ? m.replaceAll("") : m.replaceFirst(""); - } + private boolean validateItem(Object item) { + ItemStack itemStack = ItemUtils.asItemStack(item); + return itemStack != null && itemStack.getItemMeta() != null; } @Override @@ -244,7 +189,11 @@ public Class getReturnType() { } @Override - public String toString(final @Nullable Event e, final boolean debug) { - return (lineNumber != null ? "the line " + lineNumber.toString(e, debug) + " of " : "") + "the lore of " + item.toString(e, debug); + public String toString(@Nullable Event event, boolean debug) { + SyntaxStringBuilder syntaxBuilder = new SyntaxStringBuilder(event, debug); + if (lineNumber != null) + return syntaxBuilder.append("line", lineNumber, "of the lore of", item).toString(); + return syntaxBuilder.append("the lore of", item).toString(); } + } diff --git a/src/test/skript/tests/syntaxes/expressions/ExprLore.sk b/src/test/skript/tests/syntaxes/expressions/ExprLore.sk new file mode 100644 index 00000000000..c0273046dec --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprLore.sk @@ -0,0 +1,35 @@ +test "ExprLore - Lore of Item": + set {_item} to a diamond + set lore of {_item} to "Line1", "Line2", "Line3" and "Line4" + assert lore of {_item} is "Line1", "Line2", "Line3" and "Line4" + + copy {_item} into {_item2} + set lore of {_item2} to {_null} + assert lore of {_item2} is not set + + remove "Line2" from lore of {_item} + remove {_null} from lore of {_item} + assert lore of {_item} is "Line1", "Line3" and "Line4" + + add "Line5" and "Line5" to lore of {_item} + add {_null} to lore of {_item} + assert lore of {_item} is "Line1", "Line3", "Line4", "Line5" and "Line5" + + remove all "Line5" from lore of {_item} + assert lore of {_item} is "Line1", "Line3" and "Line4" + + clear lore of {_item} + assert lore of {_item} is not set with "The lore of an item after being cleared did not become null" + +test "ExprLore - Line X of Lore of Item": + set {_item} to a diamond + set lore of {_item} to "Line1", "Line2", "Line3" + + delete line 3 of lore of {_item} + assert line 3 of lore of {_item} is not set with "Line 3 of item was not removed" + + set line 6 of lore of {_item} to "3 Blanks = 1 Word" + assert line 6 of lore of {_item} is "3 Blanks = 1 Word" + + set line 4 of lore of {_item} to {_null} + assert size of lore of {_item} is 5 with "Setting a line to a null value, did not default to delete"