diff --git a/.gitignore b/.gitignore index a8a6a665..7fc1007e 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,6 @@ build # Common working directory run/ + +# Proto +compile_proto.sh \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 50c6e120..f6a96dbc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,7 +50,7 @@ dependencies { implementation(project(":v1_20_R4", configuration = "default")) } -var pluginVersion = "1.7.10-pre2" +var pluginVersion = "1.8-pre2" allprojects { group = "dev.foxikle" @@ -107,7 +107,7 @@ tasks { compileJava { options.encoding = Charsets.UTF_8.name() - options.release = 21 + options.release = 25 } javadoc { source = sourceSets["main"].allSource @@ -126,7 +126,7 @@ tasks { "apiVersion" to "1.20" ) inputs.properties(props) - filesMatching("plugin.yml") { + filesMatching("paper-plugin.yml") { expand(props) } } @@ -158,5 +158,3 @@ tasks.register("aggregatedJavadocs") { } } } - - diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 0849006a..075642e0 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -23,6 +23,8 @@ plugins { id("java") id("io.freefair.lombok") version "9.5.0" + `java-library` + java } repositories { @@ -39,10 +41,14 @@ dependencies { compileOnly("me.clip:placeholderapi:2.12.2") compileOnly("io.papermc.paper:paper-api:1.20.6-R0.1-SNAPSHOT") compileOnly("net.kyori:adventure-api:5.1.1") + compileOnly("net.kyori:adventure-nbt:5.1.1") compileOnly("org.mineskin:java-client-jsoup:3.2.6") - compileOnly("dev.velix:imperat-bukkit:1.9.7") - compileOnly("dev.velix:imperat-core:1.9.7") compileOnly("org.mineskin:java-client:3.2.6") + compileOnly("org.mongodb:mongodb-driver-sync:5.3.0") + compileOnly("com.mysql:mysql-connector-j:9.1.0") + compileOnly("com.zaxxer:HikariCP:6.2.1") + compileOnlyApi("dev.minestom-united.common:codec:0.0.2") + testImplementation("org.junit.jupiter:junit-jupiter:6.1.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher") @@ -112,7 +118,7 @@ tasks.compileJava { tasks { java { toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) + languageVersion.set(JavaLanguageVersion.of(25)) } } @@ -130,7 +136,7 @@ tasks { } compileJava { - options.release = 21 + options.release = 25 } jar { diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/Action.java b/core/src/main/java/dev/foxikle/customnpcs/actions/Action.java index 9e8c968c..779c81bc 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/Action.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/Action.java @@ -22,17 +22,21 @@ package dev.foxikle.customnpcs.actions; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.utils.Utils; import io.github.mqzen.menus.base.Menu; +import io.github.mqzen.menus.misc.button.Button; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import lombok.SneakyThrows; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,43 +49,135 @@ @Getter @Setter +@NoArgsConstructor public abstract class Action { + @Deprecated(forRemoval = true) private static final Pattern SPLITTER = Pattern.compile("^([A-z])*(?=(\\{.*}))"); - private List conditions = new ArrayList<>(); - private int delay = 0; - private Condition.SelectionMode mode = Condition.SelectionMode.ONE; - private int cooldown = 0; - private Map cooldowns = new ConcurrentHashMap<>(); - /** - * Default constructor - */ - public Action() { + public static final Codec CODEC = Codec.STRING.unionType("id", s -> { + Class clazz = CustomNPCs.ACTION_REGISTRY.getActionClass(s); + if (clazz == null) throw new IllegalArgumentException("Invalid action type: " + s); + try { + return clazz.newInstance().getCodec(); + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException("Failed to parse action: ", e); + } + }, Action::getId); - } + private List conditions; + private int delay; + private Selector selector; + private int cooldown; + private final Map cooldowns = new ConcurrentHashMap<>(); - public Action(int delay, Condition.SelectionMode mode, List conditions, int cooldown) { + public Action(int delay, Selector selector, List conditions, int cooldown) { this.delay = delay; - this.mode = mode; + this.selector = selector; this.conditions = conditions; this.cooldown = cooldown; } + public boolean isOnCooldown(UUID uuid) { + if (!cooldowns.containsKey(uuid)) return false; + Instant i = cooldowns.get(uuid); + if (i.isBefore(Instant.now())) { + cooldowns.remove(uuid); + return false; + } + return true; + } + + public void activateCooldown(UUID uuid) { + if (cooldown == 0) return; + cooldowns.put(uuid, Instant.now().plusMillis(50L * cooldown)); + } /** - * Deprecated, use {@link Action#Action(int, Condition.SelectionMode, List, int)} Action} + * A convenience method to add a condition to the action + * + * @param condition the condition to add */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public Action(int delay, Condition.SelectionMode mode, List conditions) { - this(delay, mode, conditions, 0); + public void addCondition(Condition condition) { + conditions.add(condition); } - protected static List deserializeConditions(String json) { - String data = parseArray(json, "conditions").replace("},]", "}]"); - return CustomNPCs.getGson().fromJson(data, Utils.CONDITIONS_LIST); + public void removeCondition(Condition condition) { + conditions.remove(condition); + } + + /** + * Contains the execution of the action + * + * @param npc The NPC + * @param menu The menu + * @param player The player + */ + public abstract void perform(InternalNpc npc, Menu menu, Player player); + + + /** + * The item used as an icon in the NPC's action menu + * + * @param player the player who will see them + * @return the ItemStack to use + */ + public abstract ItemStack getFavicon(Player player); + + public abstract Menu getMenu(); + + /** + * Returns if the action should be processed. This takes the cooldown into account. + * + * @param player the player + * @return if the action should be processed + */ + public boolean processConditions(Player player) { + if (isOnCooldown(player.getUniqueId())) return false; + if (conditions == null || conditions.isEmpty()) return true; // no conditions + Set results = new HashSet<>(conditions.size()); + conditions.forEach(conditional -> results.add(conditional.compute(player))); + return (selector == Selector.ALL ? !results.contains(false) : results.contains(true)); + } + + + /** + * Provides a codec for the union type serializer. + * You should include these elements in your implementation: + *
+     *     {@code
+     *             "delay", Codec.INT, Action::getDelay,
+     *             "selector", Codec.Enum(Selector.class), Action::getSelector,
+     *             "conditions", Condition.CODEC.list(), Action::getConditions,
+     *             "cooldown", Codec.INT, Action::getCooldown,
+     *     }
+     * 
+ *

+ * As a consequence of this, your action must have a public, no-args constructor for the codec to be accesible. + *

+ * + * @return the codec delegated to your action + * @see Codec Documentation + */ + public abstract StructCodec getCodec(); + + public abstract String getId(); + + public abstract Button creationButton(Player player); + + public abstract Action clone(); + + public boolean canEdit() { + return true; + } + + public boolean canDelay() { + return true; + } + + public boolean canDuplicate() { + return true; } /** @@ -89,6 +185,7 @@ protected static List deserializeConditions(String json) { */ @SneakyThrows @Nullable + @Deprecated(forRemoval = true) public static Action parse(@NotNull String s) { Matcher matcher = SPLITTER.matcher(s); @@ -110,16 +207,23 @@ public static Action parse(@NotNull String s) { } } + @Deprecated(forRemoval = true) + protected static List deserializeConditions(String json) { + String data = parseArray(json, "conditions").replace("},]", "}]"); + return CustomNPCs.getGson().fromJson(data, Utils.CONDITIONS_LIST); + } + /** * Parses the base data required for every action. (SelectionMode, delay, and conditions) * * @param data The serialized action string * @return The parsed "base" data, required for every action. */ + @Deprecated(forRemoval = true) protected static ParseResult parseBase(String data) { int cooldown = parseInt(data, "cooldown"); int delay = parseInt(data, "delay"); - Condition.SelectionMode mode = parseEnum(data, "mode", Condition.SelectionMode.class); + Selector mode = parseEnum(data, "mode", Selector.class); List conditions = deserializeConditions(parseString(data, "conditions")); return new ParseResult(delay, mode, conditions, cooldown); } @@ -133,6 +237,7 @@ protected static ParseResult parseBase(String data) { * @param the enum type * @return The parsed enum constant, by name */ + @Deprecated(forRemoval = true) protected static > T parseEnum(String data, String key, Class type) { String constantName = data.replaceAll(".*" + key + "=([A-Z_]+).*", "$1"); return Enum.valueOf(type, constantName); @@ -145,6 +250,7 @@ protected static > T parseEnum(String data, String key, Class< * @param key the key to search for, ie `delay` * @return The parsed integer. 0, if the key is not found. */ + @Deprecated(forRemoval = true) protected static int parseInt(String data, String key) { try { return Integer.parseInt(data.replaceAll(".*" + key + "=(\\d+).*", "$1")); @@ -160,6 +266,7 @@ protected static int parseInt(String data, String key) { * @param key the key to search for, ie `raw` * @return The parsed string. */ + @Deprecated(forRemoval = true) protected static String parseString(String data, String key) { return data.replaceAll(".*" + key + "=`(.*?)`.*", "$1"); } @@ -171,6 +278,7 @@ protected static String parseString(String data, String key) { * @param key the key to search for, ie `raw` * @return The parsed string. */ + @Deprecated(forRemoval = true) protected static String parseArray(String data, String key) { return data.replaceAll(".*" + key + "=(\\[.*?]).*", "$1"); } @@ -182,6 +290,7 @@ protected static String parseArray(String data, String key) { * @param key the key to search for, ie `asConsole` * @return The parsed boolean. */ + @Deprecated(forRemoval = true) protected static boolean parseBoolean(String data, String key) { return Boolean.parseBoolean(data.replaceAll(".*" + key + "=(true|false).*", "$1")); } @@ -193,6 +302,7 @@ protected static boolean parseBoolean(String data, String key) { * @param key the key to search for, ie `yaw` * @return The parsed float. */ + @Deprecated(forRemoval = true) protected static float parseFloat(String data, String key) { return Float.parseFloat(data.replaceAll(".*" + key + "=(-?\\d+\\.\\d+).*", "$1")); } @@ -204,143 +314,13 @@ protected static float parseFloat(String data, String key) { * @param key the key to search for, ie `x` * @return The parsed double. */ + @Deprecated(forRemoval = true) protected static double parseDouble(String data, String key) { return Double.parseDouble(data.replaceAll(".*" + key + "=(-?\\d+\\.\\d+).*", "$1")); } - public boolean isOnCooldown(UUID uuid) { - if (!cooldowns.containsKey(uuid)) return false; - Instant i = cooldowns.get(uuid); - if (i.isBefore(Instant.now())) { - cooldowns.remove(uuid); - return false; - } - return true; - } - - public void activateCooldown(UUID uuid) { - if (cooldown == 0) return; - cooldowns.put(uuid, Instant.now().plusMillis(50L * cooldown)); - } - - /** - * A convenience method to add a condition to the action - * - * @param condition the condition to add - */ - public void addCondition(Condition condition) { - conditions.add(condition); - } - - public void removeCondition(Condition condition) { - conditions.remove(condition); - } - - /** - * Contains the execution of the action - * - * @param npc The NPC - * @param menu The menu - * @param player The player - */ - public abstract void perform(InternalNpc npc, Menu menu, Player player); - - /** - * Serializes the action to a string - */ - public abstract String serialize(); - - public abstract ItemStack getFavicon(Player player); - - @Override - public String toString() { - return serialize(); - } - - public abstract Menu getMenu(); - - /** - * Returns if the action should be processed. This takes the cooldown into account. - * - * @param player the player - * @return if the action should be processed - */ - public boolean processConditions(Player player) { - if (isOnCooldown(player.getUniqueId())) return false; - if (conditions == null || conditions.isEmpty()) return true; // no conditions - Set results = new HashSet<>(conditions.size()); - conditions.forEach(conditional -> results.add(conditional.compute(player))); - return (mode == Condition.SelectionMode.ALL ? !results.contains(false) : results.contains(true)); - } - - private String getConditionSerialized() { - return CustomNPCs.getGson().toJson(conditions, Utils.CONDITIONS_LIST); - } - - /** - * Generates the serialized string for storing Actions. - *

- * This correctly serializes `int`, `double`, `float`, `boolean`, {@link String}, {@link Enum<>} - * - * @param id The id of the action ID{params...} - * @param params The parameters of the action. **DON'T INCLUDE THE DELAY, CONDITIONS, OR SELECTION MODE** - * @return The serialized strings - */ - protected String generateSerializedString(String id, Map params) { - Map base = new HashMap<>(); // get around Map.of() immutability - base.put("delay", delay); - base.put("mode", mode); - base.put("conditions", getConditionSerialized()); - base.put("cooldown", cooldown); - - StringBuilder builder = new StringBuilder(id); - builder.append("{"); - - for (Map.Entry entry : params.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - - builder.append(key).append("="); - if (value instanceof String) { - builder.append("`").append(value).append("`"); - } else { - builder.append(value); - } - builder.append(", "); - } - - for (Map.Entry entry : base.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - - builder.append(key).append("="); - if (value instanceof String) { - builder.append("`").append(value).append("`"); - } else { - builder.append(value); - } - builder.append(", "); - } - - builder.deleteCharAt(builder.length() - 1); // delete the last comma - builder.deleteCharAt(builder.length() - 1); // delete the last comma - - - builder.append("}"); - - return builder.toString(); - } - - public abstract Action clone(); + @Deprecated + protected record ParseResult(int delay, Selector mode, List conditions, int cooldown) { - protected record ParseResult(int delay, Condition.SelectionMode mode, List conditions, int cooldown) { - /** - * @deprecated Use {@link ParseResult#ParseResult(int, Condition.SelectionMode, List, int)}} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public ParseResult(int delay, Condition.SelectionMode mode, List conditions) { - this(delay, mode, conditions, 0); - } } } diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/ActionType.java b/core/src/main/java/dev/foxikle/customnpcs/actions/ActionType.java deleted file mode 100644 index 749547b2..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/ActionType.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2024-2026. Foxikle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.foxikle.customnpcs.actions; - -import dev.foxikle.customnpcs.actions.defaultImpl.*; -import lombok.Getter; -import org.jetbrains.annotations.ApiStatus; - -/** - * All supported types of actions - *

- * Scheduled for removal in 1.9.0, meaning configurations prior to 1.7 will - * need to update to 1.7.x-1.8.x prior to updating to 1.9.0 - * - * @deprecated - */ -@Getter -@Deprecated -@ApiStatus.ScheduledForRemoval(inVersion = "1.9.0") -public enum ActionType { - - /** - * Represents running a command. The argument syntax is as follows: - * label, args... - */ - RUN_COMMAND(true, true, true, RunCommand.class), - - /** - * Represents sending the player a message. The argument syntax is as follows: - * message... - */ - SEND_MESSAGE(true, true, true, SendMessage.class), - - /** - * Represents sending the player a title. The argument syntax is as follows: - * fade_in, stay, fade_out, title... - */ - DISPLAY_TITLE(true, true, true, DisplayTitle.class), - - /** - * Represents sending the player an action bar. The argument syntax is as follows: - * message... - */ - ACTION_BAR(true, true, true, ActionBar.class), - - /** - * Represents starting/stopping following the player. There are no arguments. - * @apiNote This action was removed in 1.7-pre3 - */ - @Deprecated - TOGGLE_FOLLOWING(false, false, false, null), - - /** - * Represents playing a sound for the player. The argument syntax is as follows: - * pitch, volume, sound_name - */ - PLAY_SOUND(true, true, true, PlaySound.class), - - /** - * Represents teleporting the player. The argument syntax is as follows: - * x, y, z, pitch, yaw - */ - TELEPORT(true, true, true, Teleport.class), - - /** - * Represents sending the player to a BungeeCord server. The argument syntax is as follows: - * Case_sensitive_Server_Name - */ - SEND_TO_SERVER(true, true, true, SendServer.class), - - /** - * Represents giving an effect to the player. The argument syntax is as follows: - * duration, strength, hide_particles, effect - */ - ADD_EFFECT(true, true, true, GiveEffect.class), - - /** - * Represents removing an effect to the player. The argument syntax is as follows: - * effect - */ - REMOVE_EFFECT(true, true, true, RemoveEffect.class), - - /** - * Represents giving a xp to the player. The argument syntax is as follows: - * amount, levels - */ - GIVE_EXP(true, true, true, GiveXP.class), - - /** - * Represents removing a xp to the player. The argument syntax is as follows: - * amount, levels - */ - REMOVE_EXP(true, true, true, RemoveXP.class); - - private final boolean editable; - private final boolean duplicatable; - private final boolean delayable; - private final Class actionClass; - - ActionType(boolean editable, boolean canDuplicate, boolean canDelay, Class actionClass) { - this.editable = editable; - this.duplicatable = canDuplicate; - this.delayable = canDelay; - this.actionClass = actionClass; - } -} diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java b/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java deleted file mode 100644 index 6ba6c9de..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (c) 2024-2026. Foxikle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.foxikle.customnpcs.actions; - -import dev.foxikle.customnpcs.actions.conditions.Condition; -import dev.foxikle.customnpcs.actions.defaultImpl.*; -import dev.foxikle.customnpcs.internal.CustomNPCs; -import lombok.Getter; -import lombok.Setter; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; - -/** - * The object to represent what should be done after an NPC interaction - * - * @deprecated - */ -@Getter -@Deprecated -@ApiStatus.ScheduledForRemoval(inVersion = "1.9.0") -public class LegacyAction { - - - private final ActionType actionType; - private final List args; - private final List conditionals; - @Setter - private int delay; - @Setter - private Condition.SelectionMode mode; - - /** - *

Creates a new Action - *

- * - * @param actionType The type of action to be performed - * @param args The arguments for the Action - * @param delay The amount of ticks to delay an action - * @param matchAll If all the conditions must be met, or one - * @param conditionals The conditions to apply to this action - */ - public LegacyAction(ActionType actionType, List args, int delay, Condition.SelectionMode matchAll, List conditionals) { - this.actionType = actionType; - this.args = args; - this.delay = delay; - this.mode = matchAll; - this.conditionals = conditionals; - } - - private LegacyAction(String subCommand, ArrayList args, int delay) { - this.actionType = ActionType.valueOf(subCommand); - this.args = args; - this.delay = delay; - this.mode = Condition.SelectionMode.ONE; - this.conditionals = new ArrayList<>(); - } - - /** - *

Gets the action from a serialized string - *

- * - * @param string The string to deserialize. - * @return the action that was serialized. - * @throws NumberFormatException If the string was formatted improperly - * @throws ArrayIndexOutOfBoundsException if the action was formatted improperly - */ - public static LegacyAction of(String string) throws NumberFormatException, ArrayIndexOutOfBoundsException { - if (string.contains("%::%")) { - ArrayList split = new ArrayList<>(Arrays.stream(string.split("%::%")).toList()); - String sub = split.get(0); - split.remove(0); - int delay = Integer.parseInt(split.get(0)); - split.remove(0); - return new LegacyAction(sub, split, delay); // doesn't support conditionals - } else { - return CustomNPCs.getGson().fromJson(string, LegacyAction.class); - } - } - - /** - *

Gets a copy of the arguments of an action - *

- * - * @return A copy of the list of arguments for the actions - */ - public List getArgsCopy() { - return new ArrayList<>(args); - } - - /** - * Adds a condition to the action - * - * @param conditional the conditional to add - * @return if the conditional was successfully added - */ - @SuppressWarnings("UnusedReturnValue") - public boolean addConditional(Condition conditional) { - return conditionals.add(conditional); - } - - /** - * Removes a condition from the action - * - * @param conditional conditional to remove - * @return if the condition was successfully removed - */ - @SuppressWarnings("UnusedReturnValue") - public boolean removeConditional(Condition conditional) { - return conditionals.remove(conditional); - } - - /** - * Gets the command that is run to initiate the action. - * - * @param player the action is targeted at - * @return String representing the command. The arguments are: player's uuid, sub command, delay (in ticks), command specific arguments - */ - public String getCommand(@NotNull Player player) { - if (processConditions(player)) { - return "npcaction " + player.getUniqueId() + " " + actionType.name() + " " + delay + " " + String.join(" ", args); - } else { - return "npcaction"; - } - } - - private boolean processConditions(Player player) { - if (conditionals == null || conditionals.isEmpty()) return true; // no conditions - - Set results = new HashSet<>(2); - conditionals.forEach(conditional -> results.add(conditional.compute(player))); - return (mode == Condition.SelectionMode.ALL ? !results.contains(false) : results.contains(true)); - } - - /** - *

Gets the json equivalent of this action - *

- * - * @return the serialized version of the action (in json) - */ - public String toJson() { - return CustomNPCs.getGson().toJson(this); - } - - @Override - @SuppressWarnings("all") - public LegacyAction clone() { - return new LegacyAction(actionType, args, delay, mode, conditionals); - } - - - /** - * A method that converts legacy actions to the new system - * - * @return - */ - @Nullable - public Action toAction() { - return switch (actionType) { - case ACTION_BAR -> new ActionBar(String.join(" ", args), delay, mode, conditionals, 0); - case SEND_MESSAGE -> new SendMessage(String.join(" ", args), delay, mode, conditionals, 0); - case DISPLAY_TITLE -> - new DisplayTitle(String.join(" ", args.subList(3, args.size() - 1)), "", Integer.parseInt(args.get(0)), Integer.parseInt(args.get(1)), Integer.parseInt(args.get(2)), delay, mode, conditionals, 0); - case RUN_COMMAND -> new RunCommand(String.join(" ", args), false, delay, mode, conditionals, 0); - case TELEPORT -> - new Teleport(Double.parseDouble(args.get(0)), Double.parseDouble(args.get(1)), Double.parseDouble(args.get(2)), Float.parseFloat(args.get(3)), Float.parseFloat(args.get(4)), delay, mode, conditionals, 0); - case GIVE_EXP -> - new GiveXP(Integer.parseInt(args.get(0)), Boolean.parseBoolean(args.get(1)), delay, mode, conditionals, 0); - case ADD_EFFECT -> - new GiveEffect(args.get(3), Integer.parseInt(args.get(0)), Integer.parseInt(args.get(1)), Boolean.parseBoolean(args.get(2)), delay, mode, conditionals, 0); - case PLAY_SOUND -> - new PlaySound(args.get(2), Float.parseFloat(args.get(1)), Float.parseFloat(args.get(0)), delay, mode, conditionals, 0); - case REMOVE_EXP -> - new RemoveXP(Integer.parseInt(args.get(0)), Boolean.parseBoolean(args.get(1)), delay, mode, conditionals, 0); - case REMOVE_EFFECT -> new RemoveEffect(args.get(0), delay, mode, conditionals, 0); - case SEND_TO_SERVER -> new SendServer(String.join(" ", args), delay, mode, conditionals, 0); - case TOGGLE_FOLLOWING -> throw new IllegalArgumentException("Toggle following is no longer supported"); - }; - } -} diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ActionAdapter.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ActionAdapter.java deleted file mode 100644 index 296c0d20..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ActionAdapter.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) 2024-2026. Foxikle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.foxikle.customnpcs.actions.conditions; - -import com.google.gson.JsonSyntaxException; -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; -import dev.foxikle.customnpcs.actions.ActionType; -import dev.foxikle.customnpcs.actions.LegacyAction; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * A TypeAdapter for Gson - */ -public class ActionAdapter extends TypeAdapter { - - /** - * Serializes the Action object to json - * @param out The json writer - * @param value the Java object to write. May be null. - * @throws IOException if an IOException occurs - */ - @Override - public void write(JsonWriter out, LegacyAction value) throws IOException { - out.beginObject(); - out.name("actionType").value(value.getActionType().toString()); - - out.name("args"); - out.beginArray(); - for (String s : value.getArgs()) { - out.value(s); - } - out.endArray(); - - out.name("delay").value(value.getDelay()); - out.name("mode").value(value.getMode().name()); - - out.name("conditionals"); - out.beginArray(); - for (Condition c : value.getConditionals()) { - out.value(c.toJson()); - } - out.endArray(); - out.endObject(); - out.close(); - } - - - /** - * deserializes an Action - * @param in the object, in reader form - * @return the deserialized action - * @throws IOException if an error occurred - */ - @Override - public LegacyAction read(JsonReader in) throws IOException { - in.beginObject(); - ActionType actionType = null; - List args = new ArrayList<>(); - int delay = 0; - List conditions = new ArrayList<>(); - Condition.SelectionMode selectionMode = null; - - - while (in.hasNext()) { - String name = in.nextName(); - switch (name) { - case "actionType", "subCommand" -> actionType = ActionType.valueOf(in.nextString()); - case "args" -> { - in.beginArray(); - while(in.hasNext()) { - args.add(in.nextString()); - } - in.endArray(); - } - case "delay" -> delay = in.nextInt(); - case "mode" -> selectionMode = Condition.SelectionMode.valueOf(in.nextString()); - case "conditionals" -> { - in.beginArray(); - while(in.hasNext()) { - try { - // if its stored as a string - conditions.add(Condition.of(in.nextString())); - } catch (JsonSyntaxException | IllegalStateException ignored) { - // or as an object - ConditionalTypeAdapter conditionalTypeAdapter = new ConditionalTypeAdapter(); - conditions.add(conditionalTypeAdapter.read(in)); - } - } - in.endArray(); - } - default -> in.skipValue(); // Ignore unknown properties - } - } - - in.endObject(); - - return new LegacyAction(actionType, args, delay, selectionMode, conditions); - } -} diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Conditional.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Conditional.java deleted file mode 100644 index 478bcede..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Conditional.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (c) 2024-2026. Foxikle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.foxikle.customnpcs.actions.conditions; - -import dev.foxikle.customnpcs.internal.CustomNPCs; -import lombok.Getter; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.ApiStatus; - -/** - * The interface to represent a comparison - * @deprecated See {@link Condition} - */ -@Deprecated -@ApiStatus.ScheduledForRemoval(inVersion = "1.8.0") -public interface Conditional { - - /** - * Computes the condition to determine if the action should be executed - * @param player The player to fetch data from - * @return if the action should be executed - */ - boolean compute(Player player); - - /** - * Serializes the condition to json using Gson - * @return the serialized condition - */ - String toJson(); - - /** - * - * @param data the serialized condition - * @return the condition from the json - */ - static Conditional of(String data) { - return CustomNPCs.getGson().fromJson(data, Conditional.class); - } - - /** - * Gets the value the condition is comparing - * @see Value - * @return the value the condition is comparing - */ - Value getValue(); - - /** - * Gets the comparator the condition uses to compare the value and target value. - * @return the comparator - * @see Comparator - */ - Comparator getComparator(); - - /** - * Gets the type of condition - * @return the condition type - * @see Type - */ - Type getType(); - - /** - * Sets the comparator of this condition - * @param comparator the comparator to compare the value and target value - * @see Comparator - */ - void setComparator(Comparator comparator); - - /** - * Sets the value of this condition - * @param value the value to compare - * @see Value - */ - void setValue(Value value); - - /** - * Sets the target value of this condition - * @param targetValue the target value - */ - void setTargetValue(String targetValue); - - /** - * Gets the target of the condition - * @return returns the target value - */ - String getTarget(); - - /** - * Clones this conditional object - * @return the cloned object - */ - Conditional clone(); - - /** - * A list of comparators used to compare the values and target values of conditions - */ - @Getter - enum Comparator { - /** - * Represents the value being equal to the target value - */ - EQUAL_TO(true), - - /** - * Represents the value being unequal to the target value - */ - NOT_EQUAL_TO(true), - - /** - * Represents the value being less than the target value - */ - LESS_THAN(false), - - /** - * Represents the value being greater than the target value - */ - GREATER_THAN(false), - - /** - * Represents the value being less than or equal to the target value - */ - LESS_THAN_OR_EQUAL_TO(false), - - /** - * Represents the value being greater than or equal to the target value - */ - GREATER_THAN_OR_EQUAL_TO(false); - - private final boolean strictlyLogical; - - /** - * Constructor for the Comparator - * @param strictlyLogical if the comparator is to only be used on logical parameters - */ - Comparator(boolean strictlyLogical) { - this.strictlyLogical = strictlyLogical; - } - - } - - /** - * A list of comparator types - */ - enum Type { - /** - * Represents a comparison between a Value and a target value that can be any numeric value. - * @see Value - */ - NUMERIC, - - /** - * Represents a comparison between a Value and a target value with a finite number of possibilities - * @see Value - */ - LOGICAL - } - - /** - * A list of values the plugin can compare - */ - enum Value { - // numeric - /** - * Represents the player's experience levels - */ - EXP_LEVELS(false), - - /** - * Represents the player's experience points - */ - EXP_POINTS(false), - - /** - * Represents the player's health - */ - HEALTH(false), - - /** - * Represents the player's absorption - * @deprecated - Misspelled, see {@link Conditional.Value#ABSORPTION} - */ - @ApiStatus.ScheduledForRemoval(inVersion = "1.7") - @Deprecated - ABSORBTION(false), - - /** - * Represents the player's absorption - */ - ABSORPTION(false), - - /** - * Represents the player's Y coordinate - */ - Y_COORD(false), - - /** - * Represents the player's X coordinate - */ - X_COORD(false), - - /** - * Represents the player's Z coordinate - */ - Z_COORD(false), - - - // logical - /** - * Represents if the player has an effect - */ - HAS_EFFECT(true), - - /** - * Represents if the player has a permission node - */ - HAS_PERMISSION(true), - - /** - * Represents if the player is in the gamemode - */ - GAMEMODE(true), - - /** - * Represents if the player is flying - */ - IS_FLYING(true), - - /** - * Represents if the player is sprinting - */ - IS_SPRINTING(true), - - /** - * Represents if the player is sneaking - */ - IS_SNEAKING(true), - - /** - * Represents if the player is frozen - */ - IS_FROZEN(true), - - /** - * Represents if the player is gliding - */ - IS_GLIDING(true); - - - - private final boolean isLogical; - - /** - * The constructor for the Value - * @param isLogical if the value is considered 'logical' - */ - Value(boolean isLogical) { - this.isLogical = isLogical; - } - - /** - * Determines if the value is considered 'logical' - * @return if the value is logical - */ - public boolean isLogical() { - return isLogical; - } - } - - /** - * Represents if how the conditions should be computed - */ - enum SelectionMode { - /** - * If ALL the conditions must be true for the action to be executed - */ - ALL, - - /** - * if at least ONE of the conditions must be met for the action to be executed - */ - ONE - } -} diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalConditional.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalConditional.java deleted file mode 100644 index 57e906a2..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalConditional.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 2024-2026. Foxikle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.foxikle.customnpcs.actions.conditions; - -import dev.foxikle.customnpcs.internal.CustomNPCs; -import org.bukkit.GameMode; -import org.bukkit.entity.Player; -import org.bukkit.potion.PotionEffectType; -import org.jetbrains.annotations.ApiStatus; - -import java.util.Objects; - -/** - * The object representing two non-numeric values - * @deprecated See {@link LogicalCondition} - */ -@Deprecated -@ApiStatus.ScheduledForRemoval(inVersion = "1.8.0") -public class LogicalConditional implements Condition { - private final Type type = Type.LOGICAL; - private Comparator comparator; - private Value value; - private String target; - - /** - * - * @param comparator the comparator to use - * @param value the value to compare - * @param target the target to compare to - * @see Value - * @see Comparator - */ - public LogicalConditional(Comparator comparator, Value value, String target) { - this.comparator = comparator; - this.value = value; - this.target = target; - } - - /** - * Computes the condition to determine if the action should be executed - * @param player The player to fetch data from - * @return if the action should be executed - */ - @Override - public boolean compute(Player player) { - boolean value = false; - switch (this.value) { - case HAS_PERMISSION -> value = player.hasPermission(target); - case HAS_EFFECT -> value = player.hasPotionEffect(Objects.requireNonNull(PotionEffectType.getByName(target))); - case GAMEMODE -> value = player.getGameMode().equals(GameMode.valueOf(target)); - case IS_FLYING -> value = player.isFlying(); - case IS_SPRINTING -> value = player.isSprinting(); - case IS_SNEAKING -> value = player.isSneaking(); - case IS_FROZEN -> value = player.isFrozen(); - case IS_GLIDING -> value = player.isGliding(); - } - switch (comparator) { - case EQUAL_TO -> { - return value; - } - case NOT_EQUAL_TO -> { - return !value; - } - } - return false; - } - - /** - * Serializes the condition to json using Gson - * @return the serialized condition - */ - @Override - public String toJson(){ - return CustomNPCs.getGson().toJson(this); - } - - /** - * - * @param data the serialized condition - * @return the condition from the json - */ - public static LogicalConditional of(String data) { - return CustomNPCs.getGson().fromJson(data, LogicalConditional.class); - } - - /** - * Gets the type of condition - * @return the condition type - * @see Type - */ - @Override - public Type getType() { - return type; - } - - /** - * Sets the comparator of this condition - * @param comparator the comparator to compare the value and target value - * @see Comparator - */ - @Override - public void setComparator(Comparator comparator) { - this.comparator = comparator; - } - - /** - * Sets the value of this condition - * @param value the value to compare - * @see Value - */ - @Override - public void setValue(Value value) { - this.value = value; - } - - /** - * Sets the target value of this condition - * @param targetValue the target value - */ - @Override - public void setTargetValue(String targetValue) { - this.target = targetValue; - } - - /** - * Gets the value the condition is comparing - * @see Value - * @return the value the condition is comparing - */ - @Override - public Value getValue() { - return this.value; - } - - /** - * Gets the target of the condition - * @return returns the target value - */ - @Override - public String getTarget() { - return target; - } - - @Override - public Condition clone() { - try { - return (LogicalConditional) super.clone(); - } catch (CloneNotSupportedException e) { - return new LogicalConditional(comparator, value, target); - } - } - - /** - * Gets the comparator the condition uses to compare the value and target value. - * @return the comparator - * @see Comparator - */ - @Override - public Comparator getComparator() { - return this.comparator; - } -} diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericConditional.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericConditional.java deleted file mode 100644 index 91453f40..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericConditional.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) 2024-2026. Foxikle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.foxikle.customnpcs.actions.conditions; - -import dev.foxikle.customnpcs.internal.CustomNPCs; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.ApiStatus; - -/** - * The object representing a comparison of two numeric values - * @deprecated See {@link NumericCondition} - */ -@Deprecated -@ApiStatus.ScheduledForRemoval(inVersion = "1.8.0") -public class NumericConditional implements Condition { - - private final Type type = Type.NUMERIC; - private Comparator comparator; - private Value value; - private double target; - - /** - * - * @param comparator the comparator to use - * @param value the value to compare - * @param target the target to compare to - * @see Value - * @see Comparator - */ - public NumericConditional(Comparator comparator, Value value, double target) { - this.comparator = comparator; - this.value = value; - this.target = target; - } - - /** - * Computes the condition to determine if the action should be executed - * @param player The player to fetch data from - * @return if the action should be executed - */ - @Override - public boolean compute(Player player) { - double value = 0; - switch (this.value) { - case X_COORD -> value = player.getLocation().x(); - case Y_COORD -> value = player.getLocation().y(); - case Z_COORD -> value = player.getLocation().z(); - case EXP_LEVELS -> value = player.getLevel(); - case EXP_POINTS -> value = player.getExp(); - } - switch (comparator) { - case EQUAL_TO -> { - return value == target; - } - case NOT_EQUAL_TO -> { - return value != target; - } - case LESS_THAN -> { - return value < target; - } - case LESS_THAN_OR_EQUAL_TO -> { - return value <= target; - } - case GREATER_THAN -> { - return value > target; - } - case GREATER_THAN_OR_EQUAL_TO -> { - return value >= target; - } - } - return false; - } - - /** - * Serializes the condition to json using Gson - * @return the serialized condition - */ - @Override - public String toJson(){ - return CustomNPCs.getGson().toJson(this); - } - - /** - * - * @param data the serialized condition - * @return the condition from the json - */ - public static NumericConditional of(String data) { - return CustomNPCs.getGson().fromJson(data, NumericConditional.class); - } - - /** - * Gets the type of condition - * @return the condition type - * @see Type - */ - @Override - public Type getType() { - return type; - } - - /** - * Sets the comparator of this condition - * @param comparator the comparator to compare the value and target value - * @see Comparator - */ - @Override - public void setComparator(Comparator comparator) { - this.comparator = comparator; - } - - /** - * Sets the value of this condition - * @param value the value to compare - * @see Value - */ - @Override - public void setValue(Value value) { - this.value = value; - } - - /** - * Sets the target value of this condition - * @param targetValue the target value - */ - @Override - public void setTargetValue(String targetValue) { - this.target = Double.parseDouble(targetValue); - } - - /** - * Gets the value the condition is comparing - * @see Value - * @return the value the condition is comparing - */ - @Override - public Value getValue() { - return this.value; - } - - /** - * Gets the target of the condition - * @return returns the target value - */ - @Override - public String getTarget() { - return String.valueOf(target); - } - - /** - * Gets the comparator the condition uses to compare the value and target value. - * @return the comparator - * @see Comparator - */ - @Override - public Comparator getComparator() { - return this.comparator; - } - - @Override - public Condition clone() { - try { - return (NumericConditional) super.clone(); - } catch (CloneNotSupportedException e) { - return new NumericConditional(comparator, value, target); - } - } -} diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/ActionBar.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/ActionBar.java index 79e02242..be754b1a 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/ActionBar.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/ActionBar.java @@ -23,7 +23,8 @@ package dev.foxikle.customnpcs.actions.defaultImpl; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuUtils; @@ -40,51 +41,49 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import me.clip.placeholderapi.PlaceholderAPI; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -import java.util.Map; import static org.bukkit.Material.IRON_INGOT; import static org.bukkit.Material.PAPER; @Getter @Setter +@NoArgsConstructor public class ActionBar extends Action { - private String rawMessage; + public static final StructCodec CODEC = StructCodec.struct( + "raw", Codec.STRING, ActionBar::getRawMessage, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + ActionBar::new + ); - /** - * Creates a new SendMessage with the specified message - * - * @param rawMessage The raw message - * @deprecated Use {@link ActionBar#ActionBar(String, int, Condition.SelectionMode, List, int)} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public ActionBar(String rawMessage, int delay, Condition.SelectionMode mode, List conditionals) { - super(delay, mode, conditionals, 0); - this.rawMessage = rawMessage; - } + private String rawMessage; /** * Creates a new SendMessage with the specified message * * @param rawMessage The raw message */ - public ActionBar(String rawMessage, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { + public ActionBar(String rawMessage, int delay, Selector mode, List conditionals, int cooldown) { super(delay, mode, conditionals, cooldown); this.rawMessage = rawMessage; } - public static Button creationButton(Player player) { + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(IRON_INGOT) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.actionbar")) .setLore(Msg.lore(player.locale(), "customnpcs.favicons.actionbar.description")) @@ -92,24 +91,12 @@ public static Button creationButton(Player player) { ButtonClickAction.plain((menuView, event) -> { event.setCancelled(true); Player p = (Player) event.getWhoClicked(); - ActionBar actionImpl = new ActionBar("", 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0); + ActionBar actionImpl = new ActionBar("", 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); } - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(ActionBar.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + ActionBar.class.getName()); - } - String rawMessage = parseString(serialized, "raw"); - ParseResult pr = parseBase(serialized); - - ActionBar message = new ActionBar(rawMessage, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - - return clazz.cast(message); - } - @Override public Menu getMenu() { return new ActionbarCustomizer(this); @@ -121,7 +108,10 @@ public ItemStack getFavicon(Player player) { .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.actionbar")) .setLore(Msg.translate(player.locale(), "customnpcs.favicons.delay", getDelay()), Msg.translate(player.locale(), "customnpcs.favicons.preview", Msg.format(getRawMessage())), - Msg.format(getRawMessage().isEmpty() ? "" + Msg.translatedString(player.locale(), "customnpcs.messages.empty_string") : getRawMessage()), + Msg.format(getRawMessage().isEmpty() ? + "" + Msg.translatedString(player.locale(), "customnpcs.messages" + + ".empty_string") : + getRawMessage()), Msg.format(""), Msg.translate(player.locale(), "customnpcs.favicons.edit"), Msg.translate(player.locale(), "customnpcs.favicons.remove") @@ -138,18 +128,36 @@ public ItemStack getFavicon(Player player) { @Override public void perform(InternalNpc npc, Menu menu, Player player) { if (!processConditions(player)) return; - player.sendActionBar(Msg.format(CustomNPCs.getInstance().papi ? PlaceholderAPI.setPlaceholders(player, rawMessage) : rawMessage)); + player.sendActionBar(Msg.format(CustomNPCs.getInstance().papi ? PlaceholderAPI.setPlaceholders(player, + rawMessage) : rawMessage)); activateCooldown(player.getUniqueId()); } + public StructCodec getCodec() { + return CODEC; + } + @Override - public String serialize() { - return generateSerializedString("ActionBar", Map.of("raw", rawMessage)); + public String getId() { + return "ActionBar"; } @Override public Action clone() { - return new ActionBar(rawMessage, getDelay(), getMode(), new ArrayList<>(getConditions()), getCooldown()); + return new ActionBar(rawMessage, getDelay(), getSelector(), new ArrayList<>(getConditions()), getCooldown()); + } + + @Deprecated(forRemoval = true) + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(ActionBar.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + ActionBar.class.getName()); + } + String rawMessage = parseString(serialized, "raw"); + ParseResult pr = parseBase(serialized); + + ActionBar message = new ActionBar(rawMessage, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); + + return clazz.cast(message); } public class ActionbarCustomizer implements Menu { @@ -179,10 +187,13 @@ public String getName() { public @NotNull Content getContent(DataRegistry dataRegistry, Player player, Capacity capacity) { return MenuUtils.actionBase(actionBar, player) .setButton(22, Button.clickable(ItemBuilder.modern(PAPER) - .setDisplay(Msg.translate(player.locale(), getRawMessage().isEmpty() ? "" + Msg.translatedString(player.locale(), "customnpcs.messages.empty_string") : getRawMessage())) + .setDisplay(Msg.translate(player.locale(), getRawMessage().isEmpty() ? + "" + Msg.translatedString(player.locale(), "customnpcs" + + ".messages" + + ".empty_string") : getRawMessage())) .setLore(Msg.translate(player.locale(), "customnpcs.items.click_to_change")) .build(), - ButtonClickAction.plain((menuView, event) -> { + ButtonClickAction.plain((_, event) -> { event.setCancelled(true); player.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); CustomNPCs plugin = CustomNPCs.getInstance(); diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/DisplayTitle.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/DisplayTitle.java index 71cd8ea3..7c56744e 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/DisplayTitle.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/DisplayTitle.java @@ -23,7 +23,8 @@ package dev.foxikle.customnpcs.actions.defaultImpl; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuItems; @@ -42,28 +43,42 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import me.clip.placeholderapi.PlaceholderAPI; import net.kyori.adventure.text.Component; import net.kyori.adventure.title.Title; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.time.Duration; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import static org.bukkit.Material.*; @Getter @Setter +@NoArgsConstructor public class DisplayTitle extends Action { + public static final StructCodec CODEC = StructCodec.struct( + "title", Codec.STRING, DisplayTitle::getTitle, + "subtitle", Codec.STRING, DisplayTitle::getSubTitle, + "fade_in", Codec.INT, DisplayTitle::getFadeIn, + "stay", Codec.INT, DisplayTitle::getStay, + "fade_out", Codec.INT, DisplayTitle::getFadeOut, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + DisplayTitle::new + ); + private String title; private String subTitle; private int fadeIn; @@ -75,8 +90,9 @@ public class DisplayTitle extends Action { * * @param title The raw message */ - public DisplayTitle(String title, String subTitle, int fadeIn, int stay, int fadeOut, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { - super(delay, mode, conditionals, cooldown); + public DisplayTitle(String title, String subTitle, int fadeIn, int stay, int fadeOut, int delay, Selector mode, + List conditions, int cooldown) { + super(delay, mode, conditions, cooldown); this.title = title; this.subTitle = subTitle; this.fadeIn = fadeIn; @@ -84,24 +100,8 @@ public DisplayTitle(String title, String subTitle, int fadeIn, int stay, int fad this.fadeOut = fadeOut; } - /** - * Creates a new SendMessage with the specified message - * - * @param title The raw message - * @deprecated Use {@link DisplayTitle#DisplayTitle(String, String, int, int, int, int, Condition.SelectionMode, List, int)}} - */ - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - @Deprecated - public DisplayTitle(String title, String subTitle, int fadeIn, int stay, int fadeOut, int delay, Condition.SelectionMode mode, List conditionals) { - super(delay, mode, conditionals, 0); - this.title = title; - this.subTitle = subTitle; - this.fadeIn = fadeIn; - this.stay = stay; - this.fadeOut = fadeOut; - } - public static Button creationButton(Player player) { + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(OAK_SIGN) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.title")) .setLore(Msg.lore(player.locale(), "customnpcs.favicons.title.description")) @@ -110,30 +110,12 @@ public static Button creationButton(Player player) { event.setCancelled(true); Player p = (Player) event.getWhoClicked(); p.playSound(p, Sound.UI_BUTTON_CLICK, 1, 1); - DisplayTitle actionImpl = new DisplayTitle("Title", "Subtitle", 10, 10, 10, 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0); + DisplayTitle actionImpl = new DisplayTitle("Title", "Subtitle", 10, 10, 10, 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); } - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(DisplayTitle.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + DisplayTitle.class.getName()); - } - - String title = parseString(serialized, "title"); - String subTitle = parseString(serialized, "subTitle"); - int in = parseInt(serialized, "in"); - int stay = parseInt(serialized, "stay"); - int out = parseInt(serialized, "out"); - - ParseResult pr = parseBase(serialized); - - DisplayTitle message = new DisplayTitle(title, subTitle, in, stay, out, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - - return clazz.cast(message); - } - @Override public ItemStack getFavicon(Player player) { return ItemBuilder.modern(OAK_SIGN).setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.title")) @@ -158,13 +140,6 @@ public Menu getMenu() { return new DisplayTitleCustomizer(this); } - /** - * Sends a message to the player - * - * @param npc The NPC - * @param menu The menu - * @param player The player - */ @Override public void perform(InternalNpc npc, Menu menu, Player player) { if (!processConditions(player)) return; @@ -177,20 +152,38 @@ public void perform(InternalNpc npc, Menu menu, Player player) { } @Override - public String serialize() { - - Map params = new HashMap<>(); - params.put("title", title); - params.put("subTitle", subTitle); - params.put("in", fadeIn); - params.put("stay", stay); - params.put("out", fadeOut); + public StructCodec getCodec() { + return CODEC; + } - return generateSerializedString("DisplayTitle", params); + @Override + public String getId() { + return "DisplayTitle"; } public Action clone() { - return new DisplayTitle(title, subTitle, fadeIn, stay, fadeOut, getDelay(), getMode(), new ArrayList<>(getConditions()), getCooldown()); + return new DisplayTitle(title, subTitle, fadeIn, stay, fadeOut, getDelay(), getSelector(), + new ArrayList<>(getConditions()), getCooldown()); + } + + @Deprecated(forRemoval = true) + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(DisplayTitle.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + DisplayTitle.class.getName()); + } + + String title = parseString(serialized, "title"); + String subTitle = parseString(serialized, "subTitle"); + int in = parseInt(serialized, "in"); + int stay = parseInt(serialized, "stay"); + int out = parseInt(serialized, "out"); + + ParseResult pr = parseBase(serialized); + + DisplayTitle message = new DisplayTitle(title, subTitle, in, stay, out, pr.delay(), pr.mode(), + pr.conditions(), pr.cooldown()); + + return clazz.cast(message); } public static class DisplayTitleCustomizer implements Menu { diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/FollowPresetPathAction.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/FollowPresetPath.java similarity index 73% rename from core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/FollowPresetPathAction.java rename to core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/FollowPresetPath.java index 797e7129..075d4c2e 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/FollowPresetPathAction.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/FollowPresetPath.java @@ -22,9 +22,10 @@ package dev.foxikle.customnpcs.actions.defaultImpl; -import com.google.gson.reflect.TypeToken; +import com.google.common.reflect.TypeToken; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuUtils; @@ -41,22 +42,39 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.*; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.scheduler.BukkitTask; -import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -public class FollowPresetPathAction extends Action { +@NoArgsConstructor +public class FollowPresetPath extends Action { + public static final StructCodec CODEC = StructCodec.struct( + "nodes", RecordedPathNode.CODEC.list(), FollowPresetPath::getPath, + "loop", Codec.BOOLEAN, FollowPresetPath::isLoop, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + FollowPresetPath::new + ); + + public static final Map lastRecordedPos = new ConcurrentHashMap<>(); private static final Map> recordingPaths = new ConcurrentHashMap<>(); private static final Map viewPaths = new ConcurrentHashMap<>(); - private static final Map lastRecordTime = new ConcurrentHashMap<>(); + private static final Map recordingTasks = new ConcurrentHashMap<>(); private static final Map activePlaybacks = new ConcurrentHashMap<>(); @Getter @Setter @@ -66,89 +84,85 @@ public class FollowPresetPathAction extends Action { @Setter private boolean loop; - public FollowPresetPathAction(List path, boolean loop, int delay, Condition.SelectionMode mode, - List conditions, int cooldown) { + public FollowPresetPath(List path, boolean loop, int delay, Selector mode, + List conditions, int cooldown) { super(delay, mode, conditions, cooldown); this.path = path; this.loop = loop; } - public static Button creationButton(Player player) { - return Button.clickable(ItemBuilder.modern(Material.RAIL) - .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.favicon")) - .setLore(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.description")) - .build(), - ButtonClickAction.plain((menuView, event) -> { - Player p = (Player) event.getWhoClicked(); - event.setCancelled(true); - p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); - FollowPresetPathAction action = new FollowPresetPathAction(new ArrayList<>(), false, 0, - Condition.SelectionMode.ONE, new ArrayList<>(), 0); - CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), action); - menuView.getAPI().openMenu(p, action.getMenu()); - })); - } - - public static FollowPresetPathAction deserialize(String serialized, Class clazz) { - if (clazz != FollowPresetPathAction.class) - throw new IllegalArgumentException("This deserialize method only supports the FollowPresetPathAction"); - ParseResult result = Action.parseBase(serialized); - String pathData = Action.parseString(serialized, "path"); - List path = CustomNPCs.getGson().fromJson(pathData, new TypeToken>() { - }.getType()); - boolean loop = Action.parseBoolean(pathData, "loop"); - return new FollowPresetPathAction(path, loop, result.delay(), result.mode(), result.conditions(), - result.cooldown()); - } - - public static void startRecording(Player player) { - UUID uuid = player.getUniqueId(); + public static void startRecording(final Player player) { + final UUID uuid = player.getUniqueId(); recordingPaths.put(uuid, new ArrayList<>()); - lastRecordTime.put(uuid, 0L); + lastRecordedPos.put(uuid, player.getLocation()); viewPaths.put(uuid, Bukkit.getScheduler().runTaskTimer(CustomNPCs.getInstance(), () -> { + Location loc = new Location(player.getWorld(), 0, 0, 0); for (RecordedPathNode n : recordingPaths.get(uuid)) { - player.spawnParticle(Particle.END_ROD, n.toLocation(player.getWorld()), 1, 0, 0, 0, 0); + loc = loc.add(n.getDelta(loc.getWorld())); + player.spawnParticle(Particle.END_ROD, loc, 1, 0, 0, 0, 0); } + }, 5, 5)); + recordingTasks.put(uuid, Bukkit.getScheduler().runTaskTimer(CustomNPCs.getInstance(), () -> { + recordMovement(player); + }, 1, 1)); + CustomNPCs.getInstance().wait(player, WaitingType.RECORDING); new RecordingRunnable(player, CustomNPCs.getInstance()).runTaskTimer(CustomNPCs.getInstance(), 0, 10); } public static List stopRecording(Player player) { UUID uuid = player.getUniqueId(); - lastRecordTime.remove(uuid); viewPaths.remove(uuid).cancel(); + recordingTasks.remove(uuid).cancel(); + lastRecordedPos.remove(uuid); return recordingPaths.remove(uuid); } // Better recordMovement - public static void recordMovement(Player player, Location loc) { + public static void recordMovement(Player player) { + final Location loc = player.getLocation().clone(); UUID uuid = player.getUniqueId(); List currentPath = recordingPaths.get(uuid); if (currentPath == null) return; - long now = System.currentTimeMillis(); if (currentPath.isEmpty()) { - lastRecordTime.put(uuid, now); - currentPath.add(new RecordedPathNode(0, loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch())); + lastRecordedPos.put(uuid, loc); + currentPath.add(new RecordedPathNode(0, loc, new Location(loc.getWorld(), 0, 0, 0))); return; } - long startTime = lastRecordTime.get(uuid); - long elapsed = now - startTime; - - // Sample every 50ms - RecordedPathNode lastNode = currentPath.get(currentPath.size() - 1); - if (elapsed - lastNode.timestamp() < 50) return; + currentPath.add(new RecordedPathNode(currentPath.size() + 1, player.getLocation(), lastRecordedPos.get(uuid))); + lastRecordedPos.put(uuid, player.getLocation()); + } - // Don't record duplicates - if (lastNode.x() == loc.getX() && lastNode.y() == loc.getY() && lastNode.z() == loc.getZ() && - lastNode.yaw() == loc.getYaw() && lastNode.pitch() == loc.getPitch()) { - return; - } + @Deprecated(forRemoval = true) + public static FollowPresetPath deserialize(String serialized, Class clazz) { + if (clazz != FollowPresetPath.class) + throw new IllegalArgumentException("This deserialize method only supports the FollowPresetPathAction"); + ParseResult result = parseBase(serialized); + String pathData = parseString(serialized, "path"); + List path = CustomNPCs.getGson().fromJson(pathData, new TypeToken>() { + }.getType()); + boolean loop = parseBoolean(pathData, "loop"); + return new FollowPresetPath(path, loop, result.delay(), result.mode(), result.conditions(), + result.cooldown()); + } - currentPath.add(new RecordedPathNode(elapsed, loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), - loc.getPitch())); + public Button creationButton(Player player) { + return Button.clickable(ItemBuilder.modern(Material.RAIL) + .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.favicon")) + .setLore(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.description")) + .build(), + ButtonClickAction.plain((menuView, event) -> { + Player p = (Player) event.getWhoClicked(); + event.setCancelled(true); + p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); + FollowPresetPath action = new FollowPresetPath(new ArrayList<>(), false, 0, + Selector.ONE, new ArrayList<>(), 0); + CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), action); + menuView.getAPI().openMenu(p, action.getMenu()); + })); } @Override @@ -159,32 +173,28 @@ public void perform(InternalNpc npc, Menu menu, Player player) { activePlaybacks.get(npc).cancel(); } final Location returnTo = npc.getCurrentLocation(); + final Location first = path.getFirst().getDelta(npc.getWorld()); + npc.teleport(first); - npc.teleport(path.getFirst().toLocation(npc.getWorld())); - - final int[] currentIndex = {0}; + final int[] currentIndex = {1}; BukkitTask task = Bukkit.getScheduler().runTaskTimer(CustomNPCs.getInstance(), () -> { if (currentIndex[0] >= path.size()) { if (loop) { - currentIndex[0] = 0; - npc.teleport(path.getFirst().toLocation(npc.getWorld())); + currentIndex[0] = 1; + npc.teleport(first); } else { BukkitTask t = activePlaybacks.remove(npc); if (t != null) t.cancel(); - Bukkit.getScheduler().runTaskLater(CustomNPCs.getInstance(), () -> npc.teleport(returnTo), 20L); + Bukkit.getScheduler().runTaskLater(CustomNPCs.getInstance(), () -> npc.teleport(returnTo), 1L); } return; } - Location prev; - if (currentIndex[0] > 0) { - prev = path.get(currentIndex[0] - 1).toLocation(npc.getWorld()); - } else prev = npc.getCurrentLocation(); + RecordedPathNode node = path.get(currentIndex[0]); - Location loc = node.toLocation(npc.getWorld()); + Location loc = node.getDelta(npc.getWorld()); npc.setYRotation(loc.getYaw()); npc.setXRotation(loc.getPitch()); - Vector vec = loc.subtract(prev).toVector(); - npc.moveTo(vec); + npc.moveTo(loc.toVector()); currentIndex[0]++; }, 0, 1); @@ -192,14 +202,6 @@ public void perform(InternalNpc npc, Menu menu, Player player) { activePlaybacks.put(npc, task); } - @Override - public String serialize() { - Map params = new HashMap<>(); - params.put("path", CustomNPCs.getGson().toJson(path)); - params.put("loop", loop); - return generateSerializedString("FollowPresetPath", params).replace("},]", "}]"); - } - @Override public ItemStack getFavicon(Player player) { return ItemBuilder.modern(Material.RAIL) @@ -223,14 +225,24 @@ public Menu getMenu() { @Override public Action clone() { - return new FollowPresetPathAction(new ArrayList<>(path), loop, getDelay(), getMode(), + return new FollowPresetPath(new ArrayList<>(path), loop, getDelay(), getSelector(), new ArrayList<>(getConditions()), getCooldown()); } + @Override + public StructCodec getCodec() { + return CODEC; + } + + @Override + public String getId() { + return "FollowPresetPath"; + } + private class FollowPathCustomizer implements Menu { - private final FollowPresetPathAction action; + private final FollowPresetPath action; - public FollowPathCustomizer(FollowPresetPathAction action) { + public FollowPathCustomizer(FollowPresetPath action) { this.action = action; } diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveEffect.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveEffect.java index ad9bdf7f..83faa545 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveEffect.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveEffect.java @@ -23,7 +23,8 @@ package dev.foxikle.customnpcs.actions.defaultImpl; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuItems; @@ -39,28 +40,45 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import net.kyori.adventure.text.Component; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import java.util.stream.Stream; import static org.bukkit.Material.*; @Getter @Setter +@NoArgsConstructor public class GiveEffect extends Action { + public static final StructCodec CODEC = StructCodec.struct( + "effect", Codec.STRING, GiveEffect::getEffect, + "duration", Codec.INT, GiveEffect::getDuration, + "amplifier", Codec.INT, GiveEffect::getAmplifier, + "particles", Codec.BOOLEAN, GiveEffect::isParticles, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + GiveEffect::new + ); + private static final List fields = Stream.of(PotionEffectType.class.getDeclaredFields()).filter(f -> Modifier.isStatic(f.getModifiers()) && Modifier.isPublic(f.getModifiers())).toList(); boolean particles; private String effect; @@ -72,7 +90,7 @@ public class GiveEffect extends Action { * * @param effect The raw message */ - public GiveEffect(String effect, int duration, int amplifier, boolean particles, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { + public GiveEffect(String effect, int duration, int amplifier, boolean particles, int delay, Selector mode, List conditionals, int cooldown) { super(delay, mode, conditionals, cooldown); this.effect = effect; this.duration = duration; @@ -80,23 +98,8 @@ public GiveEffect(String effect, int duration, int amplifier, boolean particles, this.particles = particles; } - /** - * Creates a new GiveEffect with the specified parameters - * - * @param effect The raw message - * @deprecated Use {@link #GiveEffect(String, int, int, boolean, int, Condition.SelectionMode, List, int)} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public GiveEffect(String effect, int duration, int amplifier, boolean particles, int delay, Condition.SelectionMode mode, List conditionals) { - super(delay, mode, conditionals, 0); - this.effect = effect; - this.duration = duration; - this.amplifier = amplifier; - this.particles = particles; - } - public static Button creationButton(Player player) { + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(BREWING_STAND) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.give_effect")) .setLore(Msg.lore(player.locale(), "customnpcs.favicons.give_effect.description")) @@ -105,28 +108,12 @@ public static Button creationButton(Player player) { event.setCancelled(true); Player p = (Player) event.getWhoClicked(); p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); - GiveEffect actionImpl = new GiveEffect("SPEED", 100, 0, false, 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0); + GiveEffect actionImpl = new GiveEffect("SPEED", 100, 0, false, 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); } - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(GiveEffect.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + GiveEffect.class.getName()); - } - String effect = parseString(serialized, "effect"); - int duration = parseInt(serialized, "duration"); - int amplifier = parseInt(serialized, "amplifier"); - boolean particles = parseBoolean(serialized, "particles"); - - ParseResult pr = parseBase(serialized); - - GiveEffect message = new GiveEffect(effect, duration, amplifier, particles, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - - return clazz.cast(message); - } - public ItemStack getFavicon(Player player) { return ItemBuilder.modern(BREWING_STAND).setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.give_effect")) .setLore( @@ -142,13 +129,6 @@ public ItemStack getFavicon(Player player) { ).build(); } - /** - * Sends a message to the player - * - * @param npc The NPC - * @param menu The menu - * @param player The player - */ @Override public void perform(InternalNpc npc, Menu menu, Player player) { if (!processConditions(player)) return; @@ -159,18 +139,36 @@ public void perform(InternalNpc npc, Menu menu, Player player) { } @Override - public String serialize() { + public StructCodec getCodec() { + return CODEC; + } - Map params = new HashMap<>(); - params.put("effect", effect); - params.put("duration", duration); - params.put("amplifier", amplifier); - params.put("particles", particles); - return generateSerializedString("GiveEffect", params); + @Override + public String getId() { + return "GiveEffect"; } public Action clone() { - return new GiveEffect(getEffect(), getDuration(), getAmplifier(), isParticles(), getDelay(), getMode(), new ArrayList<>(getConditions()), getCooldown()); + return new GiveEffect(getEffect(), getDuration(), getAmplifier(), isParticles(), getDelay(), getSelector(), + new ArrayList<>(getConditions()), getCooldown()); + } + + @Deprecated(forRemoval = true) + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(GiveEffect.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + GiveEffect.class.getName()); + } + String effect = parseString(serialized, "effect"); + int duration = parseInt(serialized, "duration"); + int amplifier = parseInt(serialized, "amplifier"); + boolean particles = parseBoolean(serialized, "particles"); + + ParseResult pr = parseBase(serialized); + + GiveEffect message = new GiveEffect(effect, duration, amplifier, particles, pr.delay(), pr.mode(), + pr.conditions(), pr.cooldown()); + + return clazz.cast(message); } @Override diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveXP.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveXP.java index d799f541..1689f687 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveXP.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveXP.java @@ -23,7 +23,8 @@ package dev.foxikle.customnpcs.actions.defaultImpl; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuItems; @@ -39,25 +40,37 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import net.kyori.adventure.text.Component; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -import java.util.Map; import static org.bukkit.Material.*; @Getter @Setter +@NoArgsConstructor public class GiveXP extends Action { - public static Button creationButton(Player player) { + public static final StructCodec CODEC = StructCodec.struct( + "amount", Codec.INT, GiveXP::getAmount, + "levels", Codec.BOOLEAN, GiveXP::isLevels, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + GiveXP::new + ); + + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(EXPERIENCE_BOTTLE) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.give_xp")) .setLore(Msg.lore(player.locale(), "customnpcs.favicons.give_xp.description")) @@ -67,7 +80,7 @@ public static Button creationButton(Player player) { Player p = (Player) event.getWhoClicked(); p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); - GiveXP actionImpl = new GiveXP(1, true, 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0); + GiveXP actionImpl = new GiveXP(1, true, 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); @@ -82,40 +95,12 @@ public static Button creationButton(Player player) { * @param levels if the xp is in levels * @param amount the number */ - public GiveXP(int amount, boolean levels, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { + public GiveXP(int amount, boolean levels, int delay, Selector mode, List conditionals, int cooldown) { super(delay, mode, conditionals, cooldown); this.levels = levels; this.amount = amount; } - /** - * Creates a new SendMessage with the specified message - * - * @param levels if the xp is in levels - * @param amount the number - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public GiveXP(int amount, boolean levels, int delay, Condition.SelectionMode mode, List conditionals) { - super(delay, mode, conditionals, 0); - this.levels = levels; - this.amount = amount; - } - - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(GiveXP.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + GiveXP.class.getName()); - } - - int amount = parseInt(serialized, "amount"); - boolean levels = parseBoolean(serialized, "levels"); - ParseResult pr = parseBase(serialized); - - GiveXP message = new GiveXP(amount, levels, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - - return clazz.cast(message); - } - @Override public ItemStack getFavicon(Player player) { return ItemBuilder.modern(EXPERIENCE_BOTTLE).setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.give_xp")) @@ -150,13 +135,18 @@ public void perform(InternalNpc npc, Menu menu, Player player) { } @Override - public String serialize() { - return generateSerializedString("GiveXP", Map.of("amount", amount, "levels", levels)); + public Action clone() { + return new GiveXP(amount, levels, getDelay(), getSelector(), new ArrayList<>(getConditions()), getCooldown()); } @Override - public Action clone() { - return new GiveXP(amount, levels, getDelay(), getMode(), new ArrayList<>(getConditions()), getCooldown()); + public StructCodec getCodec() { + return CODEC; + } + + @Override + public String getId() { + return "GiveXP"; } @Override @@ -164,6 +154,21 @@ public Menu getMenu() { return new GiveXPCustomizer(this); } + @Deprecated(forRemoval = true) + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(GiveXP.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + GiveXP.class.getName()); + } + + int amount = parseInt(serialized, "amount"); + boolean levels = parseBoolean(serialized, "levels"); + ParseResult pr = parseBase(serialized); + + GiveXP message = new GiveXP(amount, levels, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); + + return clazz.cast(message); + } + public class GiveXPCustomizer implements Menu { private final GiveXP action; diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/PlaySound.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/PlaySound.java index ff947331..3cec2759 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/PlaySound.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/PlaySound.java @@ -23,7 +23,8 @@ package dev.foxikle.customnpcs.actions.defaultImpl; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuItems; @@ -41,27 +42,40 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -import java.util.Map; import static dev.foxikle.customnpcs.internal.utils.Utils.DECIMAL_FORMAT; import static org.bukkit.Material.*; @Getter @Setter +@NoArgsConstructor public class PlaySound extends Action { - public static Button creationButton(Player player) { + public static final StructCodec CODEC = StructCodec.struct( + "sound", Codec.STRING, PlaySound::getSound, + "volume", Codec.FLOAT, PlaySound::getVolume, + "pitch", Codec.FLOAT, PlaySound::getPitch, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + PlaySound::new + ); + + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(BELL) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.sound")) .setLore(Msg.lore(player.locale(), "customnpcs.favicons.sound.description")) @@ -70,7 +84,7 @@ public static Button creationButton(Player player) { event.setCancelled(true); Player p = (Player) event.getWhoClicked(); p.playSound(Sound.sound(Key.key("minecraft:ui.button.click"), Sound.Source.MASTER, 1, 1)); - PlaySound actionImpl = new PlaySound("minecraft:ui.button.click", 1, 1, 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0); + PlaySound actionImpl = new PlaySound("minecraft:ui.button.click", 1, 1, 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); @@ -80,51 +94,13 @@ public static Button creationButton(Player player) { float pitch; private String sound; - /** - * Creates a new SendMessage with the specified message - * - * @param sound The sound enum constants - * @param pitch The pitch, between 0.0f and 1.0f - * @param volume The volume, between 0.0f and 1.0f - */ - public PlaySound(String sound, float volume, float pitch, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { + public PlaySound(String sound, float volume, float pitch, int delay, Selector mode, List conditionals, int cooldown) { super(delay, mode, conditionals, cooldown); this.sound = sound; this.volume = volume; this.pitch = pitch; } - /** - * Creates a new SendMessage with the specified message - * - * @param sound The sound enum constants - * @param pitch The pitch, between 0.0f and 1.0f - * @param volume The volume, between 0.0f and 1.0f - * @deprecated Use {@link PlaySound#PlaySound(String, float, float, int, Condition.SelectionMode, List, int)} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public PlaySound(String sound, float volume, float pitch, int delay, Condition.SelectionMode mode, List conditionals) { - super(delay, mode, conditionals, 0); - this.sound = sound; - this.volume = volume; - this.pitch = pitch; - } - - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(PlaySound.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + PlaySound.class.getName()); - } - String sound = parseString(serialized, "sound"); - float volume = parseFloat(serialized, "volume"); - float pitch = parseFloat(serialized, "pitch"); - - ParseResult pr = parseBase(serialized); - - PlaySound message = new PlaySound(sound, volume, pitch, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - - return clazz.cast(message); - } @Override public ItemStack getFavicon(Player player) { @@ -156,13 +132,19 @@ public void perform(InternalNpc npc, Menu menu, Player player) { activateCooldown(player.getUniqueId()); } + public Action clone() { + return new PlaySound(sound, volume, pitch, getDelay(), getSelector(), new ArrayList<>(getConditions()), + getCooldown()); + } + @Override - public String serialize() { - return generateSerializedString("PlaySound", Map.of("sound", sound, "volume", volume, "pitch", pitch)); + public StructCodec getCodec() { + return CODEC; } - public Action clone() { - return new PlaySound(sound, volume, pitch, getDelay(), getMode(), new ArrayList<>(getConditions()), getCooldown()); + @Override + public String getId() { + return "PlaySound"; } @Override @@ -170,6 +152,22 @@ public Menu getMenu() { return new PlaySoundCustomizer(this); } + @Deprecated(forRemoval = true) + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(PlaySound.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + PlaySound.class.getName()); + } + String sound = parseString(serialized, "sound"); + float volume = parseFloat(serialized, "volume"); + float pitch = parseFloat(serialized, "pitch"); + + ParseResult pr = parseBase(serialized); + + PlaySound message = new PlaySound(sound, volume, pitch, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); + + return clazz.cast(message); + } + public class PlaySoundCustomizer implements Menu { private final PlaySound action; diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RecordedPathNode.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RecordedPathNode.java index d8af60b0..8e943e3e 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RecordedPathNode.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RecordedPathNode.java @@ -22,21 +22,86 @@ package dev.foxikle.customnpcs.actions.defaultImpl; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Location; import org.bukkit.World; +import org.jspecify.annotations.NonNull; import java.io.Serializable; -public record RecordedPathNode(long timestamp, double x, double y, double z, float yaw, +public record RecordedPathNode(int index, int dx, int dy, int dz, float yaw, float pitch) implements Serializable { - public Location toLocation(World world) { - return new Location(world, x, y, z, yaw, pitch); + private static final long DX_MASK = 0x1FFFFFL; // 21 bits + private static final long DY_MASK = 0x3FFFFFL; // 22 bits + private static final long DZ_MASK = 0x1FFFFFL; // 21 bits + + public static final Codec CODEC = StructCodec.struct( + "index", Codec.INT, RecordedPathNode::index, + "pos", Codec.LONG, RecordedPathNode::packedPosition, + "rot", Codec.INT, RecordedPathNode::packedRotation, + RecordedPathNode::new + ); + + public RecordedPathNode(int timestamp, Location current, Location previous) { + Location l = current.clone().subtract(previous); + this(timestamp, (int) (l.x() * 1000), (int) (l.y() * 1000), (int) (l.z() * 1000), l.getYaw(), l.getPitch()); + } + + public RecordedPathNode(int time, long packedPos, int packedRot) { + this(time, unpackX(packedPos), unpackY(packedPos), unpackZ(packedPos), + unpackYaw(packedRot), unpackPitch(packedRot)); + } + + private static int unpackX(long packed) { + return (int) (packed >> 43); + } + + private static int unpackY(long packed) { + return (int) (packed << 21 >> 42); + } + + private static int unpackZ(long packed) { + return (int) (packed << 43 >> 43); + } + + public static float unpackYaw(int packed) { + return ((packed >>> 16) & 0xFFFF) * 360f / 65535f; + } + + public static float unpackPitch(int packed) { + return ((packed & 0xFFFF) * 180f / 65535f) - 90f; + } + + private static void checkRange(int value, int min, int max) { + if (value < min || value > max) { + throw new IllegalArgumentException( + value + " outside range [" + min + ", " + max + "]"); + } + } + + public long packedPosition() { + checkRange(dx, -(1 << 20), (1 << 20) - 1); + checkRange(dy, -(1 << 21), (1 << 21) - 1); + checkRange(dz, -(1 << 20), (1 << 20) - 1); + + return ((long) dx & DX_MASK) << 43 | ((long) dy & DY_MASK) << 21 | ((long) dz & DZ_MASK); + } + + public int packedRotation() { + int y = Math.round(((yaw % 360f + 360f) % 360f) * 65535f / 360f); + int p = Math.round((pitch + 90f) * 65535f / 180f); + return (y << 16) | (p & 0xFFFF); + } + + public Location getDelta(World world) { + return new Location(world, dx / 1000.0, dy / 1000.0, dz / 1000.0, yaw, pitch); } @Override - public String toString() { - return String.format("Node{t=%d, x=%.2f, y=%.2f, z=%.2f, yaw=%.1f, pitch=%.1f}", timestamp, x, y, z, yaw, - pitch); + public @NonNull String toString() { + return String.format("Node{t=%d, dx=%.2f, dy=%.2f, dz=%.2f, yaw=%.1f, pitch=%.1f}", index, + dx / 1000.0, dy / 1000.0, dz / 1000.0, yaw, pitch); } } diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveEffect.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveEffect.java index 16e6ad6e..9a646d62 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveEffect.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveEffect.java @@ -23,7 +23,8 @@ package dev.foxikle.customnpcs.actions.defaultImpl; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuUtils; @@ -38,21 +39,22 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import net.kyori.adventure.text.Component; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffectType; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.stream.Stream; @@ -61,35 +63,28 @@ @Getter @Setter +@NoArgsConstructor public class RemoveEffect extends Action { + public static final StructCodec CODEC = StructCodec.struct( + "effect", Codec.STRING, RemoveEffect::getEffect, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + RemoveEffect::new + ); + private static final List fields = Stream.of(PotionEffectType.class.getDeclaredFields()).filter(f -> Modifier.isStatic(f.getModifiers()) && Modifier.isPublic(f.getModifiers())).toList(); private String effect; - /** - * Creates a new GiveEffect with the specified parameters - * - * @param effect The raw message - */ - public RemoveEffect(String effect, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { + public RemoveEffect(String effect, int delay, Selector mode, List conditionals, int cooldown) { super(delay, mode, conditionals, cooldown); this.effect = effect; } - /** - * Creates a new GiveEffect with the specified parameters - * - * @param effect The raw message - * @deprecated Use {@link #RemoveEffect(String, int, Condition.SelectionMode, List, int)} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public RemoveEffect(String effect, int delay, Condition.SelectionMode mode, List conditionals) { - super(delay, mode, conditionals, 0); - this.effect = effect; - } - public static Button creationButton(Player player) { + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(MILK_BUCKET) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.remove_effect")) @@ -99,23 +94,12 @@ public static Button creationButton(Player player) { event.setCancelled(true); Player p = (Player) event.getWhoClicked(); p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); - RemoveEffect actionImpl = new RemoveEffect("SPEED", 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0); + RemoveEffect actionImpl = new RemoveEffect("SPEED", 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(player.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); } - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(RemoveEffect.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + RemoveEffect.class.getName()); - } - String effect = parseString(serialized, "effect"); - ParseResult pr = parseBase(serialized); - RemoveEffect message = new RemoveEffect(effect, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - - return clazz.cast(message); - } - @Override public ItemStack getFavicon(Player player) { return ItemBuilder.modern(MILK_BUCKET).setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.remove_effect")) @@ -151,13 +135,30 @@ public void perform(InternalNpc npc, Menu menu, Player player) { } @Override - public String serialize() { - return generateSerializedString("RemoveEffect", Map.of("effect", effect)); + public StructCodec getCodec() { + return CODEC; + } + + @Override + public String getId() { + return "RemoveEffect"; } @Override public Action clone() { - return new RemoveEffect(effect, getDelay(), getMode(), new ArrayList<>(getConditions()), getCooldown()); + return new RemoveEffect(effect, getDelay(), getSelector(), new ArrayList<>(getConditions()), getCooldown()); + } + + @Deprecated(forRemoval = true) + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(RemoveEffect.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + RemoveEffect.class.getName()); + } + String effect = parseString(serialized, "effect"); + ParseResult pr = parseBase(serialized); + RemoveEffect message = new RemoveEffect(effect, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); + + return clazz.cast(message); } public class RemoveEffectCustomizer implements Menu { diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveXP.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveXP.java index 0a1edd11..0b92a948 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveXP.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveXP.java @@ -23,7 +23,8 @@ package dev.foxikle.customnpcs.actions.defaultImpl; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuItems; @@ -40,25 +41,45 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import net.kyori.adventure.text.Component; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -import java.util.Map; import static org.bukkit.Material.*; @Getter @Setter +@NoArgsConstructor public class RemoveXP extends Action { - public static Button creationButton(Player player) { + public static final StructCodec CODEC = StructCodec.struct( + "amount", Codec.INT, RemoveXP::getAmount, + "levels", Codec.BOOLEAN, RemoveXP::isLevels, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + RemoveXP::new + ); + private int amount; + private boolean levels; + + public RemoveXP(int amount, boolean levels, int delay, Selector mode, List conditionals, int cooldown) { + super(delay, mode, conditionals, cooldown); + this.levels = levels; + this.amount = amount; + } + + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(GLASS_BOTTLE) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.remove_xp")) .setLore(Msg.lore(player.locale(), "customnpcs.favicons.remove_xp.description")) @@ -68,63 +89,23 @@ public static Button creationButton(Player player) { Player p = (Player) event.getWhoClicked(); p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); - RemoveXP actionImpl = new RemoveXP(1, true, 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0); + RemoveXP actionImpl = new RemoveXP(1, true, 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); } - private int amount; - private boolean levels; - - /** - * Creates a new SendMessage with the specified message - * - * @param levels if the xp is in levels - * @param amount the number of XP to remove - */ - public RemoveXP(int amount, boolean levels, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { - super(delay, mode, conditionals, cooldown); - this.levels = levels; - this.amount = amount; - } - - /** - * Creates a new SendMessage with the specified message - * - * @param levels if the xp is in levels - * @param amount the number of XP to remove - * @deprecated Use {@link #RemoveXP(int, boolean, int, Condition.SelectionMode, List, int)} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public RemoveXP(int amount, boolean levels, int delay, Condition.SelectionMode mode, List conditionals) { - super(delay, mode, conditionals, 0); - this.levels = levels; - this.amount = amount; - } - - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(RemoveXP.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + RemoveXP.class.getName()); - } - int amount = parseInt(serialized, "amount"); - boolean levels = parseBoolean(serialized, "levels"); - ParseResult pr = parseBase(serialized); - - RemoveXP xp = new RemoveXP(amount, levels, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - - return clazz.cast(xp); - } - @Override public ItemStack getFavicon(Player player) { - return ItemBuilder.modern(GLASS_BOTTLE).setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.remove_xp")) + return ItemBuilder.modern(GLASS_BOTTLE).setDisplay(Msg.translate(player.locale(), "customnpcs.favicons" + + ".remove_xp")) .setLore( Msg.translate(player.locale(), "customnpcs.favicons.delay", getDelay()), Msg.format(""), Msg.translate(player.locale(), "customnpcs.menus.action.remove_xp.xp", amount), - Msg.translate(player.locale(), "customnpcs.menus.action.remove_xp.awarding", (levels ? Msg.translatedString(player.locale(), "customnpcs.menus.action.xp.levels") : Msg.translatedString(player.locale(), "customnpcs.menus.action.xp.points"))), + Msg.translate(player.locale(), "customnpcs.menus.action.remove_xp.awarding", (levels ? + Msg.translatedString(player.locale(), "customnpcs.menus.action.xp.levels") : + Msg.translatedString(player.locale(), "customnpcs.menus.action.xp.points"))), Msg.format(""), Msg.translate(player.locale(), "customnpcs.favicons.edit"), Msg.translate(player.locale(), "customnpcs.favicons.remove") @@ -160,13 +141,33 @@ public void perform(InternalNpc npc, Menu menu, Player player) { } @Override - public String serialize() { - return generateSerializedString("RemoveXP", Map.of("amount", amount, "levels", levels)); + public StructCodec getCodec() { + return CODEC; + } + + @Override + public String getId() { + return "RemoveXP"; } @Override public Action clone() { - return new RemoveXP(getAmount(), isLevels(), getDelay(), getMode(), new ArrayList<>(getConditions()), getCooldown()); + return new RemoveXP(getAmount(), isLevels(), getDelay(), getSelector(), new ArrayList<>(getConditions()), + getCooldown()); + } + + @Deprecated(forRemoval = true) + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(RemoveXP.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + RemoveXP.class.getName()); + } + int amount = parseInt(serialized, "amount"); + boolean levels = parseBoolean(serialized, "levels"); + ParseResult pr = parseBase(serialized); + + RemoveXP xp = new RemoveXP(amount, levels, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); + + return clazz.cast(xp); } public class RemoveXPCustomizer implements Menu { @@ -194,14 +195,17 @@ public String getName() { @Override public @NotNull Content getContent(DataRegistry dataRegistry, Player player, Capacity capacity) { - Component[] incLore = Msg.lore(player.locale(), "customnpcs.menus.action_customizer.delay.increment.description"); - Component[] decLore = Msg.lore(player.locale(), "customnpcs.menus.action_customizer.delay.decrement.description"); + Component[] incLore = Msg.lore(player.locale(), "customnpcs.menus.action_customizer.delay.increment" + + ".description"); + Component[] decLore = Msg.lore(player.locale(), "customnpcs.menus.action_customizer.delay.decrement" + + ".description"); return MenuUtils.actionBase(action, player) // increment .setButton(11, Button.clickable(ItemBuilder.modern(LIME_DYE) - .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.give_xp.increase")) + .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.give_xp" + + ".increase")) .setLore(incLore) .build(), ButtonClickAction.plain((menuView, event) -> { @@ -214,18 +218,22 @@ public String getName() { } else if (event.isRightClick()) { setAmount(getAmount() + 5); } - menuView.updateButton(20, button -> button.setItem(MenuItems.genericDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.remove_xp.xp", getAmount())))); + menuView.updateButton(20, + button -> button.setItem(MenuItems.genericDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.remove_xp.xp", getAmount())))); })) - ).setButton(20, MenuItems.genericDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.remove_xp.xp", getAmount())) + ).setButton(20, MenuItems.genericDisplay(Msg.translate(player.locale(), "customnpcs.menus.action" + + ".remove_xp.xp", getAmount())) ).setButton(29, Button.clickable(ItemBuilder.modern(RED_DYE) - .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.give_xp.decrease")) + .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.give_xp" + + ".decrease")) .setLore(decLore) .build(), ButtonClickAction.plain((menuView, event) -> { event.setCancelled(true); player.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); if (getAmount() == 1) { - event.getWhoClicked().sendMessage(Msg.translate(player.locale(), "customnpcs.menus.action.give_xp.xp_less_one")); + event.getWhoClicked().sendMessage(Msg.translate(player.locale(), "customnpcs" + + ".menus.action.give_xp.xp_less_one")); return; } @@ -236,7 +244,8 @@ public String getName() { } else if (event.isRightClick()) { setAmount(Math.max(1, getAmount() - 5)); } - menuView.updateButton(20, button -> button.setItem(MenuItems.genericDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.remove_xp.xp", getAmount())))); + menuView.updateButton(20, + button -> button.setItem(MenuItems.genericDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.remove_xp.xp", getAmount())))); })) ).setButton(24, toggle(player)) @@ -245,7 +254,10 @@ public String getName() { private Button toggle(Player player) { return Button.clickable(ItemBuilder.modern(isLevels() ? GREEN_CANDLE : RED_CANDLE) - .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.remove_xp.awarding", (isLevels() ? Msg.translate(player.locale(), "customnpcs.menus.action.xp.levels") : Msg.translate(player.locale(), "customnpcs.menus.action.xp.points")))) + .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.remove_xp.awarding", + (isLevels() ? + Msg.translate(player.locale(), "customnpcs.menus.action.xp.levels") : + Msg.translate(player.locale(), "customnpcs.menus.action.xp.points")))) .build(), ButtonClickAction.plain((menuView, event) -> { event.setCancelled(true); diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RunCommand.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RunCommand.java index 978039f1..f5309894 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RunCommand.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RunCommand.java @@ -23,7 +23,8 @@ package dev.foxikle.customnpcs.actions.defaultImpl; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuItems; @@ -42,66 +43,39 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import me.clip.placeholderapi.PlaceholderAPI; import net.kyori.adventure.text.Component; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Bukkit; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -import java.util.Map; import static org.bukkit.Material.*; @Getter @Setter +@NoArgsConstructor public class RunCommand extends Action { - private String command; - private boolean asConsole; - - /** - * Creates a new RunCommand with the specified command - * - * @param rawCommand The raw command - * @param asConsole If the command should be executed as a console command. - * @param delay The delay - * @param mode The mode - * @param conditionals The conditionals - */ - public RunCommand(String rawCommand, boolean asConsole, int delay, Condition.SelectionMode mode, - List conditionals, int cooldown) { - super(delay, mode, conditionals, cooldown); - this.command = rawCommand; - this.asConsole = asConsole; - } - - - /** - * Creates a new RunCommand with the specified command - * - * @param rawCommand The raw command - * @param asConsole If the command should be executed as a console command. - * @param delay The delay - * @param mode The mode - * @param conditionals The conditionals - * @deprecated Use {@link #RunCommand(String, boolean, int, Condition.SelectionMode, List, int)} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public RunCommand(String rawCommand, boolean asConsole, int delay, Condition.SelectionMode mode, - List conditionals) { - super(delay, mode, conditionals, 0); - this.command = rawCommand; - this.asConsole = asConsole; - } + public static final StructCodec CODEC = StructCodec.struct( + "raw", Codec.STRING, RunCommand::getCommand, + "asConsole", Codec.BOOLEAN, RunCommand::isAsConsole, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + RunCommand::new + ); - public static Button creationButton(Player player) { + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(ANVIL) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.command")) .setLore(Msg.lore(player.locale(), "customnpcs.favicons.command.description")) @@ -110,23 +84,21 @@ public static Button creationButton(Player player) { Player p = (Player) event.getWhoClicked(); event.setCancelled(true); p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); - RunCommand actionImpl = new RunCommand("say hi", false, 0, Condition.SelectionMode.ONE, - new ArrayList<>(), 0); + RunCommand actionImpl = new RunCommand("say hi", false, 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); } - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(RunCommand.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + RunCommand.class.getName()); - } - String raw = parseString(serialized, "raw"); - boolean asConsole = parseBoolean(serialized, "asConsole"); - ParseResult pr = parseBase(serialized); + private String command; + private boolean asConsole; - RunCommand command = new RunCommand(raw, asConsole, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - return clazz.cast(command); + + public RunCommand(String rawCommand, boolean asConsole, int delay, Selector mode, + List conditionals, int cooldown) { + super(delay, mode, conditionals, cooldown); + this.command = rawCommand; + this.asConsole = asConsole; } @Override @@ -148,14 +120,6 @@ public Menu getMenu() { return new RunCommandCustomizer(this); } - /** - * Runs the command. If {@code asConsole} is true, the command will be executed as the console, - * otherwise it will be executed as the player. - * - * @param npc The NPC - * @param menu The menu - * @param player The player - */ @Override public void perform(InternalNpc npc, Menu menu, Player player) { if (!processConditions(player)) return; @@ -166,16 +130,34 @@ public void perform(InternalNpc npc, Menu menu, Player player) { } @Override - public String serialize() { - return generateSerializedString("RunCommand", Map.of("raw", command, "asConsole", asConsole)); + public StructCodec getCodec() { + return CODEC; + } + + @Override + public String getId() { + return "RunCommand"; } @Override public Action clone() { - return new RunCommand(command, asConsole, getDelay(), getMode(), new ArrayList<>(getConditions()), + return new RunCommand(command, asConsole, getDelay(), getSelector(), new ArrayList<>(getConditions()), getCooldown()); } + @Deprecated(forRemoval = true) + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(RunCommand.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + RunCommand.class.getName()); + } + String raw = parseString(serialized, "raw"); + boolean asConsole = parseBoolean(serialized, "asConsole"); + ParseResult pr = parseBase(serialized); + + RunCommand command = new RunCommand(raw, asConsole, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); + return clazz.cast(command); + } + public class RunCommandCustomizer implements Menu { private final RunCommand action; @@ -220,8 +202,9 @@ private Button toggle(Player player) { return Button.clickable(ItemBuilder.modern(isAsConsole() ? RED_CANDLE : GREEN_CANDLE) .setLore(lore.toArray(new Component[]{})) .setDisplay(isAsConsole() ? Msg.translate(player.locale(), "customnpcs.menus.action" + - ".command.as_console.true") : Msg.translate(player.locale(), "customnpcs.menus" + - ".action.command.as_console.false")) + ".command.as_console.true") : + Msg.translate(player.locale(), "customnpcs.menus" + + ".action.command.as_console.false")) .build(), ButtonClickAction.plain((menuView, event) -> { event.setCancelled(true); diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendMessage.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendMessage.java index 01e084a0..4ea3e037 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendMessage.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendMessage.java @@ -23,7 +23,8 @@ package dev.foxikle.customnpcs.actions.defaultImpl; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuUtils; @@ -40,52 +41,47 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import me.clip.placeholderapi.PlaceholderAPI; import net.kyori.adventure.text.format.TextDecoration; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -import java.util.Map; import static org.bukkit.Material.OAK_HANGING_SIGN; import static org.bukkit.Material.PAPER; @Getter @Setter +@NoArgsConstructor public class SendMessage extends Action { + public static final StructCodec CODEC = StructCodec.struct( + "raw", Codec.STRING, SendMessage::getRawMessage, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + SendMessage::new + ); + private String rawMessage; - /** - * Creates a new SendMessage with the specified message - * - * @param rawMessage The raw message - */ - public SendMessage(String rawMessage, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { + + public SendMessage(String rawMessage, int delay, Selector mode, List conditionals, int cooldown) { super(delay, mode, conditionals, cooldown); this.rawMessage = rawMessage; } - /** - * Creates a new SendMessage with the specified message - * - * @param rawMessage The raw message - * @deprecated Use {@link SendMessage#SendMessage(String, int, Condition.SelectionMode, List, int)} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public SendMessage(String rawMessage, int delay, Condition.SelectionMode mode, List conditionals) { - super(delay, mode, conditionals); - this.rawMessage = rawMessage; - } - public static Button creationButton(Player player) { + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(PAPER) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.message")) .setLore(Msg.lore(player.locale(), "customnpcs.favicons.message.description")) @@ -95,23 +91,12 @@ public static Button creationButton(Player player) { Player p = (Player) event.getWhoClicked(); p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); - SendMessage actionImpl = new SendMessage("", 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0); + SendMessage actionImpl = new SendMessage("", 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); } - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(SendMessage.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + SendMessage.class.getName()); - } - String rawMessage = parseString(serialized, "raw"); - ParseResult pr = parseBase(serialized); - SendMessage message = new SendMessage(rawMessage, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - - return clazz.cast(message); - } - @Override public ItemStack getFavicon(Player player) { return ItemBuilder.modern(PAPER).setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.message")) @@ -130,13 +115,6 @@ public Menu getMenu() { return new SendMessageCustomizer(this); } - /** - * Sends a message to the player - * - * @param npc The NPC - * @param menu The menu - * @param player The player - */ @Override public void perform(InternalNpc npc, Menu menu, Player player) { if (!processConditions(player)) return; @@ -150,13 +128,29 @@ public void perform(InternalNpc npc, Menu menu, Player player) { } @Override - public String serialize() { - return generateSerializedString("SendMessage", Map.of("raw", rawMessage)); + public StructCodec getCodec() { + return CODEC; + } + + @Override + public String getId() { + return "SendMessage"; } @Override public Action clone() { - return new SendMessage(rawMessage, getDelay(), getMode(), getConditions(), getCooldown()); + return new SendMessage(rawMessage, getDelay(), getSelector(), getConditions(), getCooldown()); + } + + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(SendMessage.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + SendMessage.class.getName()); + } + String rawMessage = parseString(serialized, "raw"); + ParseResult pr = parseBase(serialized); + SendMessage message = new SendMessage(rawMessage, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); + + return clazz.cast(message); } public class SendMessageCustomizer implements Menu { diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendServer.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendServer.java index 4e9f41a0..0d7d7f46 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendServer.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendServer.java @@ -25,7 +25,8 @@ import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuUtils; @@ -42,52 +43,45 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import net.kyori.adventure.text.Component; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import static org.bukkit.Material.GRASS_BLOCK; import static org.bukkit.Material.OAK_HANGING_SIGN; @Getter @Setter +@NoArgsConstructor public class SendServer extends Action { + public static final StructCodec CODEC = StructCodec.struct( + "server", Codec.STRING, SendServer::getServer, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + SendServer::new + ); + private String server; - /** - * Creates a new SendMessage with the specified message - * - * @param server The raw message - */ - public SendServer(String server, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { - super(delay, mode, conditionals, cooldown); - this.server = server; - } - /** - * Creates a new SendMessage with the specified message - * - * @param server The raw message - * @deprecated Use {@link SendServer#SendServer(String, int, Condition.SelectionMode, List, int)} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public SendServer(String server, int delay, Condition.SelectionMode mode, List conditionals) { - super(delay, mode, conditionals, 0); + public SendServer(String server, int delay, Selector mode, List conditionals, int cooldown) { + super(delay, mode, conditionals, cooldown); this.server = server; } - public static Button creationButton(Player player) { + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(GRASS_BLOCK) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.server")) @@ -99,31 +93,13 @@ public static Button creationButton(Player player) { p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); //todo: watch out for duplications - SendServer actionImpl = new SendServer("server", 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0); + SendServer actionImpl = new SendServer("server", 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); } - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(SendServer.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + SendServer.class.getName()); - } - String server = parseString(serialized, "server"); - ParseResult pr = parseBase(serialized); - - SendServer message = new SendServer(server, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - return clazz.cast(message); - } - - /** - * Sends a message to the player - * - * @param npc The NPC - * @param menu The menu - * @param player The player - */ @Override public void perform(InternalNpc npc, Menu menu, Player player) { if (!processConditions(player)) return; @@ -136,12 +112,6 @@ public void perform(InternalNpc npc, Menu menu, Player player) { activateCooldown(player.getUniqueId()); } - @Override - public String serialize() { - Map params = new HashMap<>(); - params.put("server", server); - return generateSerializedString("SendServer", params); - } @Override public ItemStack getFavicon(Player player) { @@ -161,9 +131,32 @@ public Menu getMenu() { return new SendServerCustomizer(this); } + @Override + public StructCodec getCodec() { + return CODEC; + } + + @Override + public String getId() { + return "SendServer"; + } + @Override public Action clone() { - return new SendServer(server, getDelay(), getMode(), getConditions(), getCooldown()); + return new SendServer(server, getDelay(), getSelector(), getConditions(), getCooldown()); + } + + @Deprecated(forRemoval = true) + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(SendServer.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + SendServer.class.getName()); + } + String server = parseString(serialized, "server"); + ParseResult pr = parseBase(serialized); + + SendServer message = new SendServer(server, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); + + return clazz.cast(message); } public class SendServerCustomizer implements Menu { diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/Teleport.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/Teleport.java index a9b4be9c..963ccb0c 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/Teleport.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/Teleport.java @@ -23,7 +23,8 @@ package dev.foxikle.customnpcs.actions.defaultImpl; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuItems; @@ -39,69 +40,48 @@ import io.github.mqzen.menus.titles.MenuTitle; import io.github.mqzen.menus.titles.MenuTitles; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import net.kyori.adventure.text.Component; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Location; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import static org.bukkit.Material.*; @Getter @Setter +@NoArgsConstructor public class Teleport extends Action { + public static final StructCodec CODEC = StructCodec.struct( + "x", Codec.DOUBLE, Teleport::getX, + "y", Codec.DOUBLE, Teleport::getY, + "z", Codec.DOUBLE, Teleport::getZ, + "pitch", Codec.FLOAT, Teleport::getPitch, + "yaw", Codec.FLOAT, Teleport::getYaw, + "delay", Codec.INT, Action::getDelay, + "selector", Codec.Enum(Selector.class), Action::getSelector, + "conditions", Condition.CODEC.list(), Action::getConditions, + "cooldown", Codec.INT, Action::getCooldown, + Teleport::new + ); + private double x; private double y; private double z; private float pitch; private float yaw; - /** - * Creates a new SendMessage with the specified message - * - * @param x The x coordinate - * @param y The y coordinate - * @param z The z coordinate - * @param pitch The pitch of the player - * @param yaw The yaw of the player - * @param conditionals The conditionals - * @param delay The delay - * @param mode The selection mode of the action's conditions - */ - public Teleport(double x, double y, double z, float pitch, float yaw, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { - super(delay, mode, conditionals, cooldown); - this.x = x; - this.y = y; - this.z = z; - this.pitch = pitch; - this.yaw = yaw; - } - /** - * Creates a new SendMessage with the specified message - * - * @param x The x coordinate - * @param y The y coordinate - * @param z The z coordinate - * @param pitch The pitch of the player - * @param yaw The yaw of the player - * @param conditionals The conditionals - * @param delay The delay - * @param mode The selection mode of the action's conditions - * @deprecated Use {@link Teleport#Teleport(double, double, double, float, float, int, Condition.SelectionMode, List, int)} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public Teleport(double x, double y, double z, float pitch, float yaw, int delay, Condition.SelectionMode mode, List conditionals) { - super(delay, mode, conditionals, 0); + public Teleport(double x, double y, double z, float pitch, float yaw, int delay, Selector mode, List conditionals, int cooldown) { + super(delay, mode, conditionals, cooldown); this.x = x; this.y = y; this.z = z; @@ -109,7 +89,7 @@ public Teleport(double x, double y, double z, float pitch, float yaw, int delay, this.yaw = yaw; } - public static Button creationButton(Player player) { + public Button creationButton(Player player) { return Button.clickable(ItemBuilder.modern(ENDER_PEARL) .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.teleport")) .setLore(Msg.lore(player.locale(), "customnpcs.favicons.teleport.description")) @@ -119,28 +99,12 @@ public static Button creationButton(Player player) { Player p = (Player) event.getWhoClicked(); p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1); - Teleport actionImpl = new Teleport(0, 0, 0, 0F, 0F, 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0); + Teleport actionImpl = new Teleport(0, 0, 0, 0F, 0F, 0, Selector.ONE, new ArrayList<>(), 0); CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl); menuView.getAPI().openMenu(p, actionImpl.getMenu()); })); } - public static T deserialize(String serialized, Class clazz) { - if (!clazz.equals(Teleport.class)) { - throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + Teleport.class.getName()); - } - - double x = parseDouble(serialized, "x"); - double y = parseDouble(serialized, "y"); - double z = parseDouble(serialized, "z"); - float pitch = parseFloat(serialized, "pitch"); - float yaw = parseFloat(serialized, "yaw"); - ParseResult pr = parseBase(serialized); - - Teleport message = new Teleport(x, y, z, pitch, yaw, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); - return clazz.cast(message); - } - @Override public ItemStack getFavicon(Player player) { return ItemBuilder.modern(ENDER_PEARL).setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.teleport")) @@ -163,13 +127,7 @@ public Menu getMenu() { return new TeleportCustomizer(this); } - /** - * Sends a message to the player - * - * @param npc The NPC - * @param menu The menu - * @param player The player - */ + @Override public void perform(InternalNpc npc, Menu menu, Player player) { if (!processConditions(player)) return; @@ -177,20 +135,38 @@ public void perform(InternalNpc npc, Menu menu, Player player) { activateCooldown(player.getUniqueId()); } + @Override - public String serialize() { - Map params = new HashMap<>(); - params.put("x", x); - params.put("y", y); - params.put("z", z); - params.put("yaw", yaw); - params.put("pitch", pitch); - return generateSerializedString("Teleport", params); + public Action clone() { + return new Teleport(getX(), getY(), getZ(), getPitch(), getYaw(), getDelay(), getSelector(), getConditions(), + getCooldown()); } @Override - public Action clone() { - return new Teleport(getX(), getY(), getZ(), getPitch(), getYaw(), getDelay(), getMode(), getConditions(), getCooldown()); + public StructCodec getCodec() { + return CODEC; + } + + @Override + public String getId() { + return "Teleport"; + } + + @Deprecated(forRemoval = true) + public static T deserialize(String serialized, Class clazz) { + if (!clazz.equals(Teleport.class)) { + throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + Teleport.class.getName()); + } + + double x = parseDouble(serialized, "x"); + double y = parseDouble(serialized, "y"); + double z = parseDouble(serialized, "z"); + float pitch = parseFloat(serialized, "pitch"); + float yaw = parseFloat(serialized, "yaw"); + ParseResult pr = parseBase(serialized); + + Teleport message = new Teleport(x, y, z, pitch, yaw, pr.delay(), pr.mode(), pr.conditions(), pr.cooldown()); + return clazz.cast(message); } public static class TeleportCustomizer implements Menu { diff --git a/core/src/main/java/dev/foxikle/customnpcs/api/NPC.java b/core/src/main/java/dev/foxikle/customnpcs/api/NPC.java index 9df894cf..4810d7c9 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/api/NPC.java +++ b/core/src/main/java/dev/foxikle/customnpcs/api/NPC.java @@ -24,8 +24,8 @@ import com.google.common.base.Preconditions; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.LegacyAction; import dev.foxikle.customnpcs.api.events.NpcDeleteEvent; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.LookAtAnchor; @@ -79,7 +79,7 @@ public NPC(@NotNull World world) { UUID uuid = UUID.randomUUID(); Settings settings = new Settings(); settings.setResilient(false); - this.npc = NPCApi.plugin.createNPC(world, new Location(world, 0, 0, 0), new Equipment(), settings, uuid, null, new ArrayList<>()); + this.npc = NPCApi.plugin.createNPC(world, new Location(world, 0, 0, 0), new Equipment(), settings, uuid, null, new ArrayList<>(), new ArrayList<>(), Selector.ONE); } /** @@ -93,22 +93,6 @@ public NPC(InternalNpc npc) { this.npc = npc; } - - /** - *

Sets the location of the NPC - *

- * - * @param loc the new location for the NPC - * @return the NPC with the modified location - * @since 1.5.2-pre3 - * @deprecated see {@link #setPosition(Location)} (typo ;|) - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.8") - public NPC setPostion(@NotNull Location loc) { - return setPosition(loc); - } - /** *

Sets the location of the NPC. *

@@ -121,7 +105,6 @@ public NPC setPostion(@NotNull Location loc) { public NPC setPosition(@NotNull Location loc) { Preconditions.checkArgument(loc != null, "loc cannot be null."); npc.setSpawnLoc(loc); - npc.getSettings().setDirection(loc.getYaw()); return this; } @@ -174,27 +157,6 @@ public NPC addAction(@NotNull Action action) { return this; } - /** - *

Sets the NPC's actions to the specified Collection - *

- * - * @param actionImpls the collection of actions - * @return the NPC with the modified set of actions - * @see Action - * @since 1.5.2-pre3 - * @deprecated Use {@link #setActions(Collection)} - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.8") - public NPC setLegacyActions(Collection actionImpls) { - List actionList = new ArrayList<>(); - for (LegacyAction legacyAction : actionImpls) { - actionList.add(legacyAction.toAction()); - } - npc.setActions(actionList); - return this; - } - /** * Move the npc to the specified location. Takes into account pitch and yaw * @@ -238,6 +200,11 @@ public void swingArm() { /** * Injects the npc into the player's connection. This should be handled by the plugin, but this is here for more control. * + *

+ * This feature bypasses injection conditions! If you would like to have the plugin reevaluate if a player should + * be injected, you can remove this NPC from their client with {@link NPC#withdraw(Player)}, and + *

+ * * @param player the player to inject * @see Player * @since 1.5.2-pre3 @@ -390,4 +357,24 @@ public Location getLocation() { public void reloadSettings() { npc.reloadSettings(); } + + /** + * The inverse operation of injecting this NPC into a player's client. + *

+ * This does **NOT** remove the NPC from the server, and the npc may be reinjected later. + *

+ * @param player the player to remove the NPC from + */ + public void withdraw(Player player) { + npc.withdraw(player); + } + + /** + * Tells this NPC's InjectionManager to inject this player again, on the next injection check. This may cause the + * NPC to twitch and flicker. + * @param player the player to mark for injection. Only this player will be affected + */ + public void markForInjection(Player player){ + npc.getInjectionManager().markForInjection(player.getUniqueId()); + } } diff --git a/core/src/main/java/dev/foxikle/customnpcs/api/NPCApi.java b/core/src/main/java/dev/foxikle/customnpcs/api/NPCApi.java index 8f104be1..183ebe5f 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/api/NPCApi.java +++ b/core/src/main/java/dev/foxikle/customnpcs/api/NPCApi.java @@ -25,7 +25,6 @@ import dev.foxikle.customnpcs.internal.CustomNPCs; import lombok.experimental.UtilityClass; import org.bukkit.plugin.java.JavaPlugin; -import org.jetbrains.annotations.ApiStatus; import java.util.UUID; @@ -39,16 +38,6 @@ public class NPCApi { */ static final CustomNPCs plugin = JavaPlugin.getPlugin(CustomNPCs.class); - /** - * Initiailizes the API - * - * @deprecated since it's no longer necessary - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.8") - public static void initialize() { - } - /** * Gets the NPC object by ID. * diff --git a/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInjectEvent.java b/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInjectEvent.java index 6589a57d..80db4ebe 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInjectEvent.java +++ b/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInjectEvent.java @@ -22,9 +22,14 @@ package dev.foxikle.customnpcs.api.events; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import lombok.Getter; import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; + +import java.util.HashMap; +import java.util.Map; /** * A class representing an event called upon the injection of an NPC. @@ -34,16 +39,33 @@ public class NpcInjectEvent extends NpcEvent { private final double distanceSquared; + private final Map conditions; /** * The constructor for the event * * @param player the player associated with the action * @param npc the npc involved in the event + * @deprecated Use the constructor with the conditions */ + @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "1.9.0") public NpcInjectEvent(Player player, InternalNpc npc, double distanceSquared) { super(player, npc, true); this.distanceSquared = distanceSquared; + conditions = new HashMap<>(); + } + + /** + * The constructor for the event + * + * @param player the player associated with the action + * @param npc the npc involved in the event + */ + public NpcInjectEvent(Player player, InternalNpc npc, double distanceSquared, Map conditions) { + super(player, npc, true); + this.distanceSquared = distanceSquared; + this.conditions = conditions; } /** diff --git a/core/src/main/java/dev/foxikle/customnpcs/conditions/Comparator.java b/core/src/main/java/dev/foxikle/customnpcs/conditions/Comparator.java new file mode 100644 index 00000000..b03ee3ea --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/conditions/Comparator.java @@ -0,0 +1,53 @@ +package dev.foxikle.customnpcs.conditions; + +import lombok.Getter; + +/** + * A list of comparators used to compare the values and target values of conditions + */ +@Getter +public enum Comparator { + /** + * Represents the value being equal to the target value + */ + EQUAL_TO(true, "customnpcs.conditions.equal_to"), + + /** + * Represents the value being unequal to the target value + */ + NOT_EQUAL_TO(true, "customnpcs.conditions.not_equal_to"), + + /** + * Represents the value being less than the target value + */ + LESS_THAN(false, "customnpcs.conditions.less_than"), + + /** + * Represents the value being greater than the target value + */ + GREATER_THAN(false, "customnpcs.conditions.greater_than"), + + /** + * Represents the value being less than or equal to the target value + */ + LESS_THAN_OR_EQUAL_TO(false, "customnpcs.conditions.less_than_or_equal_to"), + + /** + * Represents the value being greater than or equal to the target value + */ + GREATER_THAN_OR_EQUAL_TO(false, "customnpcs.conditions.greater_than_or_equal_to"); + + private final boolean strictlyLogical; + private final String key; + + /** + * Constructor for the Comparator + * + * @param strictlyLogical if the comparator is to only be used on logical parameters + * @param key the translation key + */ + Comparator(boolean strictlyLogical, String key) { + this.strictlyLogical = strictlyLogical; + this.key = key; + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Condition.java b/core/src/main/java/dev/foxikle/customnpcs/conditions/Condition.java similarity index 74% rename from core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Condition.java rename to core/src/main/java/dev/foxikle/customnpcs/conditions/Condition.java index 0e7b399e..48025ad8 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Condition.java +++ b/core/src/main/java/dev/foxikle/customnpcs/conditions/Condition.java @@ -20,10 +20,13 @@ * SOFTWARE. */ -package dev.foxikle.customnpcs.actions.conditions; +package dev.foxikle.customnpcs.conditions; import dev.foxikle.customnpcs.internal.CustomNPCs; +import lombok.AllArgsConstructor; import lombok.Getter; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.entity.Player; /** @@ -31,6 +34,8 @@ */ public interface Condition { + Codec CODEC = Codec.Enum(Type.class).unionType(Type::getCodec, Condition::getType); + /** * Computes the condition to determine if the action should be executed * @@ -115,74 +120,27 @@ static Condition of(String data) { */ Condition clone(); - /** - * A list of comparators used to compare the values and target values of conditions - */ - @Getter - enum Comparator { - /** - * Represents the value being equal to the target value - */ - EQUAL_TO(true, "customnpcs.conditions.equal_to"), - - /** - * Represents the value being unequal to the target value - */ - NOT_EQUAL_TO(true, "customnpcs.conditions.not_equal_to"), - - /** - * Represents the value being less than the target value - */ - LESS_THAN(false, "customnpcs.conditions.less_than"), - - /** - * Represents the value being greater than the target value - */ - GREATER_THAN(false, "customnpcs.conditions.greater_than"), - - /** - * Represents the value being less than or equal to the target value - */ - LESS_THAN_OR_EQUAL_TO(false, "customnpcs.conditions.less_than_or_equal_to"), - - /** - * Represents the value being greater than or equal to the target value - */ - GREATER_THAN_OR_EQUAL_TO(false, "customnpcs.conditions.greater_than_or_equal_to"); - - private final boolean strictlyLogical; - private final String key; - - /** - * Constructor for the Comparator - * - * @param strictlyLogical if the comparator is to only be used on logical parameters - * @param key the translation key - */ - Comparator(boolean strictlyLogical, String key) { - this.strictlyLogical = strictlyLogical; - this.key = key; - } - - } - /** * A list of comparator types */ + @AllArgsConstructor enum Type { /** * Represents a comparison between a Value and a target value that can be any numeric value. * * @see Value */ - NUMERIC, + NUMERIC(NumericCondition.CODEC), /** * Represents a comparison between a Value and a target value with a finite number of possibilities * * @see Value */ - LOGICAL + LOGICAL(LogicalCondition.CODEC); + + @Getter + private final StructCodec codec; } /** @@ -268,6 +226,13 @@ enum Value { IS_GLIDING(true, "customnpcs.conditions.is_gliding"); + /** + * -- GETTER -- + * Determines if the value is considered 'logical' + * + * @return if the value is logical + */ + @Getter private final boolean isLogical; private final String key; @@ -286,28 +251,5 @@ public String getTranslationKey() { return key; } - /** - * Determines if the value is considered 'logical' - * - * @return if the value is logical - */ - public boolean isLogical() { - return isLogical; - } - } - - /** - * Represents if how the conditions should be computed - */ - enum SelectionMode { - /** - * If ALL the conditions must be true for the action to be executed - */ - ALL, - - /** - * if at least ONE of the conditions must be met for the action to be executed - */ - ONE } } diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ConditionalTypeAdapter.java b/core/src/main/java/dev/foxikle/customnpcs/conditions/ConditionalTypeAdapter.java similarity index 93% rename from core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ConditionalTypeAdapter.java rename to core/src/main/java/dev/foxikle/customnpcs/conditions/ConditionalTypeAdapter.java index 18d61d2b..07db1069 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ConditionalTypeAdapter.java +++ b/core/src/main/java/dev/foxikle/customnpcs/conditions/ConditionalTypeAdapter.java @@ -20,14 +20,13 @@ * SOFTWARE. */ -package dev.foxikle.customnpcs.actions.conditions; +package dev.foxikle.customnpcs.conditions; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; -import dev.foxikle.customnpcs.actions.conditions.Condition.Comparator; -import dev.foxikle.customnpcs.actions.conditions.Condition.Type; -import dev.foxikle.customnpcs.actions.conditions.Condition.Value; +import dev.foxikle.customnpcs.conditions.Condition.Type; +import dev.foxikle.customnpcs.conditions.Condition.Value; import java.io.IOException; diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalCondition.java b/core/src/main/java/dev/foxikle/customnpcs/conditions/LogicalCondition.java similarity index 91% rename from core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalCondition.java rename to core/src/main/java/dev/foxikle/customnpcs/conditions/LogicalCondition.java index f5f34520..e294de82 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalCondition.java +++ b/core/src/main/java/dev/foxikle/customnpcs/conditions/LogicalCondition.java @@ -20,9 +20,11 @@ * SOFTWARE. */ -package dev.foxikle.customnpcs.actions.conditions; +package dev.foxikle.customnpcs.conditions; import dev.foxikle.customnpcs.internal.CustomNPCs; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.GameMode; import org.bukkit.entity.Player; import org.bukkit.potion.PotionEffectType; @@ -33,6 +35,14 @@ * The object representing two non-numeric values */ public class LogicalCondition implements Condition { + + public static final StructCodec CODEC = StructCodec.struct( + "comparator", Codec.Enum(Comparator.class), Condition::getComparator, + "value", Codec.Enum(Value.class), Condition::getValue, + "target", Codec.STRING, LogicalCondition::getTarget, + LogicalCondition::new + ); + private final Type type = Type.LOGICAL; private Comparator comparator; private Value value; diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericCondition.java b/core/src/main/java/dev/foxikle/customnpcs/conditions/NumericCondition.java similarity index 90% rename from core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericCondition.java rename to core/src/main/java/dev/foxikle/customnpcs/conditions/NumericCondition.java index 30e3b6b2..1100bd40 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericCondition.java +++ b/core/src/main/java/dev/foxikle/customnpcs/conditions/NumericCondition.java @@ -20,9 +20,11 @@ * SOFTWARE. */ -package dev.foxikle.customnpcs.actions.conditions; +package dev.foxikle.customnpcs.conditions; import dev.foxikle.customnpcs.internal.CustomNPCs; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.entity.Player; /** @@ -30,6 +32,13 @@ */ public class NumericCondition implements Condition { + public static final StructCodec CODEC = StructCodec.struct( + "comparator", Codec.Enum(Comparator.class), Condition::getComparator, + "value", Codec.Enum(Value.class), Condition::getValue, + "target", Codec.DOUBLE, NumericCondition::getTypedTarget, + NumericCondition::new + ); + private final Type type = Type.NUMERIC; private Comparator comparator; private Value value; @@ -40,7 +49,7 @@ public class NumericCondition implements Condition { * @param value the value to compare * @param target the target to compare to * @see Condition.Value - * @see Condition.Comparator + * @see Comparator */ public NumericCondition(Comparator comparator, Value value, double target) { this.comparator = comparator; @@ -120,7 +129,7 @@ public Type getType() { * Sets the comparator of this condition * * @param comparator the comparator to compare the value and target value - * @see Condition.Comparator + * @see Comparator */ @Override public void setComparator(Comparator comparator) { @@ -169,6 +178,10 @@ public String getTarget() { return String.valueOf(target); } + private double getTypedTarget() { + return target; + } + /** * Gets the comparator the condition uses to compare the value and target value. * @@ -188,4 +201,4 @@ public Condition clone() { return new NumericCondition(comparator, value, target); } } -} +} \ No newline at end of file diff --git a/core/src/main/java/dev/foxikle/customnpcs/conditions/Selector.java b/core/src/main/java/dev/foxikle/customnpcs/conditions/Selector.java new file mode 100644 index 00000000..041a55e6 --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/conditions/Selector.java @@ -0,0 +1,16 @@ +package dev.foxikle.customnpcs.conditions; + +/** + * Represents how the conditions should be computed + */ +public enum Selector { + /** + * If ALL the conditions must be true for the action to be executed + */ + ALL, + + /** + * if at least ONE of the conditions must be met for the action to be executed + */ + ONE +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/data/Equipment.java b/core/src/main/java/dev/foxikle/customnpcs/data/Equipment.java index e43cb6ab..b745f3dc 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/data/Equipment.java +++ b/core/src/main/java/dev/foxikle/customnpcs/data/Equipment.java @@ -22,45 +22,47 @@ package dev.foxikle.customnpcs.data; -import dev.foxikle.customnpcs.internal.utils.Msg; -import io.github.mqzen.menus.misc.itembuilder.ItemBuilder; +import dev.foxikle.customnpcs.internal.utils.Utils; +import lombok.AllArgsConstructor; import lombok.Getter; -import org.bukkit.Material; +import lombok.Setter; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; /** * The class representing the NPC's items */ @Getter +@Setter @SuppressWarnings("UnusedReturnValue") +@AllArgsConstructor public class Equipment { - private ItemStack head = new ItemStack(Material.AIR); - private ItemStack chest = new ItemStack(Material.AIR); - private ItemStack legs = new ItemStack(Material.AIR); - private ItemStack boots = new ItemStack(Material.AIR); - private ItemStack hand = new ItemStack(Material.AIR); - private ItemStack offhand = new ItemStack(Material.AIR); + public static final Codec CODEC = StructCodec.struct( + "head", Utils.ITEM_CODEC.optional(), Equipment::getHead, + "chest", Utils.ITEM_CODEC.optional(), Equipment::getChest, + "legs", Utils.ITEM_CODEC.optional(), Equipment::getLegs, + "boots", Utils.ITEM_CODEC.optional(), Equipment::getBoots, + "hand", Utils.ITEM_CODEC.optional(), Equipment::getHand, + "offhand", Utils.ITEM_CODEC.optional(), Equipment::getOffhand, + Equipment::new + ); - /** - * The constructor to create the equipment object - * - * @param head The item on the NPC's head - * @param chest The item on the NPC's chest - * @param legs The item on the NPC's legs - * @param boots The item on the NPC's feet - * @param hand The item in the NPC's hand - * @param offhand The item in the NPC's offhand - */ - public Equipment(ItemStack head, ItemStack chest, ItemStack legs, ItemStack boots, ItemStack hand, ItemStack offhand) { - this.head = head == null ? ItemBuilder.modern(Material.BEDROCK).setDisplay(Msg.format("ERROR!")).build() : head; - this.chest = chest == null ? ItemBuilder.modern(Material.BEDROCK).setDisplay(Msg.format("ERROR!")).build() : chest; - this.legs = legs == null ? ItemBuilder.modern(Material.BEDROCK).setDisplay(Msg.format("ERROR!")).build() : legs; - this.boots = boots == null ? ItemBuilder.modern(Material.BEDROCK).setDisplay(Msg.format("ERROR!")).build() : boots; - this.hand = hand == null ? ItemBuilder.modern(Material.BEDROCK).setDisplay(Msg.format("ERROR!")).build() : hand; - this.offhand = offhand == null ? ItemBuilder.modern(Material.BEDROCK).setDisplay(Msg.format("ERROR!")).build() : offhand; - } + @Nullable + private ItemStack head = null; + @Nullable + private ItemStack chest = null; + @Nullable + private ItemStack legs = null; + @Nullable + private ItemStack boots = null; + @Nullable + private ItemStack hand = null; + @Nullable + private ItemStack offhand = null; /** * A constructor to create an equipment object with air as all items @@ -75,18 +77,25 @@ public Equipment() { * @param e The entity equipment to pull items from. */ public Equipment importFromEntityEquipment(EntityEquipment e) { - this.head = e.getHelmet() != null ? e.getHelmet().clone() : new ItemStack(Material.AIR); - this.chest = e.getChestplate() != null ? e.getChestplate().clone() : new ItemStack(Material.AIR); - this.legs = e.getLeggings() != null ? e.getLeggings().clone() : new ItemStack(Material.AIR); - this.boots = e.getBoots() != null ? e.getBoots().clone() : new ItemStack(Material.AIR); - this.hand = e.getItemInMainHand().clone(); - this.offhand = e.getItemInOffHand().clone(); + this.head = e.getHelmet() == null || e.getHelmet().isEmpty() ? null : e.getHelmet().clone(); + this.chest = e.getChestplate() == null || e.getChestplate().isEmpty() ? null : e.getChestplate().clone(); + this.legs = e.getLeggings() == null || e.getLeggings().isEmpty() ? null : e.getLeggings().clone(); + this.boots = e.getBoots() == null || e.getBoots().isEmpty() ? null : e.getBoots().clone(); + this.hand = e.getItemInMainHand().isEmpty() ? null : e.getItemInMainHand().clone(); + this.offhand = e.getItemInOffHand().isEmpty() ? null : e.getItemInOffHand().clone(); return this; } @SuppressWarnings("all") public Equipment clone() { - return new Equipment(head.clone(), chest.clone(), legs.clone(), boots.clone(), hand.clone(), offhand.clone()); + return new Equipment( + head != null ? head.clone() : null, + chest != null ? chest.clone() : null, + legs != null ? legs.clone() : null, + boots != null ? boots.clone() : null, + hand != null ? hand.clone() : null, + offhand != null ? offhand.clone() : null + ); } /** @@ -95,7 +104,7 @@ public Equipment clone() { * @param itemStack the item to use * @return this, for chaining */ - public Equipment setHead(ItemStack itemStack) { + public Equipment setHead(@Nullable ItemStack itemStack) { head = itemStack; return this; } @@ -106,7 +115,7 @@ public Equipment setHead(ItemStack itemStack) { * @param itemStack the item to use * @return this, for chaining */ - public Equipment setChest(ItemStack itemStack) { + public Equipment setChest(@Nullable ItemStack itemStack) { chest = itemStack; return this; } @@ -117,7 +126,7 @@ public Equipment setChest(ItemStack itemStack) { * @param itemStack the item to use * @return this, for chaining */ - public Equipment setLegs(ItemStack itemStack) { + public Equipment setLegs(@Nullable ItemStack itemStack) { legs = itemStack; return this; } @@ -128,7 +137,7 @@ public Equipment setLegs(ItemStack itemStack) { * @param itemStack the item to use * @return this, for chaining */ - public Equipment setBoots(ItemStack itemStack) { + public Equipment setBoots(@Nullable ItemStack itemStack) { boots = itemStack; return this; } @@ -139,7 +148,7 @@ public Equipment setBoots(ItemStack itemStack) { * @param itemStack the item to use * @return this, for chaining */ - public Equipment setOffhand(ItemStack itemStack) { + public Equipment setOffhand(@Nullable ItemStack itemStack) { offhand = itemStack; return this; } @@ -150,7 +159,7 @@ public Equipment setOffhand(ItemStack itemStack) { * @param itemStack the item to use * @return this, for chaining */ - public Equipment setHand(ItemStack itemStack) { + public Equipment setHand(@Nullable ItemStack itemStack) { hand = itemStack; return this; } diff --git a/core/src/main/java/dev/foxikle/customnpcs/data/Settings.java b/core/src/main/java/dev/foxikle/customnpcs/data/Settings.java index bd55ad3c..b04f4843 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/data/Settings.java +++ b/core/src/main/java/dev/foxikle/customnpcs/data/Settings.java @@ -26,16 +26,23 @@ import dev.foxikle.customnpcs.api.NPC; import dev.foxikle.customnpcs.api.Pose; import dev.foxikle.customnpcs.internal.CustomNPCs; +import dev.foxikle.customnpcs.internal.utils.Msg; +import dev.foxikle.customnpcs.internal.utils.Utils; import lombok.AllArgsConstructor; import lombok.Getter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.minestom.server.codec.Codec; +import net.minestom.server.codec.StructCodec; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.jetbrains.annotations.ApiStatus; +import java.util.ArrayList; +import java.util.List; + /** * A class holding the data for an NPC's settings */ @@ -43,6 +50,24 @@ @SuppressWarnings({"UnusedReturnValue", "unused"}) public class Settings { + public static final Codec CODEC = StructCodec.struct( + "interactable", Codec.BOOLEAN, Settings::isInteractable, + "tunnelvision", Codec.BOOLEAN, Settings::isTunnelvision, + "resilient", Codec.BOOLEAN, Settings::isResilient, + "interpolationDuration", Codec.INT.optional(CustomNPCs.INTERPOLATION_DURATION), + Settings::getInterpolationDuration, + "value", Codec.STRING, Settings::getValue, + "signature", Codec.STRING, Settings::getSignature, + "holograms", Codec.STRING.list(), Settings::getRawHolograms, + "skinName", Codec.STRING, Settings::getSkinName, + "hideClickableHologram", Codec.BOOLEAN, Settings::isHideClickableHologram, + "customInteractionHologram", Codec.STRING.optional(""), Settings::getCustomInteractableHologram, + "pose", Codec.Enum(Pose.class), Settings::getPose, + "hologramBackground", Utils.COLOR_CODEC.optional(), Settings::getHologramBackground, + "hideHologrambackground", Codec.BOOLEAN, Settings::isHideBackgroundHologram, + "upsideDown", Codec.BOOLEAN, Settings::isUpsideDown, + Settings::new + ); @Getter boolean interactable = false; @@ -50,17 +75,13 @@ public class Settings { boolean tunnelvision = false; @Getter boolean resilient = true; - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.8") - @Getter - double direction = 180; @Getter int interpolationDuration = CustomNPCs.INTERPOLATION_DURATION; @Getter String value = ""; @Getter String signature = ""; - String[] holograms = new String[]{"An Unnamed NPC"}; + List holograms = Utils.list("An Unnamed NPC"); @Getter String skinName = "not set"; @Getter @@ -76,65 +97,6 @@ public class Settings { @Getter boolean upsideDown = false; - /** - * Creates a settings object with the specified settings - * - * @param interactable If the npc has actions to execute - * @param tunnelvision If the npc will look at players - * @param resilient If the npc will persist on restarts - * @param direction The direction to look - * @param value The value of the npc's skin - * @param signature The signature of the npc's skin - * @param skinName The name of the skin as it is referenced in the Menu - * @param name The name of NPC formatted in SERIALIZED minimessage format - * @param customInteractableHologram The custom hologram - * @param hideClickableHologram If the NPC's Clickable hologram should be hidden - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public Settings(boolean interactable, boolean tunnelvision, boolean resilient, double direction, String value, String signature, String skinName, String name, String customInteractableHologram, boolean hideClickableHologram) { - this.interactable = interactable; - this.tunnelvision = tunnelvision; - this.resilient = resilient; - this.direction = direction; - this.value = value; - this.signature = signature; - this.skinName = skinName; - this.holograms = new String[]{name}; - this.hideClickableHologram = hideClickableHologram; - this.customInteractableHologram = customInteractableHologram; - } - - /** - * Creates a settings object with the specified settings - * - * @param interactable If the npc has actions to execute - * @param tunnelvision If the npc will look at players - * @param resilient If the npc will persist on restarts - * @param direction The direction to look - * @param value The value of the npc's skin - * @param signature The signature of the npc's skin - * @param skinName The name of the skin as it is referenced in the Menu - * @param name The name of NPC formatted in SERIALIZED minimessage format - * @param customInteractableHologram The custom hologram - * @param hideClickableHologram If the NPC's Clickable hologram should be hidden - * @param interpolationDuration How long to interpolate the teleportation of the NPC and its nametags - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public Settings(boolean interactable, boolean tunnelvision, boolean resilient, double direction, String value, String signature, String skinName, String name, String customInteractableHologram, boolean hideClickableHologram, int interpolationDuration) { - this.interactable = interactable; - this.tunnelvision = tunnelvision; - this.resilient = resilient; - this.direction = direction; - this.value = value; - this.signature = signature; - this.skinName = skinName; - this.holograms = new String[]{name}; - this.hideClickableHologram = hideClickableHologram; - this.customInteractableHologram = customInteractableHologram; - this.interpolationDuration = interpolationDuration; - } /** * Creates a settings object with the specified settings @@ -142,51 +104,21 @@ public Settings(boolean interactable, boolean tunnelvision, boolean resilient, d * @param interactable If the npc has actions to execute * @param tunnelvision If the npc will look at players * @param resilient If the npc will persist on restarts - * @param direction The direction to look * @param value The value of the npc's skin * @param signature The signature of the npc's skin * @param skinName The name of the skin as it is referenced in the Menu - * @param holograms The lines of the NPC's hologram, formatted in SERIALIZED minimessage format. Index 0 corresponds to the top (first) line. + * @param holograms The lines of the NPC's hologram, formatted in SERIALIZED minimessage format. + * Index 0 corresponds to the top (first) line. * @param customInteractableHologram The custom hologram * @param hideClickableHologram If the NPC's Clickable hologram should be hidden * @param pose The Pose of the NPC */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public Settings(boolean interactable, boolean tunnelvision, boolean resilient, double direction, String value, String signature, String skinName, String[] holograms, String customInteractableHologram, boolean hideClickableHologram, Pose pose) { + public Settings(boolean interactable, boolean tunnelvision, boolean resilient, String value, String signature, + String skinName, List holograms, String customInteractableHologram, + boolean hideClickableHologram, Pose pose, boolean upsideDown) { this.interactable = interactable; this.tunnelvision = tunnelvision; this.resilient = resilient; - this.direction = direction; - this.value = value; - this.signature = signature; - this.skinName = skinName; - this.holograms = holograms; - this.hideClickableHologram = hideClickableHologram; - this.customInteractableHologram = customInteractableHologram; - this.pose = pose; - } - - /** - * Creates a settings object with the specified settings - * - * @param interactable If the npc has actions to execute - * @param tunnelvision If the npc will look at players - * @param resilient If the npc will persist on restarts - * @param direction The direction to look - * @param value The value of the npc's skin - * @param signature The signature of the npc's skin - * @param skinName The name of the skin as it is referenced in the Menu - * @param holograms The lines of the NPC's hologram, formatted in SERIALIZED minimessage format. Index 0 corresponds to the top (first) line. - * @param customInteractableHologram The custom hologram - * @param hideClickableHologram If the NPC's Clickable hologram should be hidden - * @param pose The Pose of the NPC - */ - public Settings(boolean interactable, boolean tunnelvision, boolean resilient, double direction, String value, String signature, String skinName, String[] holograms, String customInteractableHologram, boolean hideClickableHologram, Pose pose, boolean upsideDown) { - this.interactable = interactable; - this.tunnelvision = tunnelvision; - this.resilient = resilient; - this.direction = direction; this.value = value; this.signature = signature; this.skinName = skinName; @@ -203,20 +135,21 @@ public Settings(boolean interactable, boolean tunnelvision, boolean resilient, d * @param interactable If the npc has actions to execute * @param tunnelvision If the npc will look at players * @param resilient If the npc will persist on restarts - * @param direction The direction to look * @param value The value of the npc's skin * @param signature The signature of the npc's skin * @param skinName The name of the skin as it is referenced in the Menu - * @param holograms The lines of the NPC's hologram, formatted in SERIALIZED minimessage format. Index 0 corresponds to the top (first) line. + * @param holograms The lines of the NPC's hologram, formatted in SERIALIZED minimessage format. + * Index 0 corresponds to the top (first) line. * @param customInteractableHologram The custom hologram * @param hideClickableHologram If the NPC's Clickable hologram should be hidden * @param interpolationDuration How long to interpolate the teleportation of the NPC and its nametags */ - public Settings(boolean interactable, boolean tunnelvision, boolean resilient, double direction, String value, String signature, String skinName, String[] holograms, String customInteractableHologram, boolean hideClickableHologram, int interpolationDuration) { + public Settings(boolean interactable, boolean tunnelvision, boolean resilient, String value, String signature, + String skinName, List holograms, String customInteractableHologram, + boolean hideClickableHologram, int interpolationDuration) { this.interactable = interactable; this.tunnelvision = tunnelvision; this.resilient = resilient; - this.direction = direction; this.value = value; this.signature = signature; this.skinName = skinName; @@ -276,17 +209,6 @@ public Settings setResilient(boolean resilient) { return this; } - /** - * Deprecated: - use {@link NPC#setFacing(float, float)}, {@link NPC#lookAt(Entity, boolean)}, or - * {@link NPC#lookAt(Location)} instead. - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.8") - public Settings setDirection(double direction) { - this.direction = direction; - return this; - } - /** * Sets the ticks this NPC should be interpolated for on move. This defaults to the value defined in the config, * but can be overridden for this NPC directly. The interpolation makes the nametags move more smoothly with the @@ -374,93 +296,47 @@ public Settings setUpsideDown(boolean upsideDown) { * * @return the NPC's holograms, as components */ - public Component[] getHolograms() { - Component[] components = new Component[holograms.length]; - for (int i = 0; i < holograms.length; i++) { - components[i] = MiniMessage.miniMessage().deserialize(holograms[i]); + public List getHolograms() { + List components = new ArrayList<>(); + for (String hologram : holograms) { + components.add(Msg.format(hologram)); } return components; } /** - * Sets the NPC's holograms. These components will be serialized into strings following the MiniMessage format for storage. Index 0 corresponds to the top (first) line. + * Sets the NPC's holograms. These components will be serialized into strings following the MiniMessage format + * for storage. Index 0 corresponds to the top (first) line. * * @param holograms the collection of components */ public Settings setHolograms(Component... holograms) { - this.holograms = new String[holograms.length]; - for (int i = 0; i < holograms.length; i++) { - this.holograms[i] = MiniMessage.miniMessage().serialize(holograms[i]); + this.holograms = new ArrayList<>(); + for (Component hologram : holograms) { + this.holograms.add(Msg.minimessage(hologram)); } + return this; } /** - * Gets the contents of NPC's holograms, in serialized MiniMessage format. Index 0 corresponds to the top (first) line. + * Gets the contents of NPC's holograms, in serialized MiniMessage format. Index 0 corresponds to the top (first) + * line. * * @return the NPC's holograms, as serialized MiniMessage strings */ - public String[] getRawHolograms() { + public List getRawHolograms() { return holograms; } /** - * Sets the NPC's holograms. The plugin will parse these strings into components following the MiniMessage format. Index 0 corresponds to the top (first) line. + * Sets the NPC's holograms. The plugin will parse these strings into components following the MiniMessage format + * . Index 0 corresponds to the top (first) line. * * @param holograms the collection of strings, optionally in minimessage format */ public Settings setRawHolograms(String... holograms) { - this.holograms = holograms; - return this; - } - - /** - * Gets the NPC's name in serialized minimessage format. Since 1.7.5 onward, this method returns EITHER: the first (top) hologram OR the default, an empty string. - * - * @return the NPC's serialized name - * @deprecated NPCs no longer have "names" per se, but rather a collection of name lines. - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public String getName() { - if (holograms.length > 0) { - return holograms[0]; - } - return ""; - } - - /** - * Sets the top (first) line of the NPC's hologram - * - * @param name as a component - * @deprecated As of 1.7.5, NPCs no longer have "names" per se, but rather a collection of name lines. This method sets the first (top) hologram. - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public Settings setName(Component name) { - String serialized = MiniMessage.miniMessage().serialize(name); - if (holograms.length > 0) { - holograms[0] = serialized; - return this; - } - holograms = new String[]{serialized}; - return this; - } - - /** - * Sets the top (first) line of the NPC's hologram - * - * @param name as a string, serialized in minimessage format - * @deprecated As of 1.7.5, NPCs no longer have "names" per se, but rather a collection of name lines. This method sets the first (top) hologram. - */ - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public Settings setName(String name) { - if (holograms.length > 0) { - holograms[0] = name; - return this; - } - holograms = new String[]{name}; + this.holograms = Utils.list(holograms); return this; } @@ -498,6 +374,7 @@ public Settings setSkin(Player player) { @SuppressWarnings("all") public Settings clone() { - return new Settings(interactable, tunnelvision, resilient, direction, value, signature, skinName, holograms, customInteractableHologram, hideClickableHologram, pose); + return new Settings(interactable, tunnelvision, resilient, value, signature, skinName, holograms, + customInteractableHologram, hideClickableHologram, pose, upsideDown); } } diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/CustomNPCs.java b/core/src/main/java/dev/foxikle/customnpcs/internal/CustomNPCs.java index d94e5901..3b812e6a 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/CustomNPCs.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/CustomNPCs.java @@ -28,17 +28,17 @@ import com.google.gson.GsonBuilder; import com.mojang.brigadier.CommandDispatcher; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.LegacyAction; -import dev.foxikle.customnpcs.actions.conditions.ActionAdapter; -import dev.foxikle.customnpcs.actions.conditions.Condition; -import dev.foxikle.customnpcs.actions.conditions.ConditionalTypeAdapter; import dev.foxikle.customnpcs.actions.defaultImpl.*; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.ConditionalTypeAdapter; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.commands.NpcCommandRegistrar; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.listeners.Listeners; import dev.foxikle.customnpcs.internal.menu.*; +import dev.foxikle.customnpcs.internal.storage.StorageManager; import dev.foxikle.customnpcs.internal.translations.Translations; import dev.foxikle.customnpcs.internal.utils.ActionRegistry; import dev.foxikle.customnpcs.internal.utils.AutoUpdater; @@ -82,6 +82,7 @@ public final class CustomNPCs extends JavaPlugin implements PluginMessageListene public static final ActionRegistry ACTION_REGISTRY = new ActionRegistry(); public static int INTERPOLATION_DURATION; + /** * Singleton for the NPCBuilder */ @@ -157,7 +158,7 @@ public final class CustomNPCs extends JavaPlugin implements PluginMessageListene public MiniMessage miniMessage = MiniMessage.miniMessage(); Listeners listeners; @Getter - private FileManager fileManager; + private StorageManager storageManager; /** * Singleton for menu utilities */ @@ -210,7 +211,7 @@ public void onEnable() { String s = translateVersion(); try { - getLogger().info("Loading class: " + String.format(NPC_CLASS, s)); + getLogger().info("Loading NPC class: " + String.format(NPC_CLASS, s)); getClassLoader().loadClass(String.format(NPC_CLASS, s)); } catch (ClassNotFoundException e) { getLogger().log(Level.SEVERE, "Failed to load NPC class for server version " + s + "!", e); @@ -225,39 +226,28 @@ public void onEnable() { gson = new GsonBuilder() .registerTypeAdapter(Condition.class, new ConditionalTypeAdapter()) - .registerTypeAdapter(LegacyAction.class, new ActionAdapter()) .create(); - this.fileManager = new FileManager(this); + this.storageManager = new StorageManager(this); this.mu = new MenuUtils(this); this.updater = new AutoUpdater(this); update = updater.checkForUpdates(); - if (!fileManager.createFiles()) { - throw new RuntimeException("Failed to create files"); - } - getLogger().info("Loading action registry..."); - ACTION_REGISTRY.register("ActionBar", ActionBar.class, ActionBar::creationButton); - ACTION_REGISTRY.register("DisplayTitle", DisplayTitle.class, DisplayTitle::creationButton); - ACTION_REGISTRY.register("GiveEffect", GiveEffect.class, GiveEffect::creationButton); - ACTION_REGISTRY.register("GiveXP", GiveXP.class, GiveXP::creationButton); - ACTION_REGISTRY.register("PlaySound", PlaySound.class, PlaySound::creationButton); - ACTION_REGISTRY.register("RemoveEffect", RemoveEffect.class, RemoveEffect::creationButton); - ACTION_REGISTRY.register("RemoveXP", RemoveXP.class, RemoveXP::creationButton); - ACTION_REGISTRY.register("RunCommand", RunCommand.class, RunCommand::creationButton); - ACTION_REGISTRY.register("SendMessage", SendMessage.class, SendMessage::creationButton); - ACTION_REGISTRY.register("SendServer", SendServer.class, SendServer::creationButton, true, false, true); - ACTION_REGISTRY.register("Teleport", Teleport.class, Teleport::creationButton); - ACTION_REGISTRY.register("FollowPresetPath", FollowPresetPathAction.class, - FollowPresetPathAction::creationButton); - - try { - this.getLogger().info("Loading NPCs!"); - for (UUID uuid : fileManager.getValidNPCs()) { - fileManager.loadNPC(uuid); - } - } catch (Exception e) { - getLogger().log(Level.SEVERE, "Failed to load NPC:", e); + ACTION_REGISTRY.register(new ActionBar()); + ACTION_REGISTRY.register(new DisplayTitle()); + ACTION_REGISTRY.register(new GiveEffect()); + ACTION_REGISTRY.register(new GiveXP()); + ACTION_REGISTRY.register(new PlaySound()); + ACTION_REGISTRY.register(new RemoveEffect()); + ACTION_REGISTRY.register(new RemoveXP()); + ACTION_REGISTRY.register(new RunCommand()); + ACTION_REGISTRY.register(new SendMessage()); + ACTION_REGISTRY.register(new SendServer()); + ACTION_REGISTRY.register(new Teleport()); + ACTION_REGISTRY.register(new FollowPresetPath()); + + if (!storageManager.setup()) { + throw new RuntimeException("Failed to start storage manager"); } //generate skin menus for the supported locales @@ -402,6 +392,7 @@ public void onDisable() { commandDispatcher = null; } } + ACTION_REGISTRY.clear(); } /** @@ -463,31 +454,33 @@ public void onPluginMessageReceived(@NotNull String channel, @NotNull Player pla /** * Creates an npc * - * @param world the world - * @param location the location to spawn it - * @param equipment the equipment object representing the NPC's items - * @param settings the settings object representing the NPC's settings - * @param uuid the NPC's UUID - * @param target the NPC's target to follow - * @param actionImpls the NPC's actions + * @param world the world + * @param location the location to spawn it + * @param equipment the equipment object representing the NPC's items + * @param settings the settings object representing the NPC's settings + * @param uuid the NPC's UUID + * @param target the NPC's target to follow + * @param actions the NPC's actions * @return the created NPC * @throws RuntimeException If the reflective creation of the NPC object fails */ public InternalNpc createNPC(World world, Location location, Equipment equipment, Settings settings, UUID uuid, - @Nullable Player target, List actionImpls) { + @Nullable Player target, List actions, List conditions, + Selector injectionMode) { try { Class clazz = Class.forName(String.format(NPC_CLASS, translateVersion())); return (InternalNpc) clazz .getConstructor(this.getClass(), World.class, Location.class, Equipment.class, Settings.class, - UUID.class, Player.class, List.class) - .newInstance(this, world, location, equipment, settings, uuid, target, actionImpls); + UUID.class, Player.class, List.class, List.class, Selector.class) + .newInstance(this, world, location, equipment, settings, uuid, target, actions, conditions, + injectionMode); } catch (ReflectiveOperationException e) { getLogger().log(Level.SEVERE, ("An error occurred whilst creating the NPC '{name}! This is most likely a " + - "configuration issue.").replace("{name}", settings.getName()), e); + "configuration issue.").replace("{name}", settings.getRawHolograms().getFirst()), e); throw new RuntimeException(e); } catch (Exception e) { getLogger().log(Level.SEVERE, "An error occurred whilst creating the NPC '{name}!".replace("{name}", - settings.getName()), e); + settings.getRawHolograms().getFirst()), e); throw new RuntimeException(e); } } diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/FileManager.java b/core/src/main/java/dev/foxikle/customnpcs/internal/FileManager.java deleted file mode 100644 index 47b6d930..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/FileManager.java +++ /dev/null @@ -1,749 +0,0 @@ -/* - * Copyright (c) 2024-2026. Foxikle - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.foxikle.customnpcs.internal; - -import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.ActionType; -import dev.foxikle.customnpcs.actions.LegacyAction; -import dev.foxikle.customnpcs.actions.conditions.Condition; -import dev.foxikle.customnpcs.api.Pose; -import dev.foxikle.customnpcs.data.Equipment; -import dev.foxikle.customnpcs.data.Settings; -import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; -import dev.foxikle.customnpcs.internal.utils.SkinUtils; -import dev.foxikle.customnpcs.internal.utils.Utils; -import lombok.Getter; -import lombok.SneakyThrows; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.time.Instant; -import java.util.*; -import java.util.logging.Level; - -/** - * The class that deals with all file related things - */ -@SuppressWarnings("unused") -public class FileManager { - - /** - * The config file version - */ - public static final int CONFIG_FILE_VERSION = 6; - /** - * The file version of the npcs.yml file - */ - public static final double NPC_FILE_VERSION = 1.9; - public static File PARENT_DIRECTORY = new File("plugins/CustomNPCs/"); - @Getter - private final Map brokenNPCs = new HashMap<>(); - @Getter - private final List validNPCs = new ArrayList<>(); - private final CustomNPCs plugin; - - /** - *

Gets the file manager object. - *

- * - * @param plugin The instance of the Main class - */ - public FileManager(CustomNPCs plugin) { - this.plugin = plugin; - } - - /** - *

Creates the files the plugin needs to run - *

- * - * @return if creating the files was successful - */ - public boolean createFiles() { - if (!new File(PARENT_DIRECTORY, "/npcs.yml").exists()) { - plugin.saveResource("npcs.yml", false); - } - if (!new File(PARENT_DIRECTORY, "config.yml").exists()) { - plugin.saveResource("config.yml", false); - return true; - } - // config - { - File file = new File(PARENT_DIRECTORY, "config.yml"); - YamlConfiguration yml = YamlConfiguration.loadConfiguration(file); - - if (!yml.contains("Skins")) { - BackupResult br = createBackup(file); - if (br.success) { - plugin.getLogger().warning("The config is irreparably damaged! Resetting config. Your old config was saved to the file \"" + br.filePath.toString() + "\""); - plugin.saveResource("config.yml", true); - } - } - int version = yml.getInt("CONFIG_VERSION"); - - if (version < 6) { - BackupResult br = createBackup(file); - if (!br.success()) { - throw new RuntimeException("Failed to create a backup of the config file before updating it!"); - } else { - plugin.getLogger().info("Created backup of config.yml before updating it! A copy of your existing config was saved to " + br.filePath().toString()); - } - } - - if (version == 0) { // doesn't exist? - plugin.getLogger().log(Level.WARNING, String.format("Outdated Config version! Converting config (%d -> %d).", version, 1)); - yml.set("CONFIG_VERSION", 1); - yml.setComments("CONFIG_VERSION", List.of(" DO NOT, under ANY circumstances modify the 'CONFIG_VERSION' field. Doing so can cause catastrophic data loss.", "")); - yml.set("ClickText", "&e&lCLICK"); - yml.setComments("ClickText", List.of("ClickText -> The hologram displayed above the NPC if it is interactable", " NOTE: Due to Minecraft limitations, this cannot be more than 16 characters INCLUDING color and format codes.", " (But not the &)", "")); - yml.set("DisplayClickText", true); - yml.setComments("DisplayClickText", List.of(" DisplayClickText -> Should the plugin display a hologram above the NPC's head if it is interactable?", "")); - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - if (version < 2) { // prior to 1.4-pre2 - plugin.getLogger().log(Level.WARNING, String.format("Outdated Config version! Converting config (%d -> %d).", version, 2)); - yml.set("CONFIG_VERSION", 2); - yml.set("AlertOnUpdate", true); - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - if (version < 3) { // prior to 1.5.2-pre1 - plugin.getLogger().log(Level.WARNING, String.format("Outdated Config version! Converting config (%d -> %d).", version, 3)); - yml.set("CONFIG_VERSION", 3); - yml.set("ClickText", plugin.getMiniMessage().serialize(LegacyComponentSerializer.legacyAmpersand().deserialize(Objects.requireNonNull(yml.getString("ClickText"))))); - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - if (version < 4) { //prior to 1.6-pre2 - plugin.getLogger().log(Level.WARNING, String.format("Outdated Config version! Converting config (%d -> %d).", version, 4)); - yml.set("CONFIG_VERSION", 4); - yml.set("DisableCollisions", true); - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - if (version < 5) { - plugin.getLogger().log(Level.WARNING, String.format("Outdated Config version! Converting config (%d -> %d).", version, 5)); - yml.set("CONFIG_VERSION", 5); - yml.set("NameReferenceMessages", true); - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - if (version < 6) { - plugin.getLogger().log(Level.WARNING, String.format("Outdated Config version! Converting config (%d -> %d).", version, 6)); - yml.set("CONFIG_VERSION", 6); - yml.set("InjectionDistance", 48); - yml.set("InjectionInterval", 10); - yml.set("HologramUpdateInterval", 200); - yml.set("LookInterval", 5); - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - if (version < 7) { - plugin.getLogger().log(Level.WARNING, String.format("Outdated Config version! Converting config (%d -> %d).", version, 7)); - yml.set("CONFIG_VERSION", 7); - yml.set("DefaultInterpolationDuration", 5); - yml.setComments("DefaultInterpolationDuration", List.of("DefaultInterpolationDuration -> How long should moving NPCs interpolate their Nametags moving?")); - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - if (version < 8) { - plugin.getLogger().log(Level.WARNING, String.format("Outdated Config version! Converting config (%d -> %d).", version, 8)); - yml.set("CONFIG_VERSION", 8); - ConfigurationSection section = yml.createSection("MineSkin"); - yml.setComments("MineSkin", List.of( - " ############################", - " # Skin API #", - " ############################", - "This plugin uses Mineskin.org's free skin api to generate skins from urls and player names. CustomNPCs comes with an api", - "key embedded, but the same key is used by every other person using the plugin, so it will likely be reaching the rate limit", - "nearly constantly. To combat this, you can use your own API key. You can get one here: https://account.mineskin.org/keys/" - )); - section.set("ApiKey", ""); - section.setInlineComments("ApiKey", List.of("Put your api key here, if desired")); - section.set("ApiUrl", ""); - section.setInlineComments("ApiUrl", List.of("Alternatively you can specify a proxied host to use instead: https://docs.mineskin.org/docs/guides/api-best-practises#use-a-proxy-server")); - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - SkinUtils.setup(yml.getString("MineSkin.ApiKey"), yml.getString("MineSkin.ApiUrl")); - } - - // npcs - { - boolean changed = false; - File file = new File(PARENT_DIRECTORY, "npcs.yml"); - - YamlConfiguration yml = YamlConfiguration.loadConfiguration(file); - - - String version = yml.getString("version"); - - - if (version == null) { // Config is from before 1.3-pre4 - plugin.getLogger().warning("Old NPC file version found! Bumping version! (unknown version -> 1.3)"); - BackupResult br = createBackup(file); - if (!br.success) { - plugin.getLogger().warning("Could not create backup before updating npcs.yml!"); - return false; - } - yml.set("version", "1.3"); - version = "1.3"; - - plugin.getLogger().warning("Adding delay to old actions."); - for (UUID u : getNPCIds()) { - ConfigurationSection s = yml.getConfigurationSection(u.toString()); - assert s != null; - List strings = s.getStringList("actions"); - List convertedActions = new ArrayList<>(); - for (String string : strings) { - List split = Utils.list(string.split("%::%")); - String sub = split.get(0); - split.remove(0); - int delay = 0; - LegacyAction actionImpl = new LegacyAction(ActionType.valueOf(sub), split, delay, Condition.SelectionMode.ONE, new ArrayList<>()); - convertedActions.add(actionImpl.toJson()); - } - s.set("actions", convertedActions); - } - // save after adding actions - try { - yml.save(file); - } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "An error occurred saving the npcs.yml file after saving a list of converted actions. Please report the following stacktrace to Foxikle.", e); - } - } - - if (version.equalsIgnoreCase("1.3")) { - plugin.getLogger().warning("Old NPC file version found! Bumping version! (1.3-> 1.4)"); - BackupResult br = createBackup(file); - if (!br.success) { - plugin.getLogger().warning("Could not create backup before updating npcs.yml!"); - return false; - } - yml.set("version", "1.4"); - version = "1.4"; - - Set npcs = yml.getKeys(false); - for (String npc : npcs) { - if (npc.equals("version")) continue; // it's a key - ConfigurationSection section = yml.getConfigurationSection(npc); - - plugin.getLogger().warning("Old Actions found. Converting to json."); - List legacyActions = section.getStringList("actions"); - List newActions = new ArrayList<>(); - legacyActions.forEach(s -> { - if (s != null) { - LegacyAction a = LegacyAction.of(s); // going to be converted the old way - if (a != null) { - newActions.add(a.toJson()); - } - } - }); - section.set("actions", newActions); - try { - yml.save(file); - } catch (IOException e) { - plugin.getLogger().severe("An error occurred whilst saving the converted actions. Please report the following stacktrace to Foxikle. \n" + Arrays.toString(e.getStackTrace())); - } - } - } - - if (version.equalsIgnoreCase("1.4")) { - plugin.getLogger().warning("Old NPC file version found! Bumping version! (1.4 -> 1.5)"); - BackupResult br = createBackup(file); - if (!br.success) { - plugin.getLogger().warning("Could not create backup before updating npcs.yml!"); - return false; - } - - yml.set("version", "1.5"); - version = "1.5"; - - Set npcs = yml.getKeys(false); - for (String npc : npcs) { - if (npc.equals("version")) continue; // it's a key - ConfigurationSection section = yml.getConfigurationSection(npc); - - section.set("tunnelvision", false); - try { - yml.save(file); - } catch (IOException e) { - plugin.getLogger().severe("An error occurred whilst saving the tunelvision status to the config. Please report the following stacktrace to Foxikle. \n" + Arrays.toString(e.getStackTrace())); - } - } - } - - if (version.equalsIgnoreCase("1.5")) { - plugin.getLogger().warning("Old NPC file version found! Bumping version! (1.5-> 1.6)"); - BackupResult br = createBackup(file); - if (!br.success) { - plugin.getLogger().warning("Could not create backup before updating npcs.yml!"); - return false; - } - - yml.set("version", "1.6"); - version = "1.6"; - Set npcs = yml.getKeys(false); - for (String npc : npcs) { - if (npc.equals("version")) continue; // it's a key - ConfigurationSection section = yml.getConfigurationSection(npc); - - section.set("customHologram", false); - section.set("hideInteractableHologram", ""); - try { - yml.save(file); - } catch (IOException e) { - plugin.getLogger().severe("An error occurred whilst saving the tunelvision status to the config. Please report the following stacktrace to Foxikle. \n" + Arrays.toString(e.getStackTrace())); - } - } - } - - if (version.equals("1.6")) { - plugin.getLogger().warning("Old NPC file version found! Bumping version! (1.6 -> 1.7)"); - BackupResult br = createBackup(file); - if (!br.success) { - plugin.getLogger().warning("Could not create backup before updating npcs.yml!"); - return false; - } - yml.set("version", "1.7"); - version = "1.7"; - - Set npcs = yml.getKeys(false); - for (String npc : npcs) { - if (npc.equals("version")) continue; // its a key - ConfigurationSection section = yml.getConfigurationSection(npc); - - // convert actions to new format - List actionStrs = section.getStringList("actions"); - List list = new ArrayList<>(); - for (String actionStr : actionStrs) { - LegacyAction a = LegacyAction.of(actionStr); - if (a == null) { - plugin.getLogger().warning("Found an invalid action in the config. Please report the following action string to Foxikle. \n" + actionStr); - continue; - } - if (a.getActionType() == ActionType.TOGGLE_FOLLOWING) { - plugin.getLogger().warning("Found an action of the type `TOGGLE_FOLLOWING`. This action has been removed in 1.7."); - continue; - } - - list.add(a.toAction()); - } - - List newActions = new ArrayList<>(); - - for (Action a : list) { - if (a == null) { - plugin.getLogger().warning("Found an invalid action in the config."); - continue; - } - newActions.add(a.serialize()); - } - - section.set("actions", newActions); - } - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - // After 1.7-pre6 - if (version.equals("1.7")) { - plugin.getLogger().warning("Old NPC file version found! Bumping version! (1.7 -> 1.8)"); - BackupResult br = createBackup(file); - if (!br.success) { - plugin.getLogger().warning("Could not create backup before updating npcs.yml!"); - return false; - } - yml.set("version", "1.8"); - - Set npcs = yml.getKeys(false); - for (String npc : npcs) { - if (npc.equals("version")) continue; // its a key - ConfigurationSection section = yml.getConfigurationSection(npc); - - assert section != null : "Section is null -- Upgrading NPC file from 1.7 to 1.8"; - - double dir = section.getDouble("direction"); - Location loc = section.getLocation("location"); - assert loc != null : "Location is null -- Upgrading NPC file from 1.7 to 1.8"; - loc.setYaw((float) dir); - - section.set("location", loc); // update the location - section.set("direction", null); // remove the direction field - } - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - // after 1.7.5-pre2 - if (version.equals("1.8")) { - plugin.getLogger().warning("Old NPC file version found! Bumping version! (1.8 -> 1.9)"); - BackupResult br = createBackup(file); - if (!br.success) { - plugin.getLogger().warning("Could not create backup before updating npcs.yml!"); - return false; - } - yml.set("version", "1.9"); - - Set npcs = yml.getKeys(false); - for (String npc : npcs) { - if (npc.equals("version")) continue; // its a key - ConfigurationSection section = yml.getConfigurationSection(npc); - - assert section != null : "Section is null -- Upgrading NPC file from 1.8 to 1.9"; - - String[] lines = new String[1]; - lines[0] = section.getString("name"); - - section.set("lines", lines); // update the lines - section.set("name", null); // remove the old name field - section.set("pose", Pose.STANDING.name()); - } - try { - yml.save(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - // check for valid NPCs: - boolean found = false; - Set npcs = yml.getKeys(false); - for (String npc : npcs) { - if (npc.equals("version")) continue; // not an NPC uuid - ConfigurationSection section = yml.getConfigurationSection(npc); - boolean err = false; - boolean exists = false; - UUID uuid = UUID.fromString(npc); - - try { - Location sec = section.getLocation("location"); - exists = sec != null; - } catch (Exception e) { - err = true; - } - - if (err || !exists) { - found = true; - String rawName = plugin.getMiniMessage().stripTags(section.getStringList("lines").get(0)); - brokenNPCs.put(UUID.fromString(npc), rawName); - } else { - validNPCs.add(UUID.fromString(npc)); - } - } - if (found) printInvalidConfig(); - } - - - return true; - } - - /** - *

Adds an NPC to the `npcs.yml` file. - *

- * - * @param npc The NPC to store - */ - public void addNPC(InternalNpc npc) { - File file = new File("plugins/CustomNPCs/npcs.yml"); - YamlConfiguration yml = YamlConfiguration.loadConfiguration(file); - yml.createSection(npc.getUniqueID().toString()); - ConfigurationSection section = yml.getConfigurationSection(npc.getUniqueID().toString()); - - List actions = new ArrayList<>(); - npc.getActions().forEach(action -> actions.add(action.serialize())); - assert section != null; - section.addDefault("value", npc.getSettings().getValue()); - section.addDefault("signature", npc.getSettings().getSignature()); - section.addDefault("skin", npc.getSettings().getSkinName()); - section.addDefault("clickable", npc.getSettings().isInteractable()); - section.addDefault("customHologram", npc.getSettings().getCustomInteractableHologram()); - section.addDefault("hideInteractableHologram", npc.getSettings().isHideClickableHologram()); - section.addDefault("location", npc.getSpawnLoc()); - section.addDefault("actions", actions); - section.addDefault("handItem", npc.getEquipment().getHand()); - section.addDefault("offhandItem", npc.getEquipment().getOffhand()); - section.addDefault("headItem", npc.getEquipment().getHead()); - section.addDefault("chestItem", npc.getEquipment().getChest()); - section.addDefault("legsItem", npc.getEquipment().getLegs()); - section.addDefault("feetItem", npc.getEquipment().getBoots()); - section.addDefault("lines", npc.getSettings().getRawHolograms()); - section.addDefault("pose", npc.getSettings().getPose().name()); - section.addDefault("world", npc.getWorld().getName()); - section.addDefault("tunnelvision", npc.getSettings().isTunnelvision()); - section.addDefault("upsideDown", npc.getSettings().isUpsideDown()); - yml.options().copyDefaults(true); - try { - yml.save(file); - } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "An error occurred saving the npcs.yml file after creating a new section. Please report the following stacktrace to Foxikle.", e); - } - } - - /** - *

Gets the NPC of the specified UUID - *

- * - * @param uuid The NPC to load from the file - */ - public void loadNPC(UUID uuid) { - File file = new File("plugins/CustomNPCs/npcs.yml"); - YamlConfiguration yml = YamlConfiguration.loadConfiguration(file); - ConfigurationSection section = yml.getConfigurationSection(uuid.toString()); - if (section == null) throw new IllegalArgumentException("NPC uuid cannot be null."); - List actionImpls = new ArrayList<>(); - List actions; - - if (section.getConfigurationSection("actions") == null) { // meaning it does not exist - if (section.getString("command") != null) { // if there is a legacy command - Bukkit.getLogger().info("Converting legacy commands to Actions."); - String command = section.getString("command"); - assert command != null; - LegacyAction actionImpl = new LegacyAction(ActionType.RUN_COMMAND, Utils.list(command.split(" ")), 0, Condition.SelectionMode.ONE, new ArrayList<>()); - actionImpls.add(actionImpl); - section.set("actions", actionImpls); - section.set("command", null); - try { - yml.save(file); - } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "An error occurred saving the npcs.yml file after converting legacy commands to actions. Please report the following stacktrace to Foxikle.", e); - } - } - } - List rawLines = section.getStringList("lines"); - for (int i = 0; i < rawLines.size(); i++) { - String line = rawLines.get(i); - if (line.contains("§")) { - rawLines.set(i, plugin.getMiniMessage().serialize(LegacyComponentSerializer.legacyAmpersand().deserialize(Objects.requireNonNull(line)))); - - } - - try { - yml.save(file); - } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "An error occurred saving the npcs.yml file after converting legacy names to minimessage. Please report the following stacktrace to Foxikle.", e); - } - } - - String rawName = plugin.getMiniMessage().stripTags(section.getStringList("lines").get(0)); - World world; - try { - world = Bukkit.getWorld(Objects.requireNonNull(section.getString("world"))); - } catch (IllegalArgumentException ex) { - printInvalidConfig(); - brokenNPCs.put(uuid, rawName); - return; - } - - Location location; - try { - location = section.getLocation("location"); - } catch (Exception ex) { - brokenNPCs.put(uuid, rawName); - printInvalidConfig(); - return; - } - - if (world == null) { - printInvalidConfig(); - brokenNPCs.put(uuid, rawName); - return; - } - - if (location == null) { - printInvalidConfig(); - brokenNPCs.put(uuid, rawName); - return; - } - - - // use the actions freshly converted - - actions = new ArrayList<>(); - for (String s : section.getStringList("actions")) { - actions.add(Action.parse(s)); - } - - InternalNpc npc = plugin.createNPC( - world, - location, - new Equipment( - section.getItemStack("headItem"), - section.getItemStack("chestItem"), - section.getItemStack("legsItem"), - section.getItemStack("feetItem"), - section.getItemStack("handItem"), - section.getItemStack("offhandItem") - ), new Settings( - section.getBoolean("clickable"), - section.getBoolean("tunnelvision"), - true, - location.getYaw(), - section.getString("value"), - section.getString("signature"), - section.getString("skin"), - section.getStringList("lines").toArray(new String[0]), - section.getString("customHologram"), - section.getBoolean("hideInteractableHologram"), - parsePose(section.getString("pose")), - section.getBoolean("upsideDown") - ), uuid, null, actions); - if (npc != null) { - npc.createNPC(); - } else { - plugin.getLogger().severe("The NPC '{name}' could not be created!".replace("{name}", Objects.requireNonNull(section.getStringList("lines").get(0)))); - } - } - - @NotNull - private Pose parsePose(String str) { - if (str == null) return Pose.STANDING; - try { - return Pose.valueOf(str.toUpperCase()); - } catch (Exception e) { - return Pose.STANDING; - } - } - - @Nullable - public YamlConfiguration getNpcYaml() { - File file = new File(PARENT_DIRECTORY, "npcs.yml"); - return YamlConfiguration.loadConfiguration(file); - } - - @SneakyThrows - public void saveNpcFile(YamlConfiguration section) { - File file = new File(PARENT_DIRECTORY, "npcs.yml"); - section.save(file); - } - - /** - *

Gets the set of stored UUIDs. - *

- * - * @return the set of stored NPC uuids. - */ - public Set getNPCIds() { - File file = new File("plugins/CustomNPCs/npcs.yml"); - YamlConfiguration yml; - try { - yml = YamlConfiguration.loadConfiguration(file); - } catch (Exception ex) { - printInvalidConfig(); - return new HashSet<>(); - } - Set uuids = new HashSet<>(); - for (String str : yml.getKeys(false)) { - if (!str.equalsIgnoreCase("version")) - uuids.add(UUID.fromString(str)); - } - return uuids; - } - - /** - *

Removes the specified NPC from storage - *

- * - * @param uuid The NPC uuid to remove - */ - public void remove(UUID uuid) { - File file = new File("plugins/CustomNPCs/npcs.yml"); - YamlConfiguration yml = YamlConfiguration.loadConfiguration(file); - yml.set(uuid.toString(), null); - try { - yml.save(file); - } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "An error occurred saving the npcs.yml file after removing an npc. Please report the following stacktrace to Foxikle.", e); - } - } - - private BackupResult createBackup(File file) { - YamlConfiguration yml = YamlConfiguration.loadConfiguration(file); - File f = new File(PARENT_DIRECTORY, new Date().toString().replace(" ", "_").replace(":", "_") + "_backup_of_" + file.getName() + Instant.now().hashCode()); - try { - if (f.createNewFile()) { - yml.save(f); - } else { - throw new RuntimeException("A duplicate file of file '" + f.getName() + "' exists! This means the plugin attempted to back up the file '" + file.getName() + "' multiple times within this millisecond! This is a serious issue that should be reported to @foxikle on discord!"); - } - } catch (IOException e) { - plugin.getLogger().log(Level.SEVERE, "An error occurred whilst creating a backup of the file '" + file.getName() + "'", e); - return new BackupResult(null, false); - } - return new BackupResult(f.toPath(), true); - } - - private void printInvalidConfig() { - plugin.getLogger().severe(""); - plugin.getLogger().severe("+------------------------------------------------------------------------------+"); - plugin.getLogger().severe("| NPC with an invalid configuration detected! |"); - plugin.getLogger().severe("| ** THIS IS NOT AN ERROR WITH CUSTOMNPCS ** |"); - plugin.getLogger().severe("| This is most likely a configuration error as a result of |"); - plugin.getLogger().severe("| modifying the `npcs.yml` file. |"); - plugin.getLogger().severe("+------------------------------------------------------------------------------+"); - plugin.getLogger().severe(""); - } - - private record BackupResult(Path filePath, boolean success) { - } -} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/InjectionManager.java b/core/src/main/java/dev/foxikle/customnpcs/internal/InjectionManager.java index cd6a46ca..3a7f0b97 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/InjectionManager.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/InjectionManager.java @@ -45,6 +45,10 @@ public InjectionManager(CustomNPCs plugin, InternalNpc npc) { INJECTION_DISTANCE = (int) Math.pow(plugin.getConfig().getInt("InjectionDistance"), 2); } + public void markForInjection(UUID player) { + isVisible.put(player, false); + } + public void setup() { if (task != -1) shutDown(); task = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::checkForInjections, 0, plugin.getConfig().getInt("InjectionInterval")).getTaskId(); @@ -63,7 +67,9 @@ private void checkForInjections() { if (player.getWorld() != npc.getCurrentLocation().getWorld()) { if (plugin.isDebug()) { - plugin.getLogger().info(String.format("[DEBUG] Removing %s from %s's injection handler as they are in a different world.", player.getName(), npc.getSettings().getName())); + plugin.getLogger().info(String.format("[DEBUG] Removing %s from %s's injection handler as they " + + "are in a different world.", player.getName(), + npc.getSettings().getRawHolograms().getFirst())); } isVisible.remove(player.getUniqueId()); continue; @@ -72,16 +78,22 @@ private void checkForInjections() { double distance = player.getLocation().distanceSquared(npc.getCurrentLocation()); if (distance > INJECTION_DISTANCE) { if (plugin.isDebug()) { - plugin.getLogger().info(String.format("[DEBUG] Tried to inject %s with %s, but they are too far away! (Distance^2: %f )", player.getName(), npc.getSettings().getName(), distance)); + plugin.getLogger().info(String.format("[DEBUG] Tried to inject %s with %s, but they are too far " + + "away! (Distance^2: %f )", player.getName(), + npc.getSettings().getRawHolograms().getFirst(), distance)); } isVisible.put(player.getUniqueId(), false); continue; } if (distance <= INJECTION_DISTANCE && !isVisible.getOrDefault(player.getUniqueId(), false)) { - NpcInjectEvent injectEvent = new NpcInjectEvent(player, npc, distance); + + + + NpcInjectEvent injectEvent = new NpcInjectEvent(player, npc, distance, npc.evaluateInjectionConditions(player)); Bukkit.getServer().getPluginManager().callEvent(injectEvent); if (injectEvent.isCancelled()) continue; + if (!npc.passesInectionConditions(player)) continue; npc.injectPlayer(player); isVisible.put(player.getUniqueId(), true); } @@ -89,7 +101,8 @@ private void checkForInjections() { for (UUID uuid : toRemove) { if (plugin.isDebug()) { - plugin.getLogger().info(String.format("[DEBUG] Removing %s from %s's injection handler! (likley offline)", uuid.toString(), npc.getSettings().getName())); + plugin.getLogger().info(String.format("[DEBUG] Removing %s from %s's injection handler! (likley " + + "offline)", uuid.toString(), npc.getSettings().getRawHolograms().getFirst())); } isVisible.remove(uuid); } diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CommandUtils.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CommandUtils.java index a52239f0..c8e55339 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CommandUtils.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CommandUtils.java @@ -82,6 +82,9 @@ public static Component getHelpComponent(Locale p) { .append(Msg.translate(p, "customnpcs.commands.help.debug.syntax").color(NamedTextColor.GOLD).appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.help.debug.aliases")))) .append(Msg.translate(p, "customnpcs.commands.help.debug.description").color(NamedTextColor.DARK_AQUA).appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.help.debug.hover")))) .appendNewline() + .append(Msg.translate(p, "customnpcs.commands.help.movedata.syntax").color(NamedTextColor.GOLD).appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.help.movedata.aliases")))) + .append(Msg.translate(p, "customnpcs.commands.help.movedata.description").color(NamedTextColor.DARK_AQUA).appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.help.movedata.hover")))) + .appendNewline() .append(Component.text(" ", NamedTextColor.DARK_GREEN, TextDecoration.STRIKETHROUGH)); return component; } @@ -97,7 +100,7 @@ public static Component getListComponent(Locale p) { for (InternalNpc npc : plugin.getNPCs()) { if (npc.getSettings().isResilient()) { Component name = Msg.format("â—† ") - .append(plugin.getMiniMessage().deserialize(npc.getSettings().getName()).appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.manage.copy_uuid")))).clickEvent(ClickEvent.copyToClipboard(npc.getUniqueID().toString())) + .append(plugin.getMiniMessage().deserialize(npc.getSettings().getRawHolograms().getFirst()).appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.manage.copy_uuid")))).clickEvent(ClickEvent.copyToClipboard(npc.getUniqueID().toString())) .append(Msg.translate(p, "customnpcs.commands.manage.button.edit").appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.manage.button.edit.hover"))).clickEvent(ClickEvent.runCommand("/npc edit " + npc.getUniqueID()))) .append(Msg.translate(p, "customnpcs.commands.manage.button.delete").appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.manage.button.delete.hover"))).clickEvent(ClickEvent.suggestCommand("/npc delete " + npc.getUniqueID()))) .appendNewline(); @@ -140,9 +143,8 @@ public static UUID parseNpc(CommandSender source, String data) { return null; } - Locale finalLocale = locale; Set uuids = plugin.npcs.values().stream().map(npc -> { - if (plugin.getMiniMessage().stripTags(npc.getSettings().getName()).equalsIgnoreCase(data)) { + if (plugin.getMiniMessage().stripTags(npc.getSettings().getRawHolograms().getFirst()).equalsIgnoreCase(data)) { return npc.getUniqueID(); } return null; diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommandRegistrar.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommandRegistrar.java index a59e468d..210a83ff 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommandRegistrar.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommandRegistrar.java @@ -22,41 +22,49 @@ package dev.foxikle.customnpcs.internal.commands; +import com.google.gson.JsonParser; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.tree.LiteralCommandNode; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; -import dev.foxikle.customnpcs.internal.FileManager; +import dev.foxikle.customnpcs.internal.commands.enums.FixConfigWorldStrategy; import dev.foxikle.customnpcs.internal.commands.suggestion.NpcBrokenSuggester; import dev.foxikle.customnpcs.internal.commands.suggestion.NpcSuggester; import dev.foxikle.customnpcs.internal.commands.suggestion.SoundSuggester; import dev.foxikle.customnpcs.internal.commands.suggestion.WorldSuggester; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.menu.MenuUtils; +import dev.foxikle.customnpcs.internal.storage.FileStorage; +import dev.foxikle.customnpcs.internal.storage.StorableNPC; +import dev.foxikle.customnpcs.internal.storage.StorageManager; import dev.foxikle.customnpcs.internal.utils.Msg; +import dev.foxikle.customnpcs.internal.utils.Utils; import dev.foxikle.customnpcs.internal.utils.WaitingType; import io.papermc.paper.command.brigadier.CommandSourceStack; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; +import net.minestom.server.codec.Transcoder; import org.bukkit.*; import org.bukkit.command.CommandSender; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.util.RayTraceResult; import org.bukkit.util.Vector; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; +import java.io.FileInputStream; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; import static com.mojang.brigadier.arguments.StringArgumentType.word; + @SuppressWarnings("UnstableApiUsage") public class NpcCommandRegistrar { @@ -70,6 +78,7 @@ public class NpcCommandRegistrar { private static final String PERMISSION_HELP = "customnpcs.commands.help"; private static final String PERMISSION_RELOAD = "customnpcs.commands.reload"; private static final String PERMISSION_WIKI = "customnpcs.command.wiki"; + private static final String PERMISSION_MOVEDATA = "customnpcs.commands.movedata"; private static final String PERMISSION_DEBUG = "customnpcs.edit"; private static final String PERMISSION_FIXCONFIG = "customnpcs.commands.fix_config"; private static final String PERMISSION_SETSOUND = "customnpcs.edit"; @@ -100,6 +109,7 @@ public static LiteralCommandNode buildNode() { registerDebugCommand(npcNode); registerFixConfigCommand(npcNode); registerSetsoundCommand(npcNode); + registerMoveData(npcNode); return npcNode; } @@ -122,7 +132,9 @@ private static void registerCreateCommand(LiteralCommandNode new dev.foxikle.customnpcs.data.Settings(), uuid, null, - new java.util.ArrayList<>() + new ArrayList<>(), + new ArrayList<>(), + Selector.ONE ); plugin.getEditingNPCs().put(player.getUniqueId(), npc); plugin.getLotus().openMenu(player, MenuUtils.NPC_MAIN); @@ -158,7 +170,9 @@ private static void registerEditCommand(LiteralCommandNode n finalNpc.getSettings(), finalNpc.getUniqueID(), null, - finalNpc.getActions() + finalNpc.getActions(), + finalNpc.getInjectionConditions(), + finalNpc.getInjectionSelector() ); plugin.getEditingNPCs().put(player.getUniqueId(), newNpc); plugin.getLotus().openMenu(player, MenuUtils.NPC_MAIN); @@ -224,7 +238,6 @@ private static void registerCloneCommand(LiteralCommandNode InternalNpc finalNpc = plugin.getNPCByID(uuid); InternalNpc newNpc = finalNpc.clone(); newNpc.setSpawnLoc(player.getLocation()); - newNpc.getSettings().setDirection(player.getLocation().getYaw()); newNpc.createNPC(); player.sendMessage(Msg.translate(player.locale(), "customnpcs.commands.clone.success")); return 1; @@ -396,8 +409,7 @@ private static void registerDebugCommand(LiteralCommandNode } private static void registerFixConfigCommand(LiteralCommandNode npcNode) { - LiteralCommandNode fixconfigNode = LiteralArgumentBuilder.literal( - "fixconfig") + LiteralCommandNode n = LiteralArgumentBuilder.literal("fixconfig") .requires(sender -> sender.getSender().hasPermission(PERMISSION_FIXCONFIG)) .executes(context -> { CommandSender sender = context.getSource().getSender(); @@ -406,27 +418,27 @@ private static void registerFixConfigCommand(LiteralCommandNodeliteral("world") - .executes(ctx -> executeFixConfig(ctx)) + .executes(NpcCommandRegistrar::executeFixConfig) .then(RequiredArgumentBuilder.argument("world", word()) .suggests(WorldSuggester.SUGGESTIONS) - .executes(ctx -> executeFixConfig(ctx)) + .executes(NpcCommandRegistrar::executeFixConfig) .then(RequiredArgumentBuilder.argument("strategy", word()) .suggests((ctx, builder) -> { builder.suggest("NONE"); builder.suggest("SAFE_LOCATION"); return builder.buildFuture(); }) - .executes(ctx -> executeFixConfig(ctx)) + .executes(NpcCommandRegistrar::executeFixConfig) .then(RequiredArgumentBuilder.argument("target", greedyString()) .suggests(NpcBrokenSuggester.SUGGESTIONS) - .executes(ctx -> executeFixConfig(ctx)) + .executes(NpcCommandRegistrar::executeFixConfig) ) ) ) ) .build(); - npcNode.addChild(fixconfigNode); + npcNode.addChild(n); } private static void registerSetsoundCommand(LiteralCommandNode npcNode) { @@ -475,10 +487,210 @@ private static void registerSetsoundCommand(LiteralCommandNode npcNode) { + LiteralCommandNode moveDataNode = + LiteralArgumentBuilder.literal("movedata") + .requires(sender -> sender.getSender().hasPermission(PERMISSION_MOVEDATA)) + .executes(ctx -> { + ctx.getSource().getSender().sendMessage( + Msg.translate( + CommandUtils.getLocale(ctx.getSource().getSender()), + "customnpcs.commands.movedata.invalid_operation" + ) + ); + return 1; + }) + .then(RequiredArgumentBuilder.argument("operation", word()) + .suggests((ctx, builder) -> { + builder.suggest("MERGE_LOCAL"); + builder.suggest("MERGE_REMOTE"); + builder.suggest("OVERWRITE"); + return builder.buildFuture(); + }) + + // /npc movedata + .executes(ctx -> { + String operation = StringArgumentType.getString(ctx, "operation"); + if (!(operation.equalsIgnoreCase("MERGE_LOCAL") || operation.equalsIgnoreCase( + "MERGE_REMOTE") || operation.equalsIgnoreCase("OVERWRITE"))) { + ctx.getSource().getSender().sendMessage(Msg.translate( + CommandUtils.getLocale(ctx.getSource().getSender()), + "customnpcs.commands.movedata.invalid_operation")); + return 0; + } + ctx.getSource().getSender().sendMessage( + Msg.translate(CommandUtils.getLocale(ctx.getSource().getSender()), + "customnpcs.commands.movedata.need_confirm")); + + return 1; + }) + + // /npc movedata confirm + .then(RequiredArgumentBuilder.argument("operation", + greedyString()) + .executes(ctx -> executeMoveData( + ctx.getSource().getSender(), + StringArgumentType.getString(ctx, "operation"), + Utils.list(StringArgumentType.getString(ctx, "flags").toLowerCase().split(" ")) + )) + ) + ) + .build(); + + npcNode.addChild(moveDataNode); + } + + private static int executeMoveData(CommandSender source, String operation, List flags) { + if (!(operation.equalsIgnoreCase("MERGE_LOCAL") || operation.equalsIgnoreCase("MERGE_REMOTE") || operation.equalsIgnoreCase("OVERWRITE"))) { + source.sendMessage(Msg.translate( + CommandUtils.getLocale(source), + "customnpcs.commands.movedata.invalid_operation" + )); + return 0; + } + + if (!(flags.contains("--confirm"))) { + source.sendMessage(Msg.translate(CommandUtils.getLocale(source), + "customnpcs.commands.movedata.need_confirm" + )); + return 0; + } + + switch (operation.toUpperCase()) { + case "MERGE_LOCAL" -> merge(source, true); + case "MERGE_REMOTE" -> merge(source, false); + case "OVERWRITE" -> overwrite(source); + } + + return 1; + } + + private static boolean start(CommandSender source) { + source.sendMessage(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata" + + ".operation_queued")); + + if ((CustomNPCs.getInstance().getStorageManager().getStorage() instanceof FileStorage)) { + CustomNPCs.getInstance().getLogger().log(Level.WARNING, "The current file provider is already File " + + "storage!"); + source.sendMessage(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata" + + ".operation_failure")); + return true; + } + + if (!FileStorage.FILE.exists()) { + CustomNPCs.getInstance().getLogger().log(Level.SEVERE, "The local NPC data file does not exist!", + new IllegalStateException()); + source.sendMessage(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata" + + ".operation_failure")); + return true; + } + return false; + } + + private static void merge(CommandSender source, boolean discardRemote) { + CustomNPCs plugin = CustomNPCs.getInstance(); + if (start(source)) { + return; + } + + StorageManager sm = plugin.getStorageManager(); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + byte[] currentData; + try (FileInputStream fis = new FileInputStream(FileStorage.FILE)) { + currentData = fis.readAllBytes(); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "An error occurred while reading local NPC file", e); + source.sendMessage(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata" + + ".operation_failure")); + return; + } + + List local; + try { + local = StorageManager.NPCS_CODEC.decode(Transcoder.JSON, + JsonParser.parseString(new String(currentData))).orElseThrow(); + } catch (IllegalStateException e) { + throw new RuntimeException(e); + } + sm.getAllNpcs().whenComplete((remote, throwable) -> { + if (throwable != null) { + plugin.getLogger().log(Level.SEVERE, "An error occurred while fetching remote NPC data!", + throwable); + source.sendMessage(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata" + + ".operation_failure")); + return; + } + + List merged = Stream.concat(remote.stream(), local.stream()) + .collect(Collectors.toMap( + StorableNPC::getUniqueID, npc -> npc, (t, t2) -> discardRemote ? t : t2) + ).values().stream().toList(); + finalizeMove(source, plugin, sm, merged); + }); + }); + } + + private static void overwrite(CommandSender source) { + CustomNPCs plugin = CustomNPCs.getInstance(); + if (start(source)) { + return; + } + + StorageManager sm = plugin.getStorageManager(); + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + byte[] currentData; + try (FileInputStream fis = new FileInputStream(FileStorage.FILE)) { + currentData = fis.readAllBytes(); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "An error occurred while reading local NPC file", e); + source.sendMessage(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata" + + ".operation_failure")); + return; + } + + List toWrite; + try { + toWrite = StorageManager.NPCS_CODEC.decode(Transcoder.JSON, + JsonParser.parseString(new String(currentData))).orElseThrow(); + } catch (IllegalStateException e) { + throw new RuntimeException(e); + } + finalizeMove(source, plugin, sm, toWrite); + }); + } + + private static void finalizeMove(CommandSender source, CustomNPCs plugin, StorageManager sm, + List toWrite) { + Bukkit.getScheduler().runTask(plugin, () -> { + sm.resetTracked(); + + // remove the npcs + plugin.getNPCs().forEach(InternalNpc::remove); + plugin.npcs.clear(); + + for (StorableNPC npc : toWrite) { + sm.track(npc); + sm.loadStorable(npc); + } + + sm.saveNpcs().whenComplete((aBoolean, throwable1) -> { + if (throwable1 != null) { + plugin.getLogger().log(Level.SEVERE, "An error occurred while writing remote NPC data!", + throwable1); + source.sendMessage(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata" + + ".operation_failure")); + return; + } + source.sendMessage(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata" + + ".operation_success")); + }); + }); + } + private static int executeFixConfig(CommandContext context) { CommandSender sender = context.getSource().getSender(); CustomNPCs plugin = CustomNPCs.getInstance(); - FileManager fileManager = plugin.getFileManager(); + StorageManager fileManager = plugin.getStorageManager(); Locale locale = getLocale(sender); String worldArg = ""; @@ -513,8 +725,7 @@ private static int executeFixConfig(CommandContext context) World w = Bukkit.getWorld(worldArg); assert w != null; - dev.foxikle.customnpcs.internal.commands.enums.FixConfigWorldStrategy strat = - dev.foxikle.customnpcs.internal.commands.enums.FixConfigWorldStrategy.parse(strategyArg); + FixConfigWorldStrategy strat = FixConfigWorldStrategy.parse(strategyArg); if (strat == null) { sender.sendMessage("INVALID_STRATEGY"); return 0; @@ -527,36 +738,27 @@ private static int executeFixConfig(CommandContext context) if (targetArg.isEmpty() || targetArg.equalsIgnoreCase("all")) { for (UUID uuid : fileManager.getBrokenNPCs().keySet()) { - YamlConfiguration yml = fileManager.getNpcYaml(); - ConfigurationSection parent = yml.getConfigurationSection(uuid.toString()); - if (parent == null) { - nonExistentNpcs.incrementAndGet(); - continue; - } + StorableNPC npc = fileManager.getBrokenNPCs().get(uuid); - ConfigurationSection location = parent.getConfigurationSection("location"); - Location loc; + StorableNPC.StorableLocation loc = npc.getSpawnLoc(); String locString; - if (location != null) { - double x = location.getDouble("x"); - double y = location.getDouble("y"); - double z = location.getDouble("z"); - float pitch = (float) location.getDouble("pitch"); - float yaw = (float) location.getDouble("yaw"); - loc = new Location(w, x, y, z, pitch, yaw); - locString = "(" + x + "," + y + "," + z + ")"; + + if (loc != null) { + loc = loc.withWorld(w.getName()); + locString = "(" + loc.x() + "," + loc.y() + "," + loc.z() + ")"; } else { - loc = new Location(w, 0, 0, 0, 0, 0); + loc = StorableNPC.StorableLocation.convert(new Location(w, 0, 0, 0, 0, 0)); locString = "(0, 0, 0)"; plugin.getLogger().warning("Fixed an NPC whose location data was wiped by Bukkit's configuration " + "API. Its location was set to (0,0,0)"); sender.sendMessage(Msg.translate(locale, "customnpcs.commands.fix_config.bukkit_wiped_data")); } - if (strat == dev.foxikle.customnpcs.internal.commands.enums.FixConfigWorldStrategy.SAFE_LOCATION) { - if (w.getBlockAt(loc).isSolid() || w.getBlockAt(loc.add(0, 1, 0)).isSolid()) { - RayTraceResult traceResult = w.rayTraceBlocks(loc.add(0, 329 - loc.y(), 0), + Location loc2 = loc.convert(); + if (strat == FixConfigWorldStrategy.SAFE_LOCATION) { + if (w.getBlockAt(loc2).isSolid() || w.getBlockAt(loc2.add(0, 1, 0)).isSolid()) { + RayTraceResult traceResult = w.rayTraceBlocks(loc2.add(0, 329 - loc.y(), 0), new Vector(0, -1, 0), 320D, FluidCollisionMode.NEVER); if (traceResult == null) { @@ -565,19 +767,22 @@ private static int executeFixConfig(CommandContext context) failedToFix.incrementAndGet(); continue; } - loc.setY(traceResult.getHitBlock().getY() + 1); + loc2.setY(traceResult.getHitBlock().getY() + 1); } movedbyStrategy.incrementAndGet(); } - parent.set("location", loc); + npc.setSpawnLoc(StorableNPC.StorableLocation.convert(loc2)); + fileManager.getValidNPCs().add(npc.getUniqueID()); + fileManager.getBrokenNPCs().remove(npc.getUniqueID()); + fileManager.track(npc); totalFixed.incrementAndGet(); - fileManager.saveNpcFile(yml); } + fileManager.saveNpcs(); } else { UUID uuid = null; - for (Map.Entry entry : fileManager.getBrokenNPCs().entrySet()) { - if (entry.getValue().equals(targetArg)) { + for (Map.Entry entry : fileManager.getBrokenNPCs().entrySet()) { + if (entry.getValue().getUniqueID().toString().equals(targetArg)) { uuid = entry.getKey(); break; } @@ -588,34 +793,27 @@ private static int executeFixConfig(CommandContext context) return 0; } - YamlConfiguration yml = fileManager.getNpcYaml(); - ConfigurationSection parent = yml.getConfigurationSection(uuid.toString()); - if (parent == null) { - return 0; - } - ConfigurationSection location = parent.getConfigurationSection("location"); - Location loc; + StorableNPC npc = fileManager.getBrokenNPCs().get(uuid); + + StorableNPC.StorableLocation loc = npc.getSpawnLoc(); String locString; + Location loc2; - if (location != null) { - double x = location.getDouble("x"); - double y = location.getDouble("y"); - double z = location.getDouble("z"); - float pitch = (float) location.getDouble("pitch"); - float yaw = (float) location.getDouble("yaw"); - loc = new Location(w, x, y, z, pitch, yaw); - locString = "(" + x + "," + y + "," + z + ")"; + if (loc != null) { + loc = loc.withWorld(w.getName()); + locString = "(" + loc.x() + "," + loc.y() + "," + loc.z() + ")"; } else { - loc = new Location(w, 0, 0, 0, 0, 0); + loc = StorableNPC.StorableLocation.convert(new Location(w, 0, 0, 0, 0, 0)); locString = "(0, 0, 0)"; plugin.getLogger().warning("Fixed an NPC whose location data was wiped by Bukkit's configuration API." + " Its location was set to (0,0,0)"); sender.sendMessage(Msg.translate(locale, "customnpcs.commands.fix_config.bukkit_wiped_data")); } + loc2 = loc.convert(); if (strat == dev.foxikle.customnpcs.internal.commands.enums.FixConfigWorldStrategy.SAFE_LOCATION) { - if (w.getBlockAt(loc).isSolid() || w.getBlockAt(loc.add(0, 1, 0)).isSolid()) { - RayTraceResult traceResult = w.rayTraceBlocks(loc.add(0, 329 - loc.y(), 0), + if (w.getBlockAt(loc2).isSolid() || w.getBlockAt(loc2.add(0, 1, 0)).isSolid()) { + RayTraceResult traceResult = w.rayTraceBlocks(loc2.add(0, 329 - loc2.y(), 0), new Vector(0, -1, 0), 320D, FluidCollisionMode.NEVER); if (traceResult == null) { @@ -624,21 +822,24 @@ private static int executeFixConfig(CommandContext context) failedToFix.incrementAndGet(); return 0; } - loc.setY(traceResult.getHitBlock().getY() + 1); + loc2.setY(traceResult.getHitBlock().getY() + 1); } movedbyStrategy.incrementAndGet(); } - - parent.set("location", loc); + npc.setSpawnLoc(StorableNPC.StorableLocation.convert(loc2)); + fileManager.getValidNPCs().add(npc.getUniqueID()); + fileManager.getBrokenNPCs().remove(npc.getUniqueID()); + fileManager.track(npc); totalFixed.incrementAndGet(); - fileManager.saveNpcFile(yml); } + fileManager.saveNpcs(); sender.sendMessage(Msg.translate(locale, "customnpcs.commands.fix_config.report", totalFixed.get(), movedbyStrategy.get(), failedToFix.get(), nonExistentNpcs.get())); return 1; } + private static Locale getLocale(CommandSender sender) { if (sender instanceof Player player) { return player.locale(); diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcBrokenSuggester.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcBrokenSuggester.java index af31357e..ff684e09 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcBrokenSuggester.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcBrokenSuggester.java @@ -26,14 +26,16 @@ import dev.foxikle.customnpcs.internal.CustomNPCs; import io.papermc.paper.command.brigadier.CommandSourceStack; +import java.util.UUID; + public class NpcBrokenSuggester { public static final SuggestionProvider SUGGESTIONS = (context, builder) -> { CustomNPCs plugin = CustomNPCs.getInstance(); String input = builder.getRemaining().toLowerCase(); - plugin.getFileManager().getBrokenNPCs().keySet().stream() - .map(uuid -> uuid.toString()) + plugin.getStorageManager().getBrokenNPCs().keySet().stream() + .map(UUID::toString) .filter(uuid -> uuid.toLowerCase().startsWith(input)) .forEach(builder::suggest); diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcSuggester.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcSuggester.java index 80d8369c..ebc6a0a6 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcSuggester.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcSuggester.java @@ -33,7 +33,7 @@ public class NpcSuggester { String input = builder.getRemaining().toLowerCase(); plugin.npcs.values().stream() - .map(npc -> plugin.getMiniMessage().stripTags(npc.getSettings().getName())) + .map(npc -> plugin.getMiniMessage().stripTags(npc.getSettings().getRawHolograms().getFirst())) .filter(name -> name.toLowerCase().startsWith(input)) .forEach(builder::suggest); diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/interfaces/InternalNpc.java b/core/src/main/java/dev/foxikle/customnpcs/internal/interfaces/InternalNpc.java index 8c1aee2f..e9687f7d 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/interfaces/InternalNpc.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/interfaces/InternalNpc.java @@ -23,8 +23,12 @@ package dev.foxikle.customnpcs.internal.interfaces; import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; +import dev.foxikle.customnpcs.internal.CustomNPCs; +import dev.foxikle.customnpcs.internal.InjectionManager; import dev.foxikle.customnpcs.internal.LookAtAnchor; import org.bukkit.Location; import org.bukkit.Particle; @@ -37,7 +41,9 @@ import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; /** @@ -46,7 +52,6 @@ @ApiStatus.Internal public interface InternalNpc { - /** *

Sets the NPC's location and rotation *

@@ -55,6 +60,28 @@ public interface InternalNpc { */ void setPosRot(Location location); + /** + * Gets the list of conditions checked upon injection checks + * @return + */ + List getInjectionConditions(); + + /** + * Gets this NPC's InjectionManager to handle marking players for reinjection + * @return + */ + InjectionManager getInjectionManager(); + + /** + * Gets which selection mode should be used when determining if this NPC should be injectioned + * @return return the desired {@link Selector} + */ + Selector getInjectionSelector(); + + void setInjectionConditions(List conditions); + + void setInjectionSelector(Selector mode); + /** *

Creates the NPC and injects it into every player *

@@ -193,7 +220,7 @@ public interface InternalNpc { void remove(); /** - *

Moves the npc to the location + *

Moves the npc the DELTA provided by the vector. *

* * @param v The location to move to the npc at @@ -204,7 +231,9 @@ public interface InternalNpc { *

Permanently deletes an NPC. Does NOT despawn it. *

*/ - void delete(); + default void delete() { + CustomNPCs.getInstance().getStorageManager().remove(getUniqueID()); + } /** *

Sets the actions executed when the NPC is interacted with. @@ -307,4 +336,24 @@ public interface InternalNpc { InternalNpc clone(); void teleport(Location loc); + + /** + * Remove this NPC from this player. + * @param player the player to withdraw + */ + void withdraw(Player player); + + default Map evaluateInjectionConditions(Player player){ + Map map = new HashMap<>(); + for (Condition c : getInjectionConditions()) { + map.put(c, c.compute(player)); + } + return map; + } + + default boolean passesInectionConditions(Player player) { + Map map = evaluateInjectionConditions(player); + if (map.isEmpty()) return true; + return (getInjectionSelector() == Selector.ALL ? !map.containsValue(false) : map.containsValue(true)); + } } diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/listeners/Listeners.java b/core/src/main/java/dev/foxikle/customnpcs/internal/listeners/Listeners.java index af066a5d..fed6964a 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/listeners/Listeners.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/listeners/Listeners.java @@ -25,9 +25,9 @@ import com.destroystokyo.paper.profile.PlayerProfile; import com.destroystokyo.paper.profile.ProfileProperty; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; import dev.foxikle.customnpcs.actions.defaultImpl.*; import dev.foxikle.customnpcs.api.events.NpcInteractEvent; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.LookAtAnchor; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; @@ -242,10 +242,6 @@ public void onPlayerInteract(PlayerInteractEntityEvent e) { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onPlayerMove(PlayerMoveEvent e) { Player player = e.getPlayer(); - if (plugin.isWaiting(player, WaitingType.RECORDING)) { - FollowPresetPathAction.recordMovement(player, e.getTo()); - return; - } actionPlayerMovement(player); } @@ -268,7 +264,7 @@ public void onChat(AsyncPlayerChatEvent e) { e.setCancelled(true); Action actionImpl = plugin.editingActions.get(player.getUniqueId()); - if (!(actionImpl instanceof FollowPresetPathAction follow)) { + if (!(actionImpl instanceof FollowPresetPath follow)) { plugin.getLogger().warning("Expected action to be an instance of 'FollowPresetPathAction', got " + actionImpl.getClass().getSimpleName()); return; } @@ -280,7 +276,7 @@ public void onChat(AsyncPlayerChatEvent e) { return; } - List path = FollowPresetPathAction.stopRecording(player); + List path = FollowPresetPath.stopRecording(player); follow.setPath(path); player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.set.recording", String.valueOf(path.size()))); @@ -320,22 +316,18 @@ public void onChat(AsyncPlayerChatEvent e) { plugin.waiting.remove(player.getUniqueId()); int index = HologramMenu.editingIndicies.get(player.getUniqueId()); - if (npc.getSettings().getRawHolograms().length <= index) { // an addition - npc.getSettings().setRawHolograms(Arrays.copyOf(npc.getSettings().getRawHolograms(), index + 1)); // - // extend it by 1 - } - npc.getSettings().getRawHolograms()[index] = message; + npc.getSettings().getRawHolograms().set(index, message); player.sendMessage(Msg.translate(player.locale(), "customnpcs.set.name", index + 1, Msg.format(message))); SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, MenuUtils.NPC_HOLOGRAMS)); } else if (plugin.isWaiting(player, WaitingType.TARGET)) { - Condition conditional = plugin.editingConditionals.get(player.getUniqueId()); + Condition condition = plugin.editingConditionals.get(player.getUniqueId()); if (cancel) { plugin.waiting.remove(player.getUniqueId()); SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, MenuUtils.NPC_CONDITION_CUSTOMIZER)); e.setCancelled(true); return; } - if (conditional.getType() == Condition.Type.NUMERIC) { + if (condition.getType() == Condition.Type.NUMERIC) { try { Double.parseDouble(message); } catch (NumberFormatException ignored) { @@ -344,8 +336,8 @@ public void onChat(AsyncPlayerChatEvent e) { } } plugin.waiting.remove(player.getUniqueId()); - conditional.setTargetValue(message); - plugin.editingConditionals.put(player.getUniqueId(), conditional); + condition.setTargetValue(message); + plugin.editingConditionals.put(player.getUniqueId(), condition); player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.conditions.set.target", message)); SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, MenuUtils.NPC_CONDITION_CUSTOMIZER)); } else if (plugin.isWaiting(player, WaitingType.TITLE)) { @@ -564,7 +556,6 @@ public void onChat(AsyncPlayerChatEvent e) { return; } if (message.equalsIgnoreCase("confirm")) { - npc.getSettings().setDirection(player.getLocation().getYaw()); npc.getSpawnLoc().setPitch(player.getLocation().getPitch()); npc.getSpawnLoc().setYaw(player.getLocation().getYaw()); player.sendMessage(Msg.translate(player.locale(), "customnpcs.set.facing_direction")); @@ -668,7 +659,7 @@ public void onRespawn(PlayerRespawnEvent e) { double distanceSquared = location.distanceSquared(spawnLocation); if (distanceSquared <= FIFTY_BLOCKS) { - SCHEDULER.runTaskLater(plugin, () -> npc.injectPlayer(player), 5); + SCHEDULER.runTaskLater(plugin, () -> npc.getInjectionManager().markForInjection(player.getUniqueId()), 5); } if (distanceSquared <= FIVE_BLOCKS && !npc.getSettings().isTunnelvision()) { diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionCustomizerMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionCustomizerMenu.java index 7c725e8d..40ca2939 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionCustomizerMenu.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionCustomizerMenu.java @@ -22,7 +22,7 @@ package dev.foxikle.customnpcs.internal.menu; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.utils.Msg; import io.github.mqzen.menus.base.Content; diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteLineMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteLineMenu.java index 46570ffa..d10eb228 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteLineMenu.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteLineMenu.java @@ -25,7 +25,6 @@ import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import dev.foxikle.customnpcs.internal.utils.Msg; -import dev.foxikle.customnpcs.internal.utils.Utils; import io.github.mqzen.menus.base.Content; import io.github.mqzen.menus.base.Menu; import io.github.mqzen.menus.misc.Capacity; @@ -41,6 +40,7 @@ import org.bukkit.event.inventory.InventoryCloseEvent; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; import java.util.List; public class DeleteLineMenu implements Menu { @@ -65,7 +65,7 @@ public String getName() { InternalNpc npcFor = plugin.getEditingNPCs().getIfPresent(player.getUniqueId()); int index = HologramMenu.editingIndicies.getOrDefault(player.getUniqueId(), -1); - if (npcFor == null || index < 0 || npcFor.getSettings().getHolograms().length <= index) { + if (npcFor == null || index < 0 || npcFor.getSettings().getHolograms().size() <= index) { player.sendMessage(Msg.translate(player.locale(), "customnpcs.error.npc-menu-expired")); player.playSound(player, Sound.ENTITY_VILLAGER_NO, 1, 1); return Content.empty(capacity); @@ -75,7 +75,8 @@ public String getName() { .setButton(11, Button.clickable( ItemBuilder.modern(Material.RED_STAINED_GLASS_PANE) .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.hologram.delete.confirm")) - .setLore(Msg.lore(player.locale(), "customnpcs.menus.hologram.delete.confirm.lore", npcFor.getSettings().getHolograms()[index])) + .setLore(Msg.lore(player.locale(), "customnpcs.menus.hologram.delete.confirm.lore", + npcFor.getSettings().getHolograms().get(index))) .build(), ButtonClickAction.plain((menuView, inventoryClickEvent) -> { Player p = (Player) inventoryClickEvent.getWhoClicked(); @@ -88,9 +89,8 @@ public String getName() { return; } - String[] raw = npc.getSettings().getRawHolograms(); - List mutable = Utils.list(raw); - if (index >= raw.length) { + List mutable = new ArrayList<>(npc.getSettings().getRawHolograms()); + if (index >= mutable.size()) { p.playSound(p, Sound.ENTITY_VILLAGER_NO, 1, 1); p.sendMessage(Msg.translate(player.locale(), "customnpcs.error.npc-menu-expired")); return; diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteMenu.java index 851206e1..739f644b 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteMenu.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteMenu.java @@ -112,7 +112,8 @@ public InventoryType getMenuType() { npc.remove(); npc.delete(); plugin.npcs.remove(npc.getUniqueID()); - player1.sendMessage(Msg.translate(player.locale(), "customnpcs.delete.success", npc.getSettings().getName())); + player1.sendMessage(Msg.translate(player.locale(), "customnpcs.delete.success", + npc.getSettings().getRawHolograms().getFirst())); player1.closeInventory(); player1.playSound(player1, Sound.BLOCK_END_PORTAL_SPAWN, 1, 1); npc.getCurrentLocation().getWorld().strikeLightningEffect(npc.getCurrentLocation()); diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuItems.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuItems.java index 04fa2465..6a12375d 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuItems.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuItems.java @@ -25,9 +25,8 @@ import com.destroystokyo.paper.profile.PlayerProfile; import com.destroystokyo.paper.profile.ProfileProperty; import dev.foxikle.customnpcs.actions.Action; -import dev.foxikle.customnpcs.actions.conditions.Condition; -import dev.foxikle.customnpcs.actions.conditions.LogicalCondition; -import dev.foxikle.customnpcs.actions.conditions.NumericCondition; +import dev.foxikle.customnpcs.conditions.*; +import dev.foxikle.customnpcs.conditions.Comparator; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.internal.CustomNPCs; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; @@ -50,11 +49,9 @@ import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; +import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; +import java.util.*; import java.util.logging.Level; import static org.bukkit.Material.*; @@ -73,8 +70,8 @@ public static Button changeLines(InternalNpc npc, Player player) { Component lines = Component.empty(); - for (int i = 0; i < npc.getSettings().getHolograms().length; i++) { - Component holo = npc.getSettings().getHolograms()[i]; + for (int i = 0; i < npc.getSettings().getHolograms().size(); i++) { + Component holo = npc.getSettings().getHolograms().get(i); lines = lines.append(Msg.format(" " + (i + 1) + ". ").append(holo)).append(Component.newline()); } @@ -86,6 +83,73 @@ public static Button changeLines(InternalNpc npc, Player player) { .build(), new OpenButtonAction(MenuUtils.NPC_HOLOGRAMS)); } + public static Button rotation(InternalNpc npc, Player player) { + double dir = npc.getSpawnLoc().getYaw(); + + List lore = new ArrayList<>(); + Map highlightIndexMap = Map.of(180, 0, -135, 1, -90, 2, -45, 3, 0, 4, 45, 5, 90, 6, 135, 7); + Component clickToChange = Msg.translate(player.locale(), "customnpcs.items.click_to_change"); + List directions = List.of(Msg.translate(player.locale(), "customnpcs.directions.north"), Msg.translate(player.locale(), "customnpcs.directions.north_east"), Msg.translate(player.locale(), "customnpcs.directions.east"), Msg.translate(player.locale(), "customnpcs.directions.south_east"), Msg.translate(player.locale(), "customnpcs.directions.south"), Msg.translate(player.locale(), "customnpcs.directions.south_west"), Msg.translate(player.locale(), "customnpcs.directions.west"), Msg.translate(player.locale(), "customnpcs.directions.north_west"), Msg.translate(player.locale(), "customnpcs.directions.player")); + int highlightIndex = highlightIndexMap.getOrDefault((int) dir, 8); + lore.add(Component.empty()); + + for (int i = 0; i < directions.size(); ++i) { + Component direction = directions.get(i); + if (i == highlightIndex) { + direction = direction.color(NamedTextColor.DARK_AQUA); + direction = Utils.mm("â–¸ ").append(direction); + } + + lore.add(direction); + } + + lore.add(Component.empty()); + lore.add(clickToChange); + + ItemStack item = ItemBuilder.modern(COMPASS) + .setLore(lore) + .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.main.facing_direction.name")) + .build(); + + return Button.clickable(item, ButtonClickAction.plain((menuView, event) -> { + event.setCancelled(true); + Player p = (Player) event.getWhoClicked(); + p.playSound(p.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); + + double newDir = 0.0D; + if (event.isLeftClick()) { + if (dir % 45.0D != 0.0D) { + newDir = 180.0D; + } else { + newDir = (dir + 225.0D) % 360.0D - 180.0D; + if (dir == 135.0D) { + newDir = p.getLocation().getYaw(); + } + } + } else if (event.isRightClick()) { + if (dir % 45.0D != 0.0D) { + newDir = 135.0D; + } else { + newDir = (dir - 225.0D) % 360.0D + 180.0D; + if (dir == 180.0D) { + newDir = p.getLocation().getYaw(); + } + } + } + + npc.getSpawnLoc().setYaw((float) newDir); + menuView.replaceButton(10, rotation(npc, p)); + })); + } + + public static ItemStack changeName(InternalNpc npc, Player player) { + return ItemBuilder.modern(Material.NAME_TAG) + .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.main.items.name.name")) + .setLore(Msg.translate(player.locale(), "customnpcs.menus.main.items.name.current_name", + plugin.getMiniMessage().deserialize(npc.getSettings().getRawHolograms().getFirst()))) + .build(); + } + public static Button resilient(InternalNpc npc, Player player) { ItemStack i = ItemBuilder.modern(Material.BELL) .setLore(npc.getSettings().isResilient() ? Msg.translate(player.locale(), "customnpcs.menus.main.items.resilient.true") : Msg.translate(player.locale(), "customnpcs.menus.main.items.resilient.false")) @@ -169,15 +233,28 @@ public static ItemStack editEquipment(InternalNpc npc, Player player) { Equipment equip = npc.getEquipment(); return ItemBuilder.modern(Material.ARMOR_STAND) .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.name")) - .setLore(Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.main_hand", equip.getHand().getType().name().toLowerCase()), - Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.off_hand", equip.getOffhand().getType().name().toLowerCase()), - Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.helmet", equip.getHead().getType().name().toLowerCase()), - Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.chestplate", equip.getChest().getType().name().toLowerCase()), - Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.leggings", equip.getLegs().getType().name().toLowerCase()), - Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.boots", equip.getBoots().getType().name().toLowerCase()) + .setLore(Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.main_hand", + equipmentName(player.locale(), equip.getHand())), + Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.off_hand", + equipmentName(player.locale(), equip.getOffhand())), + Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.helmet", + equipmentName(player.locale(), equip.getHead())), + Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.chestplate", + equipmentName(player.locale(), equip.getChest())), + Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.leggings", + equipmentName(player.locale(), equip.getLegs())), + Msg.translate(player.locale(), "customnpcs.menus.main.items.equipment.boots", + equipmentName(player.locale(), equip.getBoots())) ).build(); } + private static Component equipmentName(Locale locale, @Nullable ItemStack item) { + if (item == null) { + return Msg.translate(locale, "customnpcs.menus.main.items.equipment.empty"); + } + return Component.translatable(item.translationKey()); + } + public static Button tunnelVision(InternalNpc npc, Player player) { return Button.clickable(ItemBuilder.modern(Material.SPYGLASS) .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.main.vision.name")) @@ -260,7 +337,7 @@ public static Button helmetSlot(InternalNpc npc, Player player) { public static Button chestplateSlot(InternalNpc npc, Player player) { ItemStack cp = npc.getEquipment().getChest(); - if (cp.getType().isAir()) { + if (cp == null || cp.getType().isAir()) { return Button.clickable(ItemBuilder.modern(Material.LIME_STAINED_GLASS_PANE) .addFlags(ItemFlag.values()) @@ -270,7 +347,7 @@ public static Button chestplateSlot(InternalNpc npc, Player player) { ButtonClickAction.plain((menuView, event) -> { Player p = (Player) event.getWhoClicked(); event.setCancelled(true); - if (event.getCursor().getType().name().contains("CHESTPLATE") || event.getCursor().getType() == Material.ELYTRA) { + if (event.getCursor().getType().name().contains("CHESTPLATE")) { npc.getEquipment().setChest(event.getCursor().clone()); event.getCursor().setAmount(0); p.playSound(p.getLocation(), Sound.ITEM_ARMOR_EQUIP_LEATHER, 1, 1); @@ -299,7 +376,7 @@ public static Button chestplateSlot(InternalNpc npc, Player player) { p.sendMessage(Msg.translate(p.locale(), "customnpcs.menus.equipment.chestplate.reset")); menuView.replaceButton(22, chestplateSlot(npc, p)); return; - } else if (event.getCursor().getType().name().contains("CHESTPLATE") || event.getCursor().getType() == Material.ELYTRA) { + } else if (event.getCursor().getType().name().contains("CHESTPLATE")) { npc.getEquipment().setChest(event.getCursor().clone()); event.getCursor().setAmount(0); p.playSound(p.getLocation(), Sound.ITEM_ARMOR_EQUIP_LEATHER, 1, 1); @@ -317,7 +394,7 @@ public static Button chestplateSlot(InternalNpc npc, Player player) { public static Button leggingsSlot(InternalNpc npc, Player player) { ItemStack legs = npc.getEquipment().getLegs(); - if (legs.getType().isAir()) { + if (legs == null || legs.getType().isAir()) { return Button.clickable(ItemBuilder.modern(Material.LIME_STAINED_GLASS_PANE) .addFlags(ItemFlag.values()) .setLore(Msg.lore(player.locale(), "customnpcs.menus.equipment.legs.change")) @@ -370,7 +447,7 @@ public static Button leggingsSlot(InternalNpc npc, Player player) { public static Button bootsSlot(InternalNpc npc, Player player) { ItemStack boots = npc.getEquipment().getBoots(); - if (boots.getType().isAir()) { + if (boots == null || boots.getType().isAir()) { return Button.clickable(ItemBuilder.modern(LIME_STAINED_GLASS_PANE) .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.equipment.boots.empty")) .addFlags(ItemFlag.values()) @@ -423,7 +500,7 @@ public static Button bootsSlot(InternalNpc npc, Player player) { public static Button handSlot(InternalNpc npc, Player player) { ItemStack hand = npc.getEquipment().getHand(); - if (hand.getType().isAir()) { + if (hand == null || hand.getType().isAir()) { return Button.clickable(ItemBuilder.modern(YELLOW_STAINED_GLASS_PANE) .addFlags(ItemFlag.values()) .setLore(Msg.lore(player.locale(), "customnpcs.menus.equipment.hand.change")) @@ -469,7 +546,7 @@ public static Button handSlot(InternalNpc npc, Player player) { public static Button offhandSlot(InternalNpc npc, Player player) { ItemStack offhand = npc.getEquipment().getOffhand(); - if (offhand.getType().isAir()) { + if (offhand == null || offhand.getType().isAir()) { return Button.clickable(ItemBuilder.modern(YELLOW_STAINED_GLASS_PANE) .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.equipment.offhand.empty")) .setLore(Msg.lore(player.locale(), "customnpcs.menus.equipment.offhand.change")) @@ -512,16 +589,16 @@ public static Button offhandSlot(InternalNpc npc, Player player) { } } - public static Button toMain(Player player) { - return Button.clickable(ItemBuilder.modern(BARRIER).setDisplay(Msg.translate(player.locale(), "customnpcs.items.go_back")).build(), - new OpenButtonAction(MenuUtils.NPC_MAIN)); - } - public static Button toPose(Player player) { return Button.clickable(ItemBuilder.modern(SNIFFER_EGG).setDisplay(Msg.translate(player.locale(), "customnpcs.pose.pose_editor")).build(), new OpenButtonAction(MenuUtils.NPC_POSE)); } + public static Button toMain(Player player) { + return Button.clickable(ItemBuilder.modern(BARRIER).setDisplay(Msg.translate(player.locale(), "customnpcs.items.go_back")).build(), + new OpenButtonAction(MenuUtils.NPC_MAIN)); + } + public static Button toAction(Player player) { return Button.clickable(ItemBuilder.modern(ARROW).setDisplay(Msg.translate(player.locale(), "customnpcs.items.go_back")).build(), new OpenButtonAction(MenuUtils.NPC_ACTIONS)); @@ -559,7 +636,7 @@ public static List