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 568f7201..517db6bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { implementation(project(":v1_20_R1", "reobf")) } -var pluginVersion = "1.7.5" +var pluginVersion = "1.8-pre2" allprojects { group = "dev.foxikle" @@ -122,7 +122,7 @@ tasks { "apiVersion" to "1.20" ) inputs.properties(props) - filesMatching("plugin.yml") { + filesMatching("paper-plugin.yml") { expand(props) } } @@ -154,5 +154,3 @@ tasks.register("aggregatedJavadocs") { } } } - - diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 09563fb6..d3012a3a 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -42,6 +42,10 @@ dependencies { compileOnly("org.mineskin:java-client-jsoup:3.0.6") compileOnly("dev.velix:imperat-bukkit:1.9.7") compileOnly("dev.velix:imperat-core:1.9.7") + 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") + compileOnly("org.spongepowered:configurate-gson:4.2.0") } val generateClassloader = tasks.register("generateClassloader") { 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 986b200e..fd39b4e1 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/Action.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/Action.java @@ -22,7 +22,8 @@ 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; @@ -51,9 +52,9 @@ public abstract class Action { private List conditions = new ArrayList<>(); private int delay = 0; - private Condition.SelectionMode mode = Condition.SelectionMode.ONE; + private Selector mode = Selector.ONE; private int cooldown = 0; - private Map cooldowns = new ConcurrentHashMap<>(); + private final Map cooldowns = new ConcurrentHashMap<>(); /** * Default constructor @@ -62,7 +63,7 @@ public Action() { } - public Action(int delay, Condition.SelectionMode mode, List conditions, int cooldown) { + public Action(int delay, Selector mode, List conditions, int cooldown) { this.delay = delay; this.mode = mode; this.conditions = conditions; @@ -71,11 +72,11 @@ public Action(int delay, Condition.SelectionMode mode, List condition /** - * Deprecated, use {@link Action#Action(int, Condition.SelectionMode, List, int)} Action} + * Deprecated, use {@link Action#Action(int, Selector, List, int)} Action} */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public Action(int delay, Condition.SelectionMode mode, List conditions) { + public Action(int delay, Selector mode, List conditions) { this(delay, mode, conditions, 0); } @@ -119,7 +120,7 @@ public static Action parse(@NotNull String s) { 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); } @@ -270,7 +271,7 @@ public boolean processConditions(Player player) { 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)); + return (mode == Selector.ALL ? !results.contains(false) : results.contains(true)); } private String getConditionSerialized() { @@ -333,13 +334,13 @@ protected String generateSerializedString(String id, Map params) public abstract Action clone(); - protected record ParseResult(int delay, Condition.SelectionMode mode, List conditions, int cooldown) { + protected record ParseResult(int delay, Selector mode, List conditions, int cooldown) { /** - * @deprecated Use {@link ParseResult#ParseResult(int, Condition.SelectionMode, List, int)}} + * @deprecated Use {@link ParseResult#ParseResult(int, Selector, List, int)}} */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public ParseResult(int delay, Condition.SelectionMode mode, List conditions) { + public ParseResult(int delay, Selector mode, List conditions) { this(delay, mode, conditions, 0); } } diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ActionAdapter.java b/core/src/main/java/dev/foxikle/customnpcs/actions/ActionAdapter.java similarity index 92% rename from core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ActionAdapter.java rename to core/src/main/java/dev/foxikle/customnpcs/actions/ActionAdapter.java index d7cdcaa6..3e729dd1 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ActionAdapter.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/ActionAdapter.java @@ -20,14 +20,15 @@ * SOFTWARE. */ -package dev.foxikle.customnpcs.actions.conditions; +package dev.foxikle.customnpcs.actions; 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 dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.ConditionalTypeAdapter; +import dev.foxikle.customnpcs.conditions.Selector; import java.io.IOException; import java.util.ArrayList; @@ -83,7 +84,7 @@ public LegacyAction read(JsonReader in) throws IOException { List args = new ArrayList<>(); int delay = 0; List conditions = new ArrayList<>(); - Condition.SelectionMode selectionMode = null; + Selector selectionMode = null; while (in.hasNext()) { @@ -98,7 +99,7 @@ public LegacyAction read(JsonReader in) throws IOException { in.endArray(); } case "delay" -> delay = in.nextInt(); - case "mode" -> selectionMode = Condition.SelectionMode.valueOf(in.nextString()); + case "mode" -> selectionMode = Selector.valueOf(in.nextString()); case "conditionals" -> { in.beginArray(); while(in.hasNext()) { diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java b/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java index cd739d7d..69252a80 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java +++ b/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java @@ -22,8 +22,9 @@ package dev.foxikle.customnpcs.actions; -import dev.foxikle.customnpcs.actions.conditions.Condition; import dev.foxikle.customnpcs.actions.defaultImpl.*; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.internal.CustomNPCs; import lombok.Getter; import lombok.Setter; @@ -51,7 +52,7 @@ public class LegacyAction { @Setter private int delay; @Setter - private Condition.SelectionMode mode; + private Selector mode; /** *

Creates a new Action @@ -63,7 +64,7 @@ public class LegacyAction { * @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) { + public LegacyAction(ActionType actionType, List args, int delay, Selector matchAll, List conditionals) { this.actionType = actionType; this.args = args; this.delay = delay; @@ -75,7 +76,7 @@ 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.mode = Selector.ONE; this.conditionals = new ArrayList<>(); } @@ -152,7 +153,7 @@ private boolean processConditions(Player player) { Set results = new HashSet<>(2); conditionals.forEach(conditional -> results.add(conditional.compute(player))); - return (mode == Condition.SelectionMode.ALL ? !results.contains(false) : results.contains(true)); + return (mode == Selector.ALL ? !results.contains(false) : results.contains(true)); } /** 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 461d3d00..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Conditional.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (c) 2024-2025. 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 69fb58c4..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalConditional.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 2024-2025. 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 2cda6ceb..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericConditional.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) 2024-2025. 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 6ef3511b..7bf911d0 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; @@ -65,11 +66,11 @@ public class ActionBar extends Action { * 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 Use {@link ActionBar#ActionBar(String, int, Selector, List, int)} */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public ActionBar(String rawMessage, int delay, Condition.SelectionMode mode, List conditionals) { + public ActionBar(String rawMessage, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals, 0); this.rawMessage = rawMessage; } @@ -79,7 +80,7 @@ public ActionBar(String rawMessage, int delay, Condition.SelectionMode mode, Lis * * @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; } @@ -92,7 +93,7 @@ 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()); })); 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 324c088d..16dabb0d 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; @@ -75,7 +76,7 @@ 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) { + public DisplayTitle(String title, String subTitle, int fadeIn, int stay, int fadeOut, int delay, Selector mode, List conditionals, int cooldown) { super(delay, mode, conditionals, cooldown); this.title = title; this.subTitle = subTitle; @@ -88,11 +89,11 @@ public DisplayTitle(String title, String subTitle, int fadeIn, int stay, int fad * 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)}} + * @deprecated Use {@link DisplayTitle#DisplayTitle(String, String, int, int, int, int, Selector, 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) { + public DisplayTitle(String title, String subTitle, int fadeIn, int stay, int fadeOut, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals, 0); this.title = title; this.subTitle = subTitle; @@ -110,13 +111,13 @@ 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) { + public static T deserialize(String serialized, Class clazz) { if (!clazz.equals(DisplayTitle.class)) { throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + DisplayTitle.class.getName()); } 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 20eb3205..06359539 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; @@ -72,7 +73,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; @@ -84,11 +85,11 @@ public GiveEffect(String effect, int duration, int amplifier, boolean 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 Use {@link #GiveEffect(String, int, int, boolean, int, Selector, 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) { + public GiveEffect(String effect, int duration, int amplifier, boolean particles, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals, 0); this.effect = effect; this.duration = duration; @@ -105,7 +106,7 @@ 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()); })); 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 db1d8130..385e46c6 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; @@ -67,7 +68,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,7 +83,7 @@ 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; @@ -96,7 +97,7 @@ public GiveXP(int amount, boolean levels, int delay, Condition.SelectionMode mod */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public GiveXP(int amount, boolean levels, int delay, Condition.SelectionMode mode, List conditionals) { + public GiveXP(int amount, boolean levels, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals, 0); this.levels = levels; this.amount = amount; 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 4c3b9dd8..99f54fbc 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; @@ -70,7 +71,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()); })); @@ -87,7 +88,7 @@ public static Button creationButton(Player player) { * @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; @@ -100,11 +101,11 @@ public PlaySound(String sound, float volume, float pitch, int delay, Condition.S * @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 Use {@link PlaySound#PlaySound(String, float, float, int, Selector, List, int)} */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public PlaySound(String sound, float volume, float pitch, int delay, Condition.SelectionMode mode, List conditionals) { + public PlaySound(String sound, float volume, float pitch, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals, 0); this.sound = sound; this.volume = volume; 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 295d21a5..143d9d96 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; @@ -71,7 +72,7 @@ public class RemoveEffect extends Action { * * @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; } @@ -80,11 +81,11 @@ public RemoveEffect(String effect, int delay, Condition.SelectionMode mode, List * Creates a new GiveEffect with the specified parameters * * @param effect The raw message - * @deprecated Use {@link #RemoveEffect(String, int, Condition.SelectionMode, List, int)} + * @deprecated Use {@link #RemoveEffect(String, int, Selector, List, int)} */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public RemoveEffect(String effect, int delay, Condition.SelectionMode mode, List conditionals) { + public RemoveEffect(String effect, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals, 0); this.effect = effect; } @@ -99,13 +100,13 @@ 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) { + public static T deserialize(String serialized, Class clazz) { if (!clazz.equals(RemoveEffect.class)) { throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + RemoveEffect.class.getName()); } 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 df4074f9..81e2668f 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; @@ -68,7 +69,7 @@ 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()); })); @@ -83,7 +84,7 @@ public static Button creationButton(Player player) { * @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) { + 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; @@ -94,17 +95,17 @@ public RemoveXP(int amount, boolean levels, int delay, Condition.SelectionMode m * * @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 Use {@link #RemoveXP(int, boolean, int, Selector, List, int)} */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public RemoveXP(int amount, boolean levels, int delay, Condition.SelectionMode mode, List conditionals) { + public RemoveXP(int amount, boolean levels, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals, 0); this.levels = levels; this.amount = amount; } - public static T deserialize(String serialized, Class clazz) { + public static T deserialize(String serialized, Class clazz) { if (!clazz.equals(RemoveXP.class)) { throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + RemoveXP.class.getName()); } 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 05317a49..c6aaf99c 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; @@ -71,7 +72,7 @@ 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()); })); @@ -90,7 +91,7 @@ public static Button creationButton(Player player) { * @param mode The mode * @param conditionals The conditionals */ - public RunCommand(String rawCommand, boolean asConsole, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { + 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; @@ -104,17 +105,17 @@ public RunCommand(String rawCommand, boolean asConsole, int delay, Condition.Sel * @param delay The delay * @param mode The mode * @param conditionals The conditionals - * @deprecated Use {@link #RunCommand(String, boolean, int, Condition.SelectionMode, List, int)} + * @deprecated Use {@link #RunCommand(String, boolean, int, Selector, List, int)} */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public RunCommand(String rawCommand, boolean asConsole, int delay, Condition.SelectionMode mode, List conditionals) { + public RunCommand(String rawCommand, boolean asConsole, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals, 0); this.command = rawCommand; this.asConsole = asConsole; } - public static T deserialize(String serialized, Class clazz) { + public static T deserialize(String serialized, Class clazz) { if (!clazz.equals(RunCommand.class)) { throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + RunCommand.class.getName()); } 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 18bb2d4a..a34130b8 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; @@ -67,7 +68,7 @@ public class SendMessage extends Action { * * @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; } @@ -76,11 +77,11 @@ public SendMessage(String rawMessage, int delay, Condition.SelectionMode mode, L * 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 Use {@link SendMessage#SendMessage(String, int, Selector, List, int)} */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.9") - public SendMessage(String rawMessage, int delay, Condition.SelectionMode mode, List conditionals) { + public SendMessage(String rawMessage, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals); this.rawMessage = rawMessage; } @@ -95,7 +96,7 @@ 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()); })); 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 6815c51d..3e96d0d6 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; @@ -69,7 +70,7 @@ public class SendServer extends Action { * * @param server The raw message */ - public SendServer(String server, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) { + public SendServer(String server, int delay, Selector mode, List conditionals, int cooldown) { super(delay, mode, conditionals, cooldown); this.server = server; } @@ -78,11 +79,11 @@ public SendServer(String server, int delay, Condition.SelectionMode mode, List conditionals) { + public SendServer(String server, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals, 0); this.server = server; } @@ -99,7 +100,7 @@ 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()); })); 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 1bc87b0c..ef8a9038 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; @@ -76,7 +77,7 @@ public class Teleport extends Action { * @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) { + 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; @@ -96,11 +97,11 @@ public Teleport(double x, double y, double z, float pitch, float yaw, int delay, * @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 Use {@link Teleport#Teleport(double, double, double, float, float, int, Selector, 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) { + public Teleport(double x, double y, double z, float pitch, float yaw, int delay, Selector mode, List conditionals) { super(delay, mode, conditionals, 0); this.x = x; this.y = y; @@ -119,7 +120,7 @@ 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()); })); 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 470bb5d7..f36b098b 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/api/NPC.java +++ b/core/src/main/java/dev/foxikle/customnpcs/api/NPC.java @@ -26,6 +26,7 @@ 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 +80,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 +94,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 +106,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; } @@ -182,10 +166,10 @@ public NPC addAction(@NotNull Action action) { * @return the NPC with the modified set of actions * @see Action * @since 1.5.2-pre3 - * @deprecated Use {@link #setActions(Collection)} + * @deprecated Use {@link #setActions(Collection)}, to be removed in 1.9 with the removal of legacy actions. */ @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.8") + @ApiStatus.ScheduledForRemoval(inVersion = "1.9") public NPC setLegacyActions(Collection actionImpls) { List actionList = new ArrayList<>(); for (LegacyAction legacyAction : actionImpls) { @@ -238,6 +222,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 +379,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 96b68347..0b50ede5 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 0384a49a..f89649b0 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 11d83af1..9f37e0ec 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,7 +20,7 @@ * SOFTWARE. */ -package dev.foxikle.customnpcs.actions.conditions; +package dev.foxikle.customnpcs.conditions; import dev.foxikle.customnpcs.internal.CustomNPCs; import lombok.Getter; @@ -115,57 +115,6 @@ 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 */ @@ -268,6 +217,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 +242,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 89f3d647..7334958d 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 99% 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 c5dd5575..e3223884 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,7 +20,7 @@ * SOFTWARE. */ -package dev.foxikle.customnpcs.actions.conditions; +package dev.foxikle.customnpcs.conditions; import dev.foxikle.customnpcs.internal.CustomNPCs; import org.bukkit.GameMode; 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 97% 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 d982a227..8a8e5205 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,7 +20,7 @@ * SOFTWARE. */ -package dev.foxikle.customnpcs.actions.conditions; +package dev.foxikle.customnpcs.conditions; import dev.foxikle.customnpcs.internal.CustomNPCs; import org.bukkit.entity.Player; @@ -40,7 +40,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 +120,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) { @@ -188,4 +188,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 8a2eea30..6097c2ad 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/data/Equipment.java +++ b/core/src/main/java/dev/foxikle/customnpcs/data/Equipment.java @@ -28,12 +28,14 @@ import org.bukkit.Material; import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.ItemStack; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; /** * The class representing the NPC's items */ @Getter @SuppressWarnings("UnusedReturnValue") +@ConfigSerializable public class Equipment { private ItemStack head = new ItemStack(Material.AIR); private ItemStack chest = new ItemStack(Material.AIR); 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 e93e6cc5..03ff217a 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/data/Settings.java +++ b/core/src/main/java/dev/foxikle/customnpcs/data/Settings.java @@ -35,25 +35,19 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; /** * A class holding the data for an NPC's settings */ @AllArgsConstructor +@ConfigSerializable @SuppressWarnings({"UnusedReturnValue", "unused"}) public class Settings { - - @Getter - boolean interactable = false; - @Getter - boolean tunnelvision = false; - @Getter - boolean resilient = true; - @Deprecated - @ApiStatus.ScheduledForRemoval(inVersion = "1.8") - @Getter - double direction = 180; + @Getter boolean interactable = false; + @Getter boolean tunnelvision = false; + @Getter boolean resilient = true; @Getter int interpolationDuration = CustomNPCs.INTERPOLATION_DURATION; @Getter @@ -76,34 +70,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 @@ -111,69 +77,6 @@ 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 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 - * - * @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 - */ - @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) { - 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 @@ -182,11 +85,10 @@ public Settings(boolean interactable, boolean tunnelvision, boolean resilient, d * @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) { + public Settings(boolean interactable, boolean tunnelvision, boolean resilient, 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,7 +105,6 @@ 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 @@ -212,11 +113,10 @@ public Settings(boolean interactable, boolean tunnelvision, boolean resilient, d * @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, String[] 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 +176,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 @@ -497,7 +386,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); + public Settings clone(){ + 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 4b052645..195e2e53 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/CustomNPCs.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/CustomNPCs.java @@ -28,10 +28,11 @@ import com.google.gson.GsonBuilder; 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.ActionAdapter; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.conditions.ConditionalTypeAdapter; import dev.foxikle.customnpcs.actions.defaultImpl.*; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.commands.NpcCommand; @@ -41,10 +42,12 @@ 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; import dev.foxikle.customnpcs.internal.utils.WaitingType; +import dev.foxikle.customnpcs.internal.utils.configurate.*; import dev.velix.imperat.BukkitImperat; import dev.velix.imperat.BukkitSource; import dev.velix.imperat.Imperat; @@ -58,14 +61,19 @@ import org.bstats.charts.AdvancedPie; import org.bstats.charts.SimplePie; import org.bukkit.Bukkit; +import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.entity.TextDisplay; import org.bukkit.event.HandlerList; +import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.messaging.PluginMessageListener; import org.jetbrains.annotations.NotNull; +import org.spongepowered.configurate.gson.GsonConfigurationLoader; +import org.spongepowered.configurate.loader.ConfigurationLoader; +import org.spongepowered.configurate.objectmapping.ObjectMapper; import javax.annotation.Nullable; import java.util.*; @@ -83,6 +91,19 @@ public final class CustomNPCs extends JavaPlugin implements PluginMessageListene public static final ActionRegistry ACTION_REGISTRY = new ActionRegistry(); public static int INTERPOLATION_DURATION; + public static final GsonConfigurationLoader.Builder CONFIGURATE = GsonConfigurationLoader.builder() + .indent(0) + .defaultOptions(opts -> opts + .shouldCopyDefaults(true) + .serializers(builder -> { + builder.registerAnnotatedObjects(ObjectMapper.factory()); + builder.register(Action.class, new ActionSerializer()); + builder.register(Condition.class, new ConditionSerializer()); + builder.register(Location.class, new LocationSerializer()); + builder.register(ItemStack.class, new ItemstackSerializer()); + builder.register(Color.class, new ColorSerializer()); + }) + ); /** * Singleton for the NPCBuilder */ @@ -150,7 +171,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 */ @@ -190,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); @@ -207,15 +228,11 @@ public void onEnable() { .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); @@ -229,13 +246,8 @@ public void onEnable() { ACTION_REGISTRY.register("SendServer", SendServer.class, SendServer::creationButton, true, false, true); ACTION_REGISTRY.register("Teleport", Teleport.class, Teleport::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); + if (!storageManager.setup()) { + throw new RuntimeException("Failed to start storage manager"); } //generate skin menus for the supported locales @@ -467,16 +479,16 @@ public void onPluginMessageReceived(@NotNull String channel, @NotNull Player pla * @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 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) { + public InternalNpc createNPC(World world, Location location, Equipment equipment, Settings settings, UUID uuid, @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); + .getConstructor(this.getClass(), World.class, Location.class, Equipment.class, Settings.class, 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); 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 f6e3d46a..00000000 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/FileManager.java +++ /dev/null @@ -1,749 +0,0 @@ -/* - * Copyright (c) 2024-2025. 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 118a7b17..5ee52cdd 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(); @@ -79,9 +83,13 @@ private void checkForInjections() { } 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); } diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CloneCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CloneCommand.java index 3d3e93cc..dea8cb8e 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CloneCommand.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CloneCommand.java @@ -59,7 +59,6 @@ public void usage( assert finalNpc != null; InternalNpc newNpc = finalNpc.clone(); newNpc.setSpawnLoc(p.getLocation()); - newNpc.getSettings().setDirection(p.getLocation().getYaw()); newNpc.createNPC(); p.sendMessage(Msg.translate(p.locale(), "customnpcs.commands.clone.success")); } 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 8a69ad45..fdaef124 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 @@ -85,6 +85,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; } diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CreateCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CreateCommand.java index c2a4b515..ae7d99c6 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CreateCommand.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CreateCommand.java @@ -22,6 +22,8 @@ package dev.foxikle.customnpcs.internal.commands; +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; @@ -49,7 +51,7 @@ public void usage(BukkitSource source) { final Player p = source.asPlayer(); final UUID uuid = UUID.randomUUID(); final CustomNPCs plugin = CustomNPCs.getInstance(); - InternalNpc npc = plugin.createNPC(p.getWorld(), p.getLocation(), new Equipment(), new Settings(), uuid, null, new ArrayList<>()); + InternalNpc npc = plugin.createNPC(p.getWorld(), p.getLocation(), new Equipment(), new Settings(), uuid, null, new ArrayList<>(), new ArrayList<>(), Selector.ONE); plugin.getEditingNPCs().put(p.getUniqueId(), npc); plugin.getLotus().openMenu(p, MenuUtils.NPC_MAIN); } diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/EditCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/EditCommand.java index 86626360..ea758260 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/EditCommand.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/EditCommand.java @@ -59,7 +59,7 @@ public void editNpc( final CustomNPCs plugin = CustomNPCs.getInstance(); final InternalNpc finalNpc = plugin.getNPCByID(uuid); Bukkit.getScheduler().runTaskLater(plugin, () -> { - InternalNpc newNpc = plugin.createNPC(p.getWorld(), finalNpc.getSpawnLoc(), finalNpc.getEquipment(), finalNpc.getSettings(), finalNpc.getUniqueID(), null, finalNpc.getActions()); + InternalNpc newNpc = plugin.createNPC(p.getWorld(), finalNpc.getSpawnLoc(), finalNpc.getEquipment(), finalNpc.getSettings(), finalNpc.getUniqueID(), null, finalNpc.getActions(), finalNpc.getInjectionConditions(), finalNpc.getInjectionSelectionMode()); plugin.getEditingNPCs().put(p.getUniqueId(), newNpc); plugin.getLotus().openMenu(p, MenuUtils.NPC_MAIN); }, 1); diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/FixConfigCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/FixConfigCommand.java index aa043227..1694b703 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/FixConfigCommand.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/FixConfigCommand.java @@ -23,18 +23,18 @@ package dev.foxikle.customnpcs.internal.commands; 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.storage.StorableNPC; +import dev.foxikle.customnpcs.internal.storage.StorageManager; import dev.foxikle.customnpcs.internal.utils.Msg; import dev.velix.imperat.BukkitSource; import dev.velix.imperat.annotations.*; import dev.velix.imperat.command.AttachmentMode; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.FluidCollisionMode; import org.bukkit.Location; import org.bukkit.World; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.util.RayTraceResult; import org.bukkit.util.Vector; @@ -87,44 +87,23 @@ public void execute(BukkitSource source, } CustomNPCs plugin = CustomNPCs.getInstance(); - FileManager fileManager = plugin.getFileManager(); + StorageManager storageManager = plugin.getStorageManager(); if (target.equalsIgnoreCase("all")) { // apply it to all NPCs - for (UUID uuid : fileManager.getBrokenNPCs().keySet()) { - YamlConfiguration yml = fileManager.getNpcYaml(); - ConfigurationSection parent = yml.getConfigurationSection(uuid.toString()); - if (parent == null) { - nonExistentNpcs++; - continue; - } - - ConfigurationSection location = parent.getConfigurationSection("location"); - - Location loc; - String locString; - - //Bukkit's terrible config api didn't wipe the section - if (location != null) { - assert location != null : "Location is null"; + for (StorableNPC npc : storageManager.getBrokenNPCs().values()) { - 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 + ")"; - } else { + Location loc = npc.getSpawnLoc(); + if (loc == null) { loc = 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)"); source.reply(Msg.translate(locale, "customnpcs.commands.fix_config.bukkit_wiped_data")); } + String locString = "(" + loc.x() + ", " + loc.y() + ", " + loc.z() + ")"; if (strat == FixConfigWorldStrategy.SAFE_LOCATION) { @@ -136,34 +115,34 @@ public void execute(BukkitSource source, if (traceResult == null) { // The location cannot be safe - plugin.getLogger().warning("Failed to fix npc " + uuid + " at " + locString + " -- Location cannot be made safe."); + plugin.getLogger().warning("Failed to fix npc " + npc.getUniqueID() + " at " + locString + " -- Location cannot be made safe."); failedToFix++; continue; } loc.setY(traceResult.getHitBlock().getY() + 1); } movedbyStrategy++; - } - parent.set("location", loc); totalFixed++; - fileManager.saveNpcFile(yml); + npc.setSpawnLoc(loc); + storageManager.track(npc); // retrack the fixed on :) } } else { // find npc by flag try { - - if (!fileManager.getBrokenNPCs().containsValue(target)) { + if (storageManager.getBrokenNPCs().values().stream().noneMatch(npc -> MiniMessage.miniMessage().stripTags(npc.getSettings().getName()).equals(target))) { // it dont exist source.reply(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid ")); return; } UUID uuid = null; - for (Map.Entry entry : fileManager.getBrokenNPCs().entrySet()) { - if (entry.getValue().equals(target)) { + StorableNPC npc = null; + for (Map.Entry entry : storageManager.getBrokenNPCs().entrySet()) { + if (MiniMessage.miniMessage().stripTags(entry.getValue().getSettings().getName()).equals(target)) { uuid = entry.getKey(); + npc = entry.getValue(); break; } } @@ -173,38 +152,16 @@ public void execute(BukkitSource source, return; } - YamlConfiguration yml = fileManager.getNpcYaml(); - ConfigurationSection parent = yml.getConfigurationSection(uuid.toString()); - if (parent == null) { - nonExistentNpcs++; - // hacky hacky way to do this - throw new RuntimeException("Catch me!"); - } - ConfigurationSection location = parent.getConfigurationSection("location"); - - Location loc; - String locString; - - //Bukkit's terrible config api didn't wipe the section - if (location != null) { - assert location != null : "Location is 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 + ")"; - } else { + Location loc = npc.getSpawnLoc(); + if (loc == null) { loc = 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)"); source.reply(Msg.translate(locale, "customnpcs.commands.fix_config.bukkit_wiped_data")); } + String locString = "(" + loc.x() + ", " + loc.y() + ", " + loc.z() + ")"; + if (strat == FixConfigWorldStrategy.SAFE_LOCATION) { @@ -225,10 +182,10 @@ public void execute(BukkitSource source, } - parent.set("location", loc); - totalFixed++; - fileManager.saveNpcFile(yml); + totalFixed++; + npc.setSpawnLoc(loc); + storageManager.track(npc); } catch (Exception ignored) { } diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/MoveCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/MoveCommand.java index 376d829a..89075dfa 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/MoveCommand.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/MoveCommand.java @@ -61,8 +61,11 @@ public void usage( assert finalNpc != null; finalNpc.teleport(p.getLocation()); finalNpc.remove(); + finalNpc.setSpawnLoc(p.getLocation()); finalNpc.createNPC(); - Bukkit.getOnlinePlayers().forEach(finalNpc::injectPlayer); + Bukkit.getOnlinePlayers().forEach(pl -> + finalNpc.getInjectionManager().markForInjection(pl.getUniqueId()) + ); } } diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/MoveDataCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/MoveDataCommand.java new file mode 100644 index 00000000..8b903d30 --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/MoveDataCommand.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2025. 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.commands; + +import dev.foxikle.customnpcs.internal.CustomNPCs; +import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; +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.velix.imperat.BukkitSource; +import dev.velix.imperat.annotations.*; +import dev.velix.imperat.command.AttachmentMode; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.spongepowered.configurate.ConfigurateException; + +import java.io.FileInputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@SubCommand(value = "movedata", attachment = AttachmentMode.MAIN) +@Permission("customnpcs.commands.movedata") +public class MoveDataCommand { + + private static final Map run_already = new HashMap<>(); + private static boolean console_confirmed = false; + + @Usage + public void usage(BukkitSource source, @Named("operation") @Suggest({"MERGE_REMOTE", "MERGE_LOCAL", "OVERWRITE"}) @Default("MERGE_LOCAL") String operation) { + if (!checkValidOperation(operation)) { + source.reply(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata.invalid_operation")); + return; + } + if (source.isConsole()) { + if (!console_confirmed) { + console_confirmed = true; + source.reply(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata.need_confirm")); + return; + } else { + console_confirmed = false; + if (operation.equals("MERGE_LOCAL")) { + merge(source, true); + } else if (operation.equals("MERGE_REMOTE")) { + merge(source, false); + } else { + overwrite(source); + } + } + } + + Player player = source.asPlayer(); + if (!run_already.containsKey(player.getUniqueId())) { + run_already.put(player.getUniqueId(), true); + source.reply(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata.need_confirm")); + return; + } + + run_already.remove(player.getUniqueId()); + + if (operation.equals("MERGE_LOCAL")) { + merge(source, true); + } else if (operation.equals("MERGE_REMOTE")) { + merge(source, false); + } else { + overwrite(source); + } + + } + + private boolean checkValidOperation(String operation) { + return operation.equalsIgnoreCase("MERGE_LOCAL") || operation.equalsIgnoreCase("MERGE_REMOTE") || operation.equalsIgnoreCase("OVERWRITE"); + } + + private boolean start(BukkitSource source) { + source.reply(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.reply(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.reply(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata.operation_failure")); + return true; + } + return false; + } + + private void merge(BukkitSource 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.reply(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata.operation_failure")); + return; + } + + List local; + try { + local = CustomNPCs.CONFIGURATE.buildAndLoadString(new String(currentData)).get(StorageManager.NPC_LIST); + } catch (ConfigurateException 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.reply(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); + }); + }); + } + + /** + * Overwrites the currently stored data with whatever is in the local file + * + * @param source the source to reply to + */ + private void overwrite(BukkitSource 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.reply(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata.operation_failure")); + return; + } + + List toWrite; + try { + toWrite = CustomNPCs.CONFIGURATE.buildAndLoadString(new String(currentData)).get(StorageManager.NPC_LIST); + } catch (ConfigurateException e) { + throw new RuntimeException(e); + } + finalizeMove(source, plugin, sm, toWrite); + }); + } + + private void finalizeMove(BukkitSource 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.reply(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata.operation_failure")); + return; + } + source.reply(Msg.translate(CommandUtils.getLocale(source), "customnpcs.commands.movedata.operation_success")); + }); + }); + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommand.java index e74a0b69..da815e2c 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommand.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommand.java @@ -25,8 +25,6 @@ import dev.velix.imperat.BukkitSource; import dev.velix.imperat.annotations.*; -import java.util.Locale; - @Command("npc") @Description("The main CustomNPCs command") @Permission("customnpcs.commands.help") @@ -35,15 +33,14 @@ CloneCommand.class, CreateCommand.class, DeleteCommand.class, EditCommand.class, FixConfigCommand.class, ListCommand.class, MoveCommand.class, ReloadCommand.class, SetsoundCommand.class, TeleportCommand.class, - WikiCommand.class, HelpCommand.class, ManageCommand.class, DebugCommand.class + WikiCommand.class, HelpCommand.class, ManageCommand.class, DebugCommand.class, + MoveDataCommand.class } ) public class NpcCommand { @Usage public void showHelp(BukkitSource sender) { - Locale locale = Locale.getDefault(); - if (!sender.isConsole()) locale = sender.asPlayer().locale(); - sender.reply(CommandUtils.getHelpComponent(locale)); + sender.reply(CommandUtils.getHelpComponent(CommandUtils.getLocale(sender))); } } 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 260dc2c8..b89a4af7 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 @@ -23,7 +23,7 @@ package dev.foxikle.customnpcs.internal.commands.suggestion; import dev.foxikle.customnpcs.internal.CustomNPCs; -import dev.foxikle.customnpcs.internal.FileManager; +import dev.foxikle.customnpcs.internal.storage.StorageManager; import dev.velix.imperat.BukkitSource; import dev.velix.imperat.command.parameters.CommandParameter; import dev.velix.imperat.context.SuggestionContext; @@ -39,7 +39,7 @@ public class NpcBrokenSuggester implements SuggestionResolver { @Override public List autoComplete(SuggestionContext context, CommandParameter parameter) { final CustomNPCs plugin = CustomNPCs.getInstance(); - FileManager fileManager = plugin.getFileManager(); - return fileManager.getBrokenNPCs().keySet().stream().map(UUID::toString).toList(); + StorageManager storageManager = plugin.getStorageManager(); + return storageManager.getBrokenNPCs().keySet().stream().map(UUID::toString).toList(); } } \ No newline at end of file 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 eef23b32..0eae9f40 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,11 @@ 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.InjectionManager; import dev.foxikle.customnpcs.internal.LookAtAnchor; import org.bukkit.Location; import org.bukkit.Particle; @@ -37,7 +40,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; /** @@ -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 getInjectionSelectionMode(); + + void setInjectionConditions(List conditions); + + void setInjectionSelectionMode(Selector mode); + /** *

Creates the NPC and injects it into every player *

@@ -307,4 +334,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 (getInjectionSelectionMode() == 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 94c44761..ace60e08 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.google.gson.JsonObject; import com.google.gson.JsonParser; 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; @@ -284,14 +284,14 @@ public void onChat(AsyncPlayerChatEvent e) { 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) { @@ -300,8 +300,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)) { @@ -507,7 +507,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")); @@ -608,7 +607,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 d19a4816..8012bd47 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/MenuItems.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuItems.java index 356996cc..03b6a038 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024-2025. Foxikle + * Copyright (c) 2024. 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 @@ -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; @@ -51,10 +50,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.SkullMeta; -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.*; @@ -86,6 +82,72 @@ 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().getName()))) + .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")) @@ -269,7 +331,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); @@ -298,7 +360,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); @@ -511,16 +573,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)); @@ -714,7 +776,7 @@ public static Button decrementDelay(Action action, Player player) { } else if (event.isRightClick()) { action.setDelay(Math.max(0, action.getDelay() - 5)); } - menuView.updateButton(1, button -> button.setItem(delayDisplay(action, p).getItem())); + menuView.updateButton(4, button -> button.setItem(delayDisplay(action, p).getItem())); })); } @@ -734,7 +796,7 @@ public static Button incrementDelay(Action action, Player player) { } Player p = (Player) event.getWhoClicked(); p.playSound(p, Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); - menuView.updateButton(1, button -> button.setItem(delayDisplay(action, p).getItem())); + menuView.updateButton(4, button -> button.setItem(delayDisplay(action, p).getItem())); })); } @@ -836,7 +898,7 @@ public static Button saveCondition(Player player) { public static Button comparatorSwitcher(Condition condition, Player player) { List lore = new ArrayList<>(); - for (Condition.Comparator c : Condition.Comparator.values()) { + for (Comparator c : Comparator.values()) { if (condition.getType() == Condition.Type.NUMERIC || (condition.getType() == Condition.Type.LOGICAL && c.isStrictlyLogical())) { if (condition.getComparator() != c) lore.add(Msg.translate(player.locale(), c.getKey()).color(NamedTextColor.GREEN)); @@ -853,8 +915,8 @@ public static Button comparatorSwitcher(Condition condition, Player player) { return Button.clickable(i, ButtonClickAction.plain((menuView, event) -> { event.setCancelled(true); - List comparators = new ArrayList<>(); - for (Condition.Comparator value : Condition.Comparator.values()) { + List comparators = new ArrayList<>(); + for (Comparator value : Comparator.values()) { if (condition.getType() == Condition.Type.LOGICAL && !value.isStrictlyLogical()) { continue; } @@ -963,7 +1025,7 @@ public static Button interactableHologram(InternalNpc npc, Player player) { p.playSound(p, Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); event.setCancelled(true); npc.getSettings().setHideClickableHologram(!hideClickableTag); - menuView.replaceButton(11, interactableHologram(npc, p)); + menuView.replaceButton(12, interactableHologram(npc, p)); })); } @@ -1015,7 +1077,6 @@ public static Button importPlayer(Player player) { Player p = (Player) event.getWhoClicked(); p.playSound(p.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); p.closeInventory(); - plugin.wait(p, WaitingType.PLAYER); new PlayerNameRunnable(p, plugin).runTaskTimer(plugin, 0, 10); event.setCancelled(true); @@ -1051,7 +1112,6 @@ public static Button importUrl(Player player) { Player p = (Player) event.getWhoClicked(); p.playSound(p.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); p.closeInventory(); - plugin.wait(p, WaitingType.URL); new UrlRunnable(p, plugin).runTaskTimer(plugin, 0, 10); event.setCancelled(true); @@ -1107,7 +1167,7 @@ public static Button newCondition(Action action, Player player) { } public static Button toggleConditionMode(Action action, Player player) { - boolean isAll = action.getMode() == Condition.SelectionMode.ALL; + boolean isAll = action.getMode() == Selector.ALL; ItemStack i = ItemBuilder.modern(isAll ? GREEN_CANDLE : RED_CANDLE) .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.conditions.mode.toggle")) .setLore(isAll ? Msg.translate(player.locale(), "customnpcs.menus.conditions.mode.all") : Msg.translate(player.locale(), "customnpcs.menus.conditions.mode.one")) @@ -1117,7 +1177,7 @@ public static Button toggleConditionMode(Action action, Player player) { Player p = (Player) event.getWhoClicked(); p.playSound(p, Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); event.setCancelled(true); - action.setMode(isAll ? Condition.SelectionMode.ONE : Condition.SelectionMode.ALL); + action.setMode(isAll ? Selector.ONE : Selector.ALL); menuView.replaceButton(35, toggleConditionMode(action, p)); })); } @@ -1158,7 +1218,7 @@ public static Button numeric(Player player) { event.setCancelled(true); Player p = (Player) event.getWhoClicked(); p.playSound(p.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); - Condition conditional = new NumericCondition(Condition.Comparator.EQUAL_TO, Condition.Value.EXP_LEVELS, 0.0); + Condition conditional = new NumericCondition(Comparator.EQUAL_TO, Condition.Value.EXP_LEVELS, 0.0); plugin.originalEditingConditionals.remove(p.getUniqueId()); plugin.editingConditionals.put(p.getUniqueId(), conditional); menuView.getAPI().openMenu(p, MenuUtils.NPC_CONDITION_CUSTOMIZER); @@ -1175,7 +1235,7 @@ public static Button logic(Player player) { event.setCancelled(true); Player p = (Player) event.getWhoClicked(); p.playSound(p.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); - Condition conditional = new LogicalCondition(Condition.Comparator.EQUAL_TO, Condition.Value.GAMEMODE, "CREATIVE"); + Condition conditional = new LogicalCondition(Comparator.EQUAL_TO, Condition.Value.GAMEMODE, "CREATIVE"); plugin.originalEditingConditionals.remove(p.getUniqueId()); plugin.editingConditionals.put(p.getUniqueId(), conditional); menuView.getAPI().openMenu(p, MenuUtils.NPC_CONDITION_CUSTOMIZER); diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinCatalog.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinCatalog.java index d9760d5b..d64b54fb 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinCatalog.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinCatalog.java @@ -64,6 +64,7 @@ public ItemStack previousPageItem(Player player) { .build(); } + /** * Handles the click sounds on page change * diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/storage/FileStorage.java b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/FileStorage.java new file mode 100644 index 00000000..4605d4c5 --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/FileStorage.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025. 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.storage; + +import dev.foxikle.customnpcs.internal.CustomNPCs; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; + +public class FileStorage implements StorageProvider { + + public static final File FILE = new File("plugins/CustomNPCs/npcs.json"); + + /** + * Creates files + */ + @Override + public CompletableFuture init(CustomNPCs plugin) { + return CompletableFuture.supplyAsync(() -> { + if (!FILE.exists()) { + FILE.getParentFile().mkdirs(); + try { + FILE.createNewFile(); + } catch (IOException e) { + throw new RuntimeException("Failed to create local storage file.", e); + } + } + plugin.getLogger().info("Successfully set up File storage!"); + return null; + }); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture save(String json) { + return CompletableFuture.supplyAsync(() -> { + try (FileOutputStream fos = new FileOutputStream(FILE)) { + fos.write(json.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException("Failed to save local storage file.", e); + } + return true; + }); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture load() { + return CompletableFuture.supplyAsync(() -> { + try (FileInputStream fis = new FileInputStream(FILE)) { + return new String(fis.readAllBytes(), StandardCharsets.UTF_8); + } catch (Exception e) { + throw new RuntimeException("Failed to load local storage file.", e); + } + }); + } + + /** + * Does nothing. + */ + @Override + public void shutdown() { + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/storage/MongoStorage.java b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/MongoStorage.java new file mode 100644 index 00000000..2e5aa97d --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/MongoStorage.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2025. 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.storage; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.ServerApi; +import com.mongodb.ServerApiVersion; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.ReplaceOptions; +import dev.foxikle.customnpcs.internal.CustomNPCs; +import org.bson.Document; +import org.bson.types.Binary; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + + +public class MongoStorage implements StorageProvider { + + private MongoClientSettings settings = null; + private String database = null; + private String document = null; + + /** + * Initializes the storage provider. This could be connecting to a database, creating files, etc. + */ + @Override + public CompletableFuture init(CustomNPCs plugin) { + + return CompletableFuture.supplyAsync(() -> { + ServerApi serverApi = ServerApi.builder() + .version(ServerApiVersion.V1) + .build(); + settings = MongoClientSettings.builder() + .applyConnectionString(new ConnectionString(Objects.requireNonNull(plugin.getConfig().getString("storage.mongo.connectionString")))) + .serverApi(serverApi) + .applyToLoggerSettings(builder -> { + builder.maxDocumentLength(1); + }) + .build(); + database = Objects.requireNonNull(plugin.getConfig().getString("storage.mongo.database")); + document = Objects.requireNonNull(plugin.getConfig().getString("storage.mongo.document")); + plugin.getLogger().info("Successfully set up MongoDB storage!"); + return null; + }); + } + + /** + * Saves the given byte array to the storage provider. It should overwrite the data. + * + * @param data The byte array to save + * @return if the save was successful + */ + @Override + public CompletableFuture save(String data) { + checkState(); + return CompletableFuture.supplyAsync(() -> { + try (MongoClient client = MongoClients.create(settings)) { + MongoDatabase db = client.getDatabase(database); + + Document doc = new Document("_id", document).append("data", data); + // replace it if it exists :) + db.getCollection("customnpcs").replaceOne(Filters.eq("_id", document), doc, new ReplaceOptions().upsert(true)); + return true; + + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + /** + * Loads the saved byte array from the storage provider + * + * @return The array of bytes, or the loaded data. + */ + @Override + public CompletableFuture load() { + checkState(); + return CompletableFuture.supplyAsync(() -> { + try (MongoClient client = MongoClients.create(settings)) { + MongoDatabase db = client.getDatabase(database); + + Document doc = db.getCollection("customnpcs").find(Filters.eq("_id", document)).first(); + if (doc == null) return ""; // no data saved + return doc.getString("data"); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + /** + * Shuts down the storage provider. This could close buffers, connections, etc. + */ + @Override + public void shutdown() { + checkState(); + } + + private void checkState() { + if (document == null) throw new IllegalStateException("Document is null"); + if (database == null) throw new IllegalStateException("Database is null"); + if (settings == null) throw new IllegalStateException("Mongo settings is not initialized"); + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/storage/MysqlStorage.java b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/MysqlStorage.java new file mode 100644 index 00000000..88a9918c --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/MysqlStorage.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2025. 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.storage; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import dev.foxikle.customnpcs.internal.CustomNPCs; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; + +@SuppressWarnings("SqlNoDataSourceInspection") +public class MysqlStorage implements StorageProvider { + + private HikariConfig config = new HikariConfig(); + private String tableName; + + /** + * Initializes the storage provider. This could be connecting to a database, creating files, etc. + * + * @param plugin the plugin instance for fetching config values + */ + @Override + @SuppressWarnings("SqlSourceToSinkFlow") + public CompletableFuture init(CustomNPCs plugin) { + config = new HikariConfig(); + config.setJdbcUrl("jdbc:mysql://" + plugin.getConfig().getString("storage.mysql.hostname") + ":" + plugin.getConfig().getString("storage.mysql.port") + "/" + plugin.getConfig().getString("storage.mysql.database")); + config.setUsername(plugin.getConfig().getString("storage.mysql.username")); + config.setPassword(plugin.getConfig().getString("storage.mysql.password")); + config.setMaximumPoolSize(10); + config.setMinimumIdle(2); + config.setIdleTimeout(30000); + config.setConnectionTimeout(3000); + config.setDriverClassName("com.mysql.cj.jdbc.Driver"); + + tableName = getSafeTableName(plugin.getConfig().getString("storage.mysql.table")); + + return CompletableFuture.supplyAsync(() -> { + try (HikariDataSource dataSource = new HikariDataSource(config)) { + try (Connection connection = dataSource.getConnection()) { + PreparedStatement statement = connection.prepareStatement("CREATE TABLE IF NOT EXISTS npc_data (id VARCHAR(255) PRIMARY KEY, data LONGTEXT)"); + statement.executeUpdate(); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Failed to create table", e); + throw new RuntimeException(e); + } + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Failed to create database connection", e); + throw new RuntimeException(e); + } + plugin.getLogger().info("Successfully set up MySQL storage!"); + return null; + }); + } + + /** + * Saves the given byte array to the storage provider. It should overwrite the data. + * + * @param data The byte array to save + * @return if the save was successful + */ + @Override + public CompletableFuture save(String data) { + return CompletableFuture.supplyAsync(() -> { + try (HikariDataSource dataSource = new HikariDataSource(config)) { + try (Connection connection = dataSource.getConnection()) { + + // Prepare the SQL INSERT statement + String sql = "INSERT INTO npc_data (id, data) VALUES (?, ?) ON DUPLICATE KEY UPDATE data = VALUES(data)"; + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, tableName); // Set the id parameter + statement.setString(2, data); // Set the data (MEDIUMBLOB) + + // Execute the insert query + statement.executeUpdate(); + return true; + } + } + } catch (Exception e) { + throw new RuntimeException("Database insertion failed", e); + } + }); + } + + /** + * Loads the saved byte array from the storage provider + * + * @return The array of bytes, or the loaded data. + */ + @Override + public CompletableFuture load() { + return CompletableFuture.supplyAsync(() -> { + try (HikariDataSource dataSource = new HikariDataSource(config)) { + try (Connection connection = dataSource.getConnection()) { + + // Prepare the SQL statement + String sql = "SELECT data FROM npc_data WHERE id = ?"; + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, tableName); // Set the id parameter + + // Execute the query + ResultSet rs = statement.executeQuery(); + String data = ""; + while (rs.next()) { + // should only run once, as primary keys are unique. + data = rs.getString("data"); + } + return data; + } + } + } catch (Exception e) { + throw new RuntimeException("Database insertion failed", e); + } + }); + } + + /** + * Shuts down the storage provider. This could close buffers, connections, etc. + */ + @Override + public void shutdown() { + + } + + private String getSafeTableName(String tableName) { + if (tableName == null || !tableName.matches("^[a-zA-Z0-9_]+$")) { + throw new IllegalArgumentException("Invalid table name: " + tableName + ". Only alphanumeric and underscores are allowed."); + } + return tableName; + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/storage/StorableNPC.java b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/StorableNPC.java new file mode 100644 index 00000000..7505035e --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/StorableNPC.java @@ -0,0 +1,53 @@ +package dev.foxikle.customnpcs.internal.storage; + +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.interfaces.InternalNpc; +import lombok.AccessLevel; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +import java.util.List; +import java.util.UUID; + +@ConfigSerializable +@Data +@NoArgsConstructor +public class StorableNPC { + @Setter(AccessLevel.NONE) + private UUID uniqueID; + @Setter(AccessLevel.NONE) + private String world; + private Settings settings; + private Equipment equipment; + private Location spawnLoc; + private List actions; + private List injectionConditions; + private Selector injectionSelector; + + public StorableNPC(InternalNpc pluginObj) { + this.uniqueID = pluginObj.getUniqueID(); + this.world = pluginObj.getWorld().getName(); + this.settings = pluginObj.getSettings(); + this.equipment = pluginObj.getEquipment(); + this.spawnLoc = pluginObj.getSpawnLoc(); + this.actions = pluginObj.getActions(); + this.injectionConditions = pluginObj.getInjectionConditions(); + this.injectionSelector = pluginObj.getInjectionSelectionMode(); + } + + public InternalNpc toPluginObject() { + World w = Bukkit.getWorld(world); + if (w == null) throw new IllegalStateException("The world " + world + " does not exist!"); + return CustomNPCs.getInstance().createNPC(w, spawnLoc, equipment, settings, uniqueID, null, actions, injectionConditions, injectionSelector); + } +} \ No newline at end of file diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/storage/StorageManager.java b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/StorageManager.java new file mode 100644 index 00000000..a5a0c8b5 --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/StorageManager.java @@ -0,0 +1,837 @@ +/* + * Copyright (c) 2024-2025. 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.storage; + +import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.actions.ActionType; +import dev.foxikle.customnpcs.actions.LegacyAction; +import dev.foxikle.customnpcs.api.Pose; +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.interfaces.InternalNpc; +import dev.foxikle.customnpcs.internal.utils.SkinUtils; +import dev.foxikle.customnpcs.internal.utils.Utils; +import dev.foxikle.customnpcs.internal.utils.exceptions.IllegalWorldException; +import dev.foxikle.customnpcs.internal.utils.exceptions.UntrackedNpcException; +import io.leangen.geantyref.TypeToken; +import lombok.Getter; +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.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.configurate.ConfigurationNode; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.stream.Collectors; + +/** + * The class that deals with all file related things + */ +@SuppressWarnings("unused") +public class StorageManager { + + public static final String[] VALID_PROVIDERS = {"LOCAL", "MONGODB", "MYSQL"}; + public static final TypeToken> NPC_LIST = new TypeToken<>() { + }; + + /** + * The config file version + */ + public static final int CONFIG_FILE_VERSION = 9; + /** + * The file version of the npcs.yml file + */ + @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "1.10") + public static final double NPC_FILE_VERSION = 1.6; + 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; + private final List trackedNpcs = new ArrayList<>(); + + private boolean justMigrated = false; + @Getter + private StorageProvider storage = null; + + /** + *

Gets the file manager object. + *

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

Creates the files the plugin needs to run + *

+ * + * @return if creating the files was successful + */ + public boolean setup() { + 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 < 9) { + 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")); + + if (version < 9) { + plugin.getLogger().log(Level.WARNING, String.format("Outdated Config version! Converting config (%d -> %d).", version, 9)); + + yml.set("DebugMode", false); + yml.setComments("DebugMode", List.of("DebugMode -> Should the plugin launch in debug mode? This can be quite spammy, so only use this if you're sure!")); + + yml.set("CONFIG_VERSION", 9); + ConfigurationSection storage = yml.createSection("storage"); + yml.setComments("storage", Utils.list("", "+---------------------------------------+", "| NPC Data Storage |", "+---------------------------------------+")); + + storage.set("provider", "LOCAL"); + storage.setComments("provider", Utils.list("", "+---------------------------------------+", "| Storage Provider |", "+---------------------------------------+", "", "The storage proivder determines how the plugin stores the NPC data. There are 3 options:", "", "\"LOCAL\" is used by default. It stores the data on the same disc the server is running on. It is a good choice if you don't want to deal with setting up a database or don't have one.", "\"MYSQL\" is a typical relational database. It's not really optimized for this kind of storage, it's a good choice if you already use a MySQL or MariaDB database.", "\"MONGODB\" is the recommended option for using remote storage. It tends to be more performant and optimized for storing BLOBs.")); + + ConfigurationSection mysql = storage.createSection("mysql"); + storage.setComments("mysql", Utils.list("", "+---------------------------------------+", "| MySQL Configuration |", "+---------------------------------------+", "These settings only matter if the provider is set to \"MYSQL\"")); + + mysql.set("hostname", "YOUR_HOST"); + mysql.setComments("hostname", Utils.list("hostname -> the host name, or ip address of your database server.")); + + mysql.set("port", 3306); + mysql.setComments("port", Utils.list("port -> The port the database runs on. Don't change this unless you know what you're doing")); + + mysql.set("username", "YOUR_USERNAME"); + mysql.setComments("username", Utils.list("username -> The database username")); + + mysql.set("password", "YOUR_PASSWORD"); + mysql.setComments("password", Utils.list("password -> The database password")); + + mysql.set("database", "YOUR_DATABASE"); + mysql.setComments("database", Utils.list("database -> The name of the database to use")); + + mysql.set("table", "npcs"); + mysql.setComments("table", Utils.list("table -> The name of the table used to store the data in. This can be used to separate your npc configurations across servers. ie: lobby, survival, etc.")); + + ConfigurationSection mongo = storage.createSection("mongo"); + storage.setComments("mongo", Utils.list("", "+---------------------------------------+", "| MongoDB Configuration |", "+---------------------------------------+", "These settings only matter if the provider is set to \"MONGODB\"")); + + mongo.set("connectionString", "YOUR_CONNECTION_STRING"); + mongo.setComments("connectionString", Utils.list("connectionString -> The connection string provided by your mongo server.")); + + mongo.set("database", "YOUR_DATABASE"); + mongo.setComments("database", Utils.list("database -> The name of the database to use")); + + mongo.set("document", "npcs"); + mongo.setComments("document", Utils.list("document -> The document to use to store the data. This can be used to separate your npc configurations across servers. ie: lobby, survival, etc.")); + + try { + yml.save(file); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // enable debug mode as quickly as possible: + plugin.setDebug(yml.getBoolean("DebugMode")); + + if (Arrays.stream(VALID_PROVIDERS).noneMatch(s -> s.equalsIgnoreCase(yml.getString("storage.provider")))) { + throw new IllegalArgumentException("Invalid config provider " + yml.getString("storage.provider")); + } + plugin.getLogger().info("Successfully loaded " + yml.getString("storage.provider") + " storage provider."); + } + + // npcs (we only care if it exists) + if (new File(PARENT_DIRECTORY, "npcs.yml").exists()) { + // make it colapsable + 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, Selector.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 { + var sec = section.getLocation("location"); + exists = sec != null; + } catch (Exception e) { + err = true; + } + + if (err || !exists) { + String rawName = plugin.getMiniMessage().stripTags(section.getString("name")); + throw new IllegalStateException("Detected an NPC (" + rawName + ") with an invalid location! Please revert to 1.7.x and use the /npc fixconfig command to fix this!"); + } else validNPCs.add(uuid); + } + } + // migrate data to the new storage provider! + plugin.getLogger().warning(" -- Migrating NPCs to dataformat 2! -- "); + + List migrated = new ArrayList<>(); + for (String key : yml.getKeys(false)) { + if (key.equals("version")) continue; // version tracking + migrated.add(migrateNPC(UUID.fromString(key))); + } + trackedNpcs.addAll(migrated); + createBackup(file); // create a backup just in case + if (!file.delete()) { + plugin.getLogger().warning("Couldn't delete old NPC file!"); + } + justMigrated = true; + } + + File configFile = new File(PARENT_DIRECTORY, "config.yml"); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + + switch (config.getString("storage.provider").toUpperCase(Locale.ROOT)) { + case "LOCAL" -> storage = new FileStorage(); + case "MYSQL" -> storage = new MysqlStorage(); + case "MONGODB" -> storage = new MongoStorage(); + default -> throw new IllegalStateException("Unknown provider: " + config.getString("storage.provider")); + } + + storage.init(plugin).whenComplete((unused, throwable) -> { + if (throwable != null) throw new RuntimeException("Couldn't load storage provider!", throwable); + if (justMigrated) saveNpcs(); + + + // load the NPCs now (skip if just migrated) + + plugin.getLogger().info("Loading NPCs!"); + AtomicInteger successfullyLoaded = new AtomicInteger(0); + if (!justMigrated) { + trackedNpcs.clear(); + getAllNpcs().whenComplete((internalNpcs, t) -> Bukkit.getScheduler().runTask(plugin, () -> { + // this needs to be sync since it adds entities + if (t != null) { + plugin.getLogger().log(Level.SEVERE, "An error occured whilst loading NPCs!", t); + return; + } + + // track the npcs + for (StorableNPC npc : internalNpcs) { + if (plugin.isDebug()) { + plugin.getLogger().info("[DEBUG] Tracking NPC " + npc.getUniqueID()); + } + track(npc); + if (loadStorable(npc)) { + successfullyLoaded.incrementAndGet(); + } else { + plugin.getLogger().log(Level.WARNING, "NPC " + npc.getUniqueID() + " could not be loaded!"); + } + } + plugin.getLogger().info("Succesfully loaded " + successfullyLoaded + " NPCs, failed to load " + (trackedNpcs.size() - successfullyLoaded.get()) + " NPCs."); + })); + } else { + trackedNpcs.forEach(npc -> { + if (loadStorable(npc)) { + successfullyLoaded.incrementAndGet(); + } else { + plugin.getLogger().log(Level.WARNING, "NPC " + npc.getUniqueID() + " could not be loaded!"); + } + }); + plugin.getLogger().info("Succesfully loaded " + successfullyLoaded + " cached NPCs, failed to load " + (trackedNpcs.size() - successfullyLoaded.get()) + " NPCs."); + } + }); + return true; + } + + public boolean loadStorable(StorableNPC npc) { + if (plugin.isDebug()) { + plugin.getLogger().info("[DEBUG] Loading NPC " + npc.getUniqueID()); + } + try { + if (loadNPC(npc.getUniqueID())) { + return true; + } else { + plugin.getLogger().warning("Failed to load NPC " + npc.getUniqueID() + "!"); + } + } catch (IllegalWorldException e) { + plugin.getLogger().log(Level.SEVERE, "Failed to load NPC " + npc.getUniqueID() + " due to an invalid world."); + } catch (UntrackedNpcException e) { + plugin.getLogger().log(Level.SEVERE, "Failed to load NPC " + npc.getUniqueID() + " as it was not tracked."); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Failed to load NPC " + npc.getUniqueID() + " due to an unknown error.", e); + } + brokenNPCs.put(npc.getUniqueID(), npc); + return false; + } + + /** + *

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

+ * + * @param npc The NPC to store + */ + public void addNPC(InternalNpc npc) { + trackedNpcs.add(new StorableNPC(npc)); + saveNpcs(); + } + + /** + *

Gets the NPC of the specified UUID + *

+ * + * @param uuid The NPC to load from the file + * @return if the load was successful + */ + public boolean loadNPC(UUID uuid) { + if (trackedNpcs.stream().noneMatch(npc -> npc.getUniqueID().equals(uuid))) { + throw new IllegalArgumentException("NPC does not exist! Track it first!"); + } + StorableNPC stored = trackedNpcs.stream().filter(n -> n.getUniqueID().equals(uuid)).findFirst().orElse(null); + if (stored == null) throw new IllegalArgumentException("NPC does not exist!"); // should never be thrown + + InternalNpc npc = stored.toPluginObject(); + if (npc.getWorld() == null) { + printInvalidConfig(); + brokenNPCs.put(uuid, stored); + return false; + } + // has to be on a sync thread + Bukkit.getScheduler().runTask(plugin, npc::createNPC); + return true; + } + + public StorableNPC migrateNPC(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 actions; + + String rawName = plugin.getMiniMessage().stripTags(section.getStringList("lines").get(0)); + World world; + + try { + world = Bukkit.getWorld(Objects.requireNonNull(section.getString("world"))); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException("The NPC '" + rawName + "' has an invalid world. Please downgrade to 1.7.x and use the /npc fixconfig command!"); + } + + Location location; + try { + location = section.getLocation("location"); + } catch (Exception ex) { + throw new IllegalArgumentException("The NPC '" + rawName + "' has an invalid location. Please downgrade to 1.7.x and use the /npc fixconfig command!"); + } + + if (world == null) + throw new IllegalArgumentException("The NPC '" + rawName + "' has an invalid world. Please downgrade to 1.7.x and use the /npc fixconfig command!"); + if (location == null) + throw new IllegalArgumentException("The NPC '" + rawName + "' has an invalid location. Please downgrade to 1.7.x and use the /npc fixconfig command!"); + + + 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, 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, new ArrayList<>(), Selector.ONE); + + return new StorableNPC(npc); + } + + @NotNull + private Pose parsePose(String str) { + if (str == null) return Pose.STANDING; + try { + return Pose.valueOf(str.toUpperCase()); + } catch (Exception e) { + return Pose.STANDING; + } + } + + + /** + *

Gets the set of stored UUIDs. + *

+ * + * @return the set of stored NPC uuids. + */ + public Set getNPCIds() { + return trackedNpcs.stream().map(npc -> npc.getUniqueID()).collect(Collectors.toSet()); + } + + /** + *

Removes the specified NPC from storage + *

+ * + * @param uuid The NPC uuid to remove + */ + public void remove(UUID uuid) { + StorableNPC stored = trackedNpcs.stream().filter(npc -> npc.getUniqueID().equals(uuid)).findFirst().orElse(null); + if (stored == null) throw new IllegalArgumentException("NPC does not exist!"); + trackedNpcs.remove(stored); + saveNpcs(); + } + + 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("| deleting a world an NPC was in. |"); + plugin.getLogger().severe("+------------------------------------------------------------------------------+"); + plugin.getLogger().severe(""); + } + + public CompletableFuture saveNpcs() { + ConfigurationNode node = CustomNPCs.CONFIGURATE.build().createNode(); + try { + node.set(NPC_LIST, trackedNpcs); + return storage.save(CustomNPCs.CONFIGURATE.buildAndSaveString(node)); + } catch (Exception e) { + plugin.getLogger().log(Level.SEVERE, "Failed to write NPC data to json!", e); + throw new RuntimeException(e); + } + } + + public void track(StorableNPC stored) { + trackedNpcs.stream().filter(npc -> npc.getUniqueID().equals(stored.getUniqueID())).findFirst().ifPresent(trackedNpcs::remove); + trackedNpcs.add(stored); + } + + /** + * Gets ALL npcs stored in the storage provider, their configurations are not validated. + */ + public CompletableFuture> getAllNpcs() { + CompletableFuture> future = new CompletableFuture<>(); + storage.load().whenComplete((json, throwable) -> { + if (throwable != null) { + future.completeExceptionally(throwable); + return; + } + try { + future.complete(CustomNPCs.CONFIGURATE.buildAndLoadString(json).get(NPC_LIST)); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + return future; + } + + public void resetTracked() { + trackedNpcs.clear(); + } + + private record BackupResult(Path filePath, boolean success) { + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/storage/StorageProvider.java b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/StorageProvider.java new file mode 100644 index 00000000..972ce1cc --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/storage/StorageProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024. 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.storage; + +import dev.foxikle.customnpcs.internal.CustomNPCs; +import org.jetbrains.annotations.ApiStatus; + +import java.util.concurrent.CompletableFuture; + +@ApiStatus.Internal +public interface StorageProvider { + /** + * Initializes the storage provider. This could be connecting to a database, creating files, etc. + */ + CompletableFuture init(CustomNPCs plugin); + + /** + * Saves the given byte array to the storage provider. It should overwrite the data. + * + * @param json The json array to save + * @return if the save was successful + */ + CompletableFuture save(String json); + + /** + * Loads the saved byte array from the storage provider + * + * @return The array of json NPC data, or the loaded data. + */ + CompletableFuture load(); + + /** + * Shuts down the storage provider. This could close buffers, connections, etc. + */ + void shutdown(); +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Utils.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Utils.java index 2cbe8d8b..4e67e246 100644 --- a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Utils.java +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Utils.java @@ -23,7 +23,7 @@ package dev.foxikle.customnpcs.internal.utils; import com.google.common.reflect.TypeToken; -import dev.foxikle.customnpcs.actions.conditions.Condition; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.interfaces.InternalNpc; import net.kyori.adventure.text.Component; diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ActionSerializer.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ActionSerializer.java new file mode 100644 index 00000000..3012825f --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ActionSerializer.java @@ -0,0 +1,26 @@ +package dev.foxikle.customnpcs.internal.utils.configurate; + +import dev.foxikle.customnpcs.actions.Action; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.lang.reflect.Type; + +public class ActionSerializer implements TypeSerializer { + @Override + public Action deserialize(@NotNull Type type, @NotNull ConfigurationNode node) throws SerializationException { + String raw = node.getString(); + if (raw == null) throw new SerializationException("Cannot deserialize a null action!"); + return Action.parse(raw); + } + + + @Override + public void serialize(@NotNull Type type, @Nullable Action obj, @NotNull ConfigurationNode node) throws SerializationException { + if (obj == null) throw new SerializationException("Cannot serialize a null action!"); + node.set(obj.serialize()); + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ColorSerializer.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ColorSerializer.java new file mode 100644 index 00000000..4b417692 --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ColorSerializer.java @@ -0,0 +1,27 @@ +package dev.foxikle.customnpcs.internal.utils.configurate; + +import io.leangen.geantyref.TypeToken; +import org.bukkit.Color; +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.lang.reflect.Type; +import java.util.Map; + +public class ColorSerializer implements TypeSerializer { + @Override + public Color deserialize(@NotNull Type type, @NotNull ConfigurationNode node) throws SerializationException { + return Color.fromARGB(node.getInt()); + } + + + @Override + public void serialize(@NotNull Type type, @Nullable Color obj, @NotNull ConfigurationNode node) throws SerializationException { + if (obj == null) throw new SerializationException("Cannot serialize a null location!"); + node.set(obj.asARGB()); + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ConditionSerializer.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ConditionSerializer.java new file mode 100644 index 00000000..b454cc8c --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ConditionSerializer.java @@ -0,0 +1,28 @@ +package dev.foxikle.customnpcs.internal.utils.configurate; + +import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; +import dev.foxikle.customnpcs.internal.CustomNPCs; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.lang.reflect.Type; + +public class ConditionSerializer implements TypeSerializer { + @Override + public Condition deserialize(@NotNull Type type, @NotNull ConfigurationNode node) throws SerializationException { + String raw = node.getString(); + if (raw == null) throw new SerializationException("Cannot deserialize a null action!"); + return Condition.of(raw); + } + + + @Override + public void serialize(@NotNull Type type, @Nullable Condition obj, @NotNull ConfigurationNode node) throws SerializationException { + if (obj == null) throw new SerializationException("Cannot serialize a null action!"); + node.set(CustomNPCs.getGson().toJson(obj)); + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ItemstackSerializer.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ItemstackSerializer.java new file mode 100644 index 00000000..e6abe2d4 --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/ItemstackSerializer.java @@ -0,0 +1,33 @@ +package dev.foxikle.customnpcs.internal.utils.configurate; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.lang.reflect.Type; + +public class ItemstackSerializer implements TypeSerializer { + @Override + @SuppressWarnings("unchecked") + public ItemStack deserialize(@NotNull Type type, @NotNull ConfigurationNode node) throws SerializationException { + byte[] data = node.get(byte[].class); + assert data != null; + if (data.length == 0) { + return new ItemStack(Material.AIR); + } + return ItemStack.deserializeBytes(data); + } + + + @Override + public void serialize(@NotNull Type type, @Nullable ItemStack obj, @NotNull ConfigurationNode node) throws SerializationException { + if (obj == null) throw new SerializationException("Cannot serialize a null location!"); + if (obj.getType() == Material.AIR) { + node.set(new byte[0]); + } else node.set(obj.serializeAsBytes()); + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/LocationSerializer.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/LocationSerializer.java new file mode 100644 index 00000000..fb3a4f6d --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/configurate/LocationSerializer.java @@ -0,0 +1,37 @@ +package dev.foxikle.customnpcs.internal.utils.configurate; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.lang.reflect.Type; + +public class LocationSerializer implements TypeSerializer { + @Override + public Location deserialize(@NotNull Type type, @NotNull ConfigurationNode node) throws SerializationException { + String world = node.node("world").getString(); + double x = node.node("x").getDouble(); + double y = node.node("y").getDouble(); + double z = node.node("z").getDouble(); + float pitch = node.node("pitch").getFloat(); + float yaw = node.node("yaw").getFloat(); + + return new Location(Bukkit.getWorld(world), x, y, z, pitch, yaw); + } + + + @Override + public void serialize(@NotNull Type type, @Nullable Location location, @NotNull ConfigurationNode node) throws SerializationException { + if (location == null) throw new SerializationException("Cannot serialize a null location!"); + node.node("world").set(location.getWorld().getName()); + node.node("x").set(location.getX()); + node.node("y").set(location.getY()); + node.node("z").set(location.getZ()); + node.node("pitch").set(location.getPitch()); + node.node("yaw").set(location.getYaw()); + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/exceptions/IllegalWorldException.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/exceptions/IllegalWorldException.java new file mode 100644 index 00000000..5be9bc5e --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/exceptions/IllegalWorldException.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025. 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.utils.exceptions; + +public class IllegalWorldException extends NpcConfigurationException { + public IllegalWorldException(String message) { + super(message); + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/exceptions/NpcConfigurationException.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/exceptions/NpcConfigurationException.java new file mode 100644 index 00000000..4626d5ed --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/exceptions/NpcConfigurationException.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025. 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.utils.exceptions; + +public class NpcConfigurationException extends RuntimeException { + public NpcConfigurationException(String message) { + super(message); + } +} diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/exceptions/UntrackedNpcException.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/exceptions/UntrackedNpcException.java new file mode 100644 index 00000000..b7974c8b --- /dev/null +++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/exceptions/UntrackedNpcException.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025. 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.utils.exceptions; + +public class UntrackedNpcException extends NpcConfigurationException { + public UntrackedNpcException(String message) { + super(message); + } +} diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index 7a2b2703..78a6805b 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -13,7 +13,7 @@ # will be automatically generated on the next server start. # DO NOT, under ANY circumstances modify the 'CONFIG_VERSION' field. Doing so can cause catastrophic data loss. -CONFIG_VERSION: 8 +CONFIG_VERSION: 9 # InjectionDistance -> How close should a player be to the NPC before the plugin attempts to inject the NPC (Due to minecraft, npcs automatically remove themselves after getting 48 blocks from the player. InjectionDistance: 48 @@ -47,6 +47,9 @@ NameReferenceMessages: true # DefaultInterpolationDuration -> How long should moving NPCs interpolate their Nametags moving? DefaultInterpolationDuration: 5 +# DebugMode -> Should the plugin launch in debug mode? This can be quite spammy, so only use this if you're sure! +DebugMode: false + ############################ # Skin API # ############################ @@ -240,7 +243,53 @@ Skins: value: ewogICJ0aW1lc3RhbXAiIDogMTY4Mzk0MDM2NjQ5MSwKICAicHJvZmlsZUlkIiA6ICJiODMyODBlODZhMzU0ZjA4OTUzMDlkMDFmMTk3OGZmOSIsCiAgInByb2ZpbGVOYW1lIiA6ICJPeGlrbGUiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvM2YwNzYwNGRmZDJiZTFmMzZhZDU5MjdhYmExZjE4ODJjYzRjM2U3ODA4MjFhMjA2M2U0YmJhMDlmZGY2NWQ2YyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9 signature: iw9jmhcoCch+yWx2Yjy9+Dg2y9MOq9Ef6rOCY2FywOfR6+/T/sGwJtM+HqLHYO+B9uhBqfKxYXVLHQNvT7KzfiYVnbsIUQSWyBR7NxGssbbFixC3SoxTsIg0x2nvidbi/IjeG0UL1ZTb2S7jhjnvVU/bwyTz/i5fQ3lnBbW172FySPewRfVh10pZogAflKXZ22Lz5pHxlR4Ed/2RW75sltmKd3j7H76f+cEm9arixjilX/NhH/1WVFUqb7W6xUd5zrGxVkNIp3a8Fn4DCDItauhqDSjRZQkxpDnky36iuh/sCvJWFcxO9hkdqvMClUS4wtRTO1aKSnQFCMuwAuraIQClYJztI4EReSlB2NF6whEHpsRIBo5zG4J6nynu77VV1p1gn7W4g6LvmP0YoTPqed5uOL/FPBC/etn0Ofj1t3CgnOY8HQBDvkhSgyUuQwzixrGiNxRuu7IU3lDvSYVIewKGSK2Qkkl/Ob1P7BZCJTSkNA3MWfA11MDLFm8AZU6Bau+epO2C7RDwaKO61hSpdcgpORHruzXskvOfoOQ+oSP9VDAeLSfjD8fLQNgUdd/QdZXMgBitS5izHT/L6UCEoM6Tm5DmQz2D+ysvPdPpbNoa39sXL08vm5HgReFTh73qUJQC/qoK2rOk1Igef1oaiH7R1X5MiMvetNYE97vkoXM= - +# +---------------------------------------+ +# | NPC Data Storage | +# +---------------------------------------+ +Storage: + + # +---------------------------------------+ + # | Storage Provider | + # +---------------------------------------+ + + # The storage proivder determines how the plugin stores the NPC data. There are 3 options: + # + # "LOCAL" is used by default. It stores the data on the same disc the server is running on. It is a good choice if you don't want to deal with setting up a database or don't have one. + # "MYSQL" is a typical relational database. It's not really optimized for this kind of storage, it's a good choice if you already use a MySQL or MariaDB database. + # "MONGODB" is the recommended option for using remote storage. It tends to be more performant and optimized for storing BLOBs. + provider: "LOCAL" + + # +---------------------------------------+ + # | MySQL Configuration | + # +---------------------------------------+ + # These settings only matter if the provider is set to "MYSQL" + mysql: + + # hostname -> the host name, or ip address of your database server. + hostname: 'YOUR_HOST' + # port -> The port the database runs on. Don't change this unless you know what you're doing + port: 3306 + # username -> The database username + username: 'YOUR_USERNAME' + # password -> The database password + password: 'YOUR_PASSWORD' + # table -> The name of the table used to store the data in. This can be used to separate your npc configurations across servers. ie: lobby, survival, etc. + table: 'npcs' + # database -> The name of the database to use + database: 'YOUR_DATABASE' + + # +---------------------------------------+ + # | MongoDB Configuration | + # +---------------------------------------+ + # These settings only matter if the provider is set to "MONGODB" + mongodb: + + # connectionString -> The connection string provided by your mongo server. + connectionString: 'YOUR_CONNECTION_STRING' + # database -> The name of the database to use + database: 'YOUR_DATABASE' + # document -> The document to use to store the data. This can be used to separate your npc configurations across servers. ie: lobby, survival, etc. + document: 'npcs' diff --git a/core/src/main/resources/localization/English_en_US.properties b/core/src/main/resources/localization/English_en_US.properties index fa88b9c1..76595932 100644 --- a/core/src/main/resources/localization/English_en_US.properties +++ b/core/src/main/resources/localization/English_en_US.properties @@ -87,10 +87,19 @@ customnpcs.commands.help.wiki.description =Sends a link to t customnpcs.commands.help.wiki.hover =Gives you access to the Wiki! customnpcs.commands.debug.message.enable =Debug mode enabled! customnpcs.commands.debug.message.disable =Debug mode disabled! -customnpcs.commands.help.debug.syntax =\u2022 npc manage +customnpcs.commands.help.debug.syntax =\u2022 npc debug customnpcs.commands.help.debug.aliases =npc debug customnpcs.commands.help.debug.description =Debugs CustomNPCs customnpcs.commands.help.debug.hover =Allows CustomNPCs to broadcast debug messages to the logs and authorized players. +customnpcs.commands.help.movedata.syntax =\u2022 npc movedata +customnpcs.commands.help.movedata.aliases =npc movedata +customnpcs.commands.help.movedata.description =Relocates any local NPC data to the currently configured storage provider +customnpcs.commands.help.movedata.hover =Stores any local NPC data in the storage provider. The three operators, MERGE_LOCAL, MERGE_REMOTE, and OVERWRITE, determine how conflicts are handled. MERGE_REMOTE tries to merge the two lists of NPCs, discarding the local duplicate (by uuid, settings, equipment, etc are not considdered). MERGE_LOCAL tries to merge the two lists, but it discards the remote duplicates. OVERWRITE completley wipes the storage provider and replaces it with the local data. +customnpcs.commands.movedata.invalid_operation =Invalid operator! Use `OVERWRITE` to replace and `MERGE_REMOTE`/`MERGE_LOCAL` to combine. +customnpcs.commands.movedata.need_confirm =WARNING! This operation cannot be undone! Run the command again to confirm. +customnpcs.commands.movedata.operation_queued =QUEUED! The data transfer has been queued, and will be completed shortly! +customnpcs.commands.movedata.operation_success =SUCCESS! The data transfer was successful. +customnpcs.commands.movedata.operation_failure =ERROR! The data transfer was not successful. Check the server logs for details. #=====================================# # Actions Messages # #=====================================# diff --git a/core/src/main/resources/paper-plugin.yml b/core/src/main/resources/paper-plugin.yml index 303d0328..6b169261 100644 --- a/core/src/main/resources/paper-plugin.yml +++ b/core/src/main/resources/paper-plugin.yml @@ -44,6 +44,7 @@ permissions: - customnpcs.run_command.enable_console - customnpcs.commands.wiki - customnpcs.commands.fix_config + - customnpcs.commands.movedata customnpcs.commands.*: description: A wildcard permission for all command permissions default: op @@ -102,6 +103,9 @@ permissions: customnpcs.alert: description: A permission to alert if there is a new update available. default: op + customnpcs.commands.movedata: + description: A permission required to use the /npc movedata command + default: op # This permission is only a child of the customnpcs.* permission. # It is incredibly important that this permission is tightly regulated diff --git a/v1_20_R1/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R1.java b/v1_20_R1/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R1.java index cd1fc194..1b72ec7b 100644 --- a/v1_20_R1/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R1.java +++ b/v1_20_R1/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R1.java @@ -26,7 +26,9 @@ import com.mojang.authlib.properties.Property; import com.mojang.datafixers.util.Pair; import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.api.Pose; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.CustomNPCs; @@ -77,54 +79,36 @@ /** * The object representing the NPC */ +@Getter public class NPC_v1_20_R1 extends ServerPlayer implements InternalNpc { - @Getter private final UUID uniqueID; - @Getter private final Particle spawnParticle = Particle.EXPLOSION_LARGE; private final CustomNPCs plugin; - @Getter private final World world; private final EntityDataAccessor TEXT_DISPLAY_ACCESSOR; private final Map loops = new HashMap<>(); - @Getter @Setter private Equipment equipment; - @Getter @Setter private Settings settings; - @Getter @Setter private Location spawnLoc; private @Nullable ArmorStand seat; - @Getter private TextDisplay clickableHologram; - @Getter private List holograms; - @Getter @Setter private Player target; - @Getter @Setter private List actions; private String clickableName = "ERROR"; private InjectionManager injectionManager; + @Setter + private List injectionConditions; + @Setter + private Selector injectionSelectionMode; - /** - *

Gets a new NPC - *

- * - * @param actions The actions for the NPC to execute on interaction - * @param plugin The instance of the Main class - * @param uniqueID The UUID of the NPC (Should be the same as the gameprofile's uuid) - * @param spawnLoc The location to spawn the NPC - * @param target The Entity the NPC should follow - * @param world The world the NPC resides in - * @param equipment The NPC's equipment - * @param settings The NPC's settings - */ - public NPC_v1_20_R1(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uniqueID, @Nullable Player target, List actions) { + public NPC_v1_20_R1(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uniqueID, @Nullable Player target, List actions, List injectionConditions, Selector injectionSelectionMode) { super(((CraftServer) Bukkit.getServer()).getServer(), ((CraftWorld) world).getHandle(), new GameProfile(uniqueID, Utils.getNpcName(settings, uniqueID))); this.spawnLoc = spawnLoc; this.equipment = equipment; @@ -135,6 +119,8 @@ public NPC_v1_20_R1(CustomNPCs plugin, World world, Location spawnLoc, Equipment this.actions = new ArrayList<>(actions); super.connection = new FakeListener_v1_20_R1(((CraftServer) Bukkit.getServer()).getServer(), new FakeConnection_v1_20_R1(PacketFlow.CLIENTBOUND), this); this.plugin = plugin; + this.injectionConditions = injectionConditions; + this.injectionSelectionMode = injectionSelectionMode; //aL is the text data accessor try { @@ -187,7 +173,7 @@ public void createNPC() { super.getBukkitEntity().setItemInHand(equipment.getHand()); setPose(setupPose(settings.getPose())); - if (settings.isResilient()) plugin.getFileManager().addNPC(this); + if (settings.isResilient()) plugin.getStorageManager().addNPC(this); plugin.addNPC(this, holograms); injectionManager = new InjectionManager(plugin, this); @@ -437,9 +423,35 @@ public void teleport(Location loc) { } + @Override + public void withdraw(Player player) { + loops.remove(player.getUniqueId()); + List> packets = new ArrayList<>(); + + if (holograms != null) { + for (TextDisplay hologram : holograms) { + packets.add(new ClientboundRemoveEntitiesPacket(hologram.getEntityId())); + } + } + + if (clickableHologram != null) { + packets.add(new ClientboundRemoveEntitiesPacket(clickableHologram.getEntityId())); + } + packets.add(new ClientboundRemoveEntitiesPacket(super.getId())); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + + + packets.forEach(connection::send); + if (plugin.isEnabled()) { + Bukkit.getScheduler().runTaskLater(plugin, () -> packets.forEach(connection::send), 1); + } + } + + @Override public void delete() { - plugin.getFileManager().remove(this.uniqueID); + plugin.getStorageManager().remove(this.uniqueID); } @Override @@ -564,7 +576,7 @@ public double getPoseOffset(Pose pose) { @Override public InternalNpc clone() { - return new NPC_v1_20_R1(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions)); + return new NPC_v1_20_R1(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions), new ArrayList<>(injectionConditions), injectionSelectionMode); } @Override diff --git a/v1_20_R2/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R2.java b/v1_20_R2/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R2.java index bf1e873e..d80ec8c3 100644 --- a/v1_20_R2/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R2.java +++ b/v1_20_R2/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R2.java @@ -26,7 +26,9 @@ import com.mojang.authlib.properties.Property; import com.mojang.datafixers.util.Pair; import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.api.Pose; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.CustomNPCs; @@ -77,41 +79,36 @@ /** * The object representing the NPC */ +@Getter public class NPC_v1_20_R2 extends ServerPlayer implements InternalNpc { - @Getter private final UUID uniqueID; - @Getter private final Particle spawnParticle = Particle.EXPLOSION_LARGE; private final CustomNPCs plugin; - @Getter private final World world; private final EntityDataAccessor TEXT_DISPLAY_ACCESSOR; private final Map loops = new HashMap<>(); - @Getter @Setter private Settings settings; - @Getter @Setter private Equipment equipment; - @Getter @Setter private Location spawnLoc; private @Nullable ArmorStand seat; - @Getter private TextDisplay clickableHologram; - @Getter private List holograms; - @Getter @Setter private Player target; - @Getter @Setter private List actions; private String clickableName = "ERROR"; + @Getter private InjectionManager injectionManager; + @Setter private List injectionConditions; + @Setter private Selector injectionSelectionMode; + - public NPC_v1_20_R2(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uniqueID, @Nullable Player target, List actions) { + public NPC_v1_20_R2(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uniqueID, @Nullable Player target, List actions, List injectionConditions, Selector injectionSelectionMode) { super(((CraftServer) Bukkit.getServer()).getServer(), ((CraftWorld) world).getHandle(), new GameProfile(uniqueID, Utils.getNpcName(settings, uniqueID)), ClientInformation.createDefault()); this.spawnLoc = spawnLoc; this.settings = settings; @@ -122,6 +119,8 @@ public NPC_v1_20_R2(CustomNPCs plugin, World world, Location spawnLoc, Equipment this.actions = new ArrayList<>(actions); super.connection = new FakeListener_v1_20_R2(((CraftServer) Bukkit.getServer()).getServer(), new FakeConnection_v1_20_R2(PacketFlow.CLIENTBOUND), this); this.plugin = plugin; + this.injectionConditions = injectionConditions; + this.injectionSelectionMode = injectionSelectionMode; //aM try { @@ -170,7 +169,7 @@ public void createNPC() { super.getBukkitEntity().setItemInHand(equipment.getHand()); setPose(setupPose(settings.getPose())); - if (settings.isResilient()) plugin.getFileManager().addNPC(this); + if (settings.isResilient()) plugin.getStorageManager().addNPC(this); plugin.addNPC(this, holograms); injectionManager = new InjectionManager(plugin, this); @@ -390,7 +389,7 @@ public void teleport(Location loc) { } public void delete() { - plugin.getFileManager().remove(this.uniqueID); + plugin.getStorageManager().remove(this.uniqueID); } @Override @@ -514,7 +513,7 @@ public double getPoseOffset(Pose pose) { @Override public InternalNpc clone() { - return new NPC_v1_20_R2(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions)); + return new NPC_v1_20_R2(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions), new ArrayList<>(injectionConditions), injectionSelectionMode); } @@ -527,5 +526,30 @@ public float getYaw() { public float getPitch() { return getXRot(); } + + @Override + public void withdraw(Player player) { + loops.remove(player.getUniqueId()); + List> packets = new ArrayList<>(); + + if (holograms != null) { + for (TextDisplay hologram : holograms) { + packets.add(new ClientboundRemoveEntitiesPacket(hologram.getEntityId())); + } + } + + if (clickableHologram != null) { + packets.add(new ClientboundRemoveEntitiesPacket(clickableHologram.getEntityId())); + } + packets.add(new ClientboundRemoveEntitiesPacket(super.getId())); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + + + packets.forEach(connection::send); + if (plugin.isEnabled()) { + Bukkit.getScheduler().runTaskLater(plugin, () -> packets.forEach(connection::send), 1); + } + } } diff --git a/v1_20_R3/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R3.java b/v1_20_R3/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R3.java index 134ae629..75a1abd8 100644 --- a/v1_20_R3/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R3.java +++ b/v1_20_R3/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R3.java @@ -26,7 +26,9 @@ import com.mojang.authlib.properties.Property; import com.mojang.datafixers.util.Pair; import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.api.Pose; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.CustomNPCs; @@ -77,42 +79,36 @@ /** * The object representing the NPC */ +@Getter public class NPC_v1_20_R3 extends ServerPlayer implements InternalNpc { // reflection for data accessors private final EntityDataAccessor TEXT_DISPLAY_ACCESSOR; - @Getter private final Particle spawnParticle = Particle.EXPLOSION_LARGE; - @Getter private final UUID uniqueID; private final CustomNPCs plugin; - @Getter private final World world; private final Map loops = new HashMap<>(); - @Getter @Setter private Settings settings; - @Getter @Setter private Equipment equipment; - @Getter @Setter private Location spawnLoc; private @Nullable ArmorStand seat; - @Getter private TextDisplay clickableHologram; - @Getter private List holograms; - @Getter @Setter private Player target; - @Getter @Setter private List actions; private String clickableName = "ERROR"; private InjectionManager injectionManager; + @Setter private List injectionConditions; + @Setter private Selector injectionSelectionMode; - public NPC_v1_20_R3(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uniqueID, @Nullable Player target, List actions) { + + public NPC_v1_20_R3(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uniqueID, @Nullable Player target, List actions, List injectionConditions, Selector injectionSelectionMode) { super(((CraftServer) Bukkit.getServer()).getServer(), ((CraftWorld) world).getHandle(), new GameProfile(uniqueID, Utils.getNpcName(settings, uniqueID)), ClientInformation.createDefault()); this.spawnLoc = spawnLoc; this.equipment = equipment; @@ -122,7 +118,9 @@ public NPC_v1_20_R3(CustomNPCs plugin, World world, Location spawnLoc, Equipment this.target = target; this.actions = actions; super.connection = new FakeListener_v1_20_R3(((CraftServer) Bukkit.getServer()).getServer(), new FakeConnection_v1_20_R3(PacketFlow.CLIENTBOUND), this); - this.plugin = plugin; + this.plugin = plugin; + this.injectionConditions = injectionConditions; + this.injectionSelectionMode = injectionSelectionMode; // aM try { @@ -174,7 +172,7 @@ public void createNPC() { super.getBukkitEntity().setItemInHand(equipment.getHand()); setPose(setupPose(settings.getPose())); - if (settings.isResilient()) plugin.getFileManager().addNPC(this); + if (settings.isResilient()) plugin.getStorageManager().addNPC(this); plugin.addNPC(this, holograms); injectionManager = new InjectionManager(plugin, this); @@ -396,7 +394,7 @@ public void teleport(Location loc) { } public void delete() { - plugin.getFileManager().remove(this.uniqueID); + plugin.getStorageManager().remove(this.uniqueID); } @Override @@ -520,7 +518,7 @@ public double getPoseOffset(Pose pose) { @Override public InternalNpc clone() { - return new NPC_v1_20_R3(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions)); + return new NPC_v1_20_R3(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions), new ArrayList<>(injectionConditions), injectionSelectionMode); } @@ -533,5 +531,30 @@ public float getYaw() { public float getPitch() { return getXRot(); } + + @Override + public void withdraw(Player player) { + loops.remove(player.getUniqueId()); + List> packets = new ArrayList<>(); + + if (holograms != null) { + for (TextDisplay hologram : holograms) { + packets.add(new ClientboundRemoveEntitiesPacket(hologram.getEntityId())); + } + } + + if (clickableHologram != null) { + packets.add(new ClientboundRemoveEntitiesPacket(clickableHologram.getEntityId())); + } + packets.add(new ClientboundRemoveEntitiesPacket(super.getId())); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + + + packets.forEach(connection::send); + if (plugin.isEnabled()) { + Bukkit.getScheduler().runTaskLater(plugin, () -> packets.forEach(connection::send), 1); + } + } } diff --git a/v1_20_R4/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R4.java b/v1_20_R4/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R4.java index dc6a1ce3..c4eb2a2a 100644 --- a/v1_20_R4/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R4.java +++ b/v1_20_R4/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_20_R4.java @@ -26,7 +26,9 @@ import com.mojang.authlib.properties.Property; import com.mojang.datafixers.util.Pair; import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.api.Pose; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.CustomNPCs; @@ -79,6 +81,7 @@ /** * The object representing the NPC */ +@Getter public class NPC_v1_20_R4 extends ServerPlayer implements InternalNpc { private static final EntityDataAccessor TEXT_DISPLAY_ACCESSOR; @@ -95,38 +98,31 @@ public class NPC_v1_20_R4 extends ServerPlayer implements InternalNpc { } - @Getter private final Particle spawnParticle = Particle.EXPLOSION; - @Getter private final UUID uniqueID; private final CustomNPCs plugin; - @Getter private final World world; private final Map loops = new HashMap<>(); - @Getter @Setter private Settings settings; - @Getter @Setter private Equipment equipment; - @Getter @Setter private Location spawnLoc; private @Nullable ArmorStand seat; - @Getter private TextDisplay clickableHologram; - @Getter private List holograms; - @Getter @Setter private Player target; - @Getter @Setter private List actions; private String clickableName = "ERROR"; - private InjectionManager injectionManager; + @Getter private InjectionManager injectionManager; + @Setter private List injectionConditions; + @Setter private Selector injectionSelectionMode; - public NPC_v1_20_R4(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uuid, @Nullable Player target, List actions) { + + public NPC_v1_20_R4(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uuid, @Nullable Player target, List actions, List injectionConditions, Selector injectionSelectionMode) { super(((CraftServer) Bukkit.getServer()).getServer(), ((CraftWorld) world).getHandle(), new GameProfile(uuid, Utils.getNpcName(settings, uuid)), ClientInformation.createDefault()); this.spawnLoc = spawnLoc; this.equipment = equipment; @@ -136,7 +132,9 @@ public NPC_v1_20_R4(CustomNPCs plugin, World world, Location spawnLoc, Equipment this.target = target; this.actions = actions; super.connection = new FakeListener_v1_20_R4(((CraftServer) Bukkit.getServer()).getServer(), new FakeConnection_v1_20_R4(PacketFlow.CLIENTBOUND), this); - this.plugin = plugin; + this.plugin = plugin; + this.injectionConditions = injectionConditions; + this.injectionSelectionMode = injectionSelectionMode; } public void setPosRot(Location location) { @@ -181,7 +179,7 @@ public void createNPC() { super.getBukkitEntity().setItemInHand(equipment.getHand()); setPose(setupPose(settings.getPose())); - if (settings.isResilient()) plugin.getFileManager().addNPC(this); + if (settings.isResilient()) plugin.getStorageManager().addNPC(this); plugin.addNPC(this, holograms); injectionManager = new InjectionManager(plugin, this); @@ -414,7 +412,7 @@ public void teleport(Location loc) { *

*/ public void delete() { - plugin.getFileManager().remove(this.uuid); + plugin.getStorageManager().remove(this.uuid); } @Override @@ -537,7 +535,7 @@ public double getPoseOffset(Pose pose) { @Override public InternalNpc clone() { - return new NPC_v1_20_R4(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions)); + return new NPC_v1_20_R4(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions), new ArrayList<>(injectionConditions), injectionSelectionMode); } @@ -550,5 +548,30 @@ public float getYaw() { public float getPitch() { return getXRot(); } + + @Override + public void withdraw(Player player) { + loops.remove(player.getUniqueId()); + List> packets = new ArrayList<>(); + + if (holograms != null) { + for (TextDisplay hologram : holograms) { + packets.add(new ClientboundRemoveEntitiesPacket(hologram.getEntityId())); + } + } + + if (clickableHologram != null) { + packets.add(new ClientboundRemoveEntitiesPacket(clickableHologram.getEntityId())); + } + packets.add(new ClientboundRemoveEntitiesPacket(super.getId())); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + + + packets.forEach(connection::send); + if (plugin.isEnabled()) { + Bukkit.getScheduler().runTaskLater(plugin, () -> packets.forEach(connection::send), 1); + } + } } diff --git a/v1_21_R0/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R0.java b/v1_21_R0/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R0.java index 361b90d0..97ee0477 100644 --- a/v1_21_R0/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R0.java +++ b/v1_21_R0/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R0.java @@ -26,7 +26,9 @@ import com.mojang.authlib.properties.Property; import com.mojang.datafixers.util.Pair; import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.api.Pose; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.CustomNPCs; @@ -76,6 +78,7 @@ import java.util.*; import java.util.stream.Stream; +@Getter public class NPC_v1_21_R0 extends ServerPlayer implements InternalNpc { // reflection for data accessors @@ -92,39 +95,33 @@ public class NPC_v1_21_R0 extends ServerPlayer implements InternalNpc { } } - @Getter private final Particle spawnParticle = Particle.EXPLOSION; - @Getter private final UUID uniqueID; private final CustomNPCs plugin; - @Getter private final World world; private final Map loops = new HashMap<>(); - @Getter @Setter private Settings settings; - @Getter @Setter private Equipment equipment; - @Getter @Setter private Location spawnLoc; private @Nullable ArmorStand seat; - @Getter private TextDisplay clickableHologram; - @Getter private List holograms; - @Getter @Setter private Player target; - @Getter @Setter - private List actions; private String clickableName = "ERROR"; private InjectionManager injectionManager; + @Setter + private List injectionConditions; + @Setter + private Selector injectionSelectionMode; - public NPC_v1_21_R0(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uniqueID, @Nullable Player target, List actions) { + + public NPC_v1_21_R0(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uniqueID, @Nullable Player target, List actions, List injectionConditions, Selector injectionSelectionMode) { super(((CraftServer) Bukkit.getServer()).getServer(), ((CraftWorld) world).getHandle(), new GameProfile(uniqueID, Utils.getNpcName(settings, uniqueID)), ClientInformation.createDefault()); this.spawnLoc = spawnLoc; this.equipment = equipment; @@ -135,6 +132,8 @@ public NPC_v1_21_R0(CustomNPCs plugin, World world, Location spawnLoc, Equipment this.actions = actions; super.connection = new FakeListener_v1_21_R0(((CraftServer) Bukkit.getServer()).getServer(), new FakeConnection_v1_21_R0(PacketFlow.CLIENTBOUND), this); this.plugin = plugin; + this.injectionConditions = injectionConditions; + this.injectionSelectionMode = injectionSelectionMode; } public void setPosRot(Location location) { @@ -178,7 +177,7 @@ public void createNPC() { super.getBukkitEntity().setItemInHand(equipment.getHand()); setPose(setupPose(settings.getPose())); - if (settings.isResilient()) plugin.getFileManager().addNPC(this); + if (settings.isResilient()) plugin.getStorageManager().addNPC(this); plugin.addNPC(this, holograms); injectionManager = new InjectionManager(plugin, this); @@ -403,7 +402,7 @@ public void teleport(Location loc) { } public void delete() { - plugin.getFileManager().remove(this.uniqueID); + plugin.getStorageManager().remove(this.uniqueID); } @Override @@ -526,7 +525,7 @@ public double getPoseOffset(Pose pose) { @Override public InternalNpc clone() { - return new NPC_v1_21_R0(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions)); + return new NPC_v1_21_R0(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions), new ArrayList<>(injectionConditions), injectionSelectionMode); } @@ -539,5 +538,30 @@ public float getYaw() { public float getPitch() { return getXRot(); } + + @Override + public void withdraw(Player player) { + loops.remove(player.getUniqueId()); + List> packets = new ArrayList<>(); + + if (holograms != null) { + for (TextDisplay hologram : holograms) { + packets.add(new ClientboundRemoveEntitiesPacket(hologram.getEntityId())); + } + } + + if (clickableHologram != null) { + packets.add(new ClientboundRemoveEntitiesPacket(clickableHologram.getEntityId())); + } + packets.add(new ClientboundRemoveEntitiesPacket(super.getId())); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + + + packets.forEach(connection::send); + if (plugin.isEnabled()) { + Bukkit.getScheduler().runTaskLater(plugin, () -> packets.forEach(connection::send), 1); + } + } } diff --git a/v1_21_R1/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R1.java b/v1_21_R1/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R1.java index 760e7844..54f50211 100644 --- a/v1_21_R1/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R1.java +++ b/v1_21_R1/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R1.java @@ -26,7 +26,9 @@ import com.mojang.authlib.properties.Property; import com.mojang.datafixers.util.Pair; import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.api.Pose; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.CustomNPCs; @@ -77,6 +79,7 @@ import java.util.*; import java.util.stream.Stream; +@Getter public class NPC_v1_21_R1 extends ServerPlayer implements InternalNpc { // reflection for data accessors @@ -93,38 +96,33 @@ public class NPC_v1_21_R1 extends ServerPlayer implements InternalNpc { } } - @Getter private final Particle spawnParticle = Particle.EXPLOSION; - @Getter private final UUID uniqueID; private final CustomNPCs plugin; - @Getter private final World world; private final Map loops = new HashMap<>(); - @Getter @Setter private Settings settings; - @Getter @Setter private Equipment equipment; @Setter - @Getter private Location spawnLoc; private @Nullable ArmorStand seat; - @Getter private TextDisplay clickableHologram; - @Getter private List holograms; - @Getter @Setter private Player target; @Setter - @Getter private List actions; private String clickableName = "ERROR"; private InjectionManager injectionManager; + @Setter + private List injectionConditions; + @Setter + private Selector injectionSelectionMode; + - public NPC_v1_21_R1(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uuid, @Nullable Player target, List actions) { + public NPC_v1_21_R1(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uuid, @Nullable Player target, List actions, List injectionConditions, Selector injectionSelectionMode) { super(((CraftServer) Bukkit.getServer()).getServer(), ((CraftWorld) world).getHandle(), new GameProfile(uuid, Utils.getNpcName(settings, uuid)), ClientInformation.createDefault()); this.spawnLoc = spawnLoc; this.equipment = equipment; @@ -135,6 +133,8 @@ public NPC_v1_21_R1(CustomNPCs plugin, World world, Location spawnLoc, Equipment this.actions = actions; super.connection = new FakeListener_v1_21_R1(((CraftServer) Bukkit.getServer()).getServer(), new FakeConnection_v1_21_R1(PacketFlow.CLIENTBOUND), this); this.plugin = plugin; + this.injectionConditions = injectionConditions; + this.injectionSelectionMode = injectionSelectionMode; } public void setPosRot(Location location) { @@ -178,7 +178,7 @@ public void createNPC() { super.getBukkitEntity().setItemInHand(equipment.getHand()); setPose(setupPose(settings.getPose())); - if (settings.isResilient()) plugin.getFileManager().addNPC(this); + if (settings.isResilient()) plugin.getStorageManager().addNPC(this); plugin.addNPC(this, holograms); injectionManager = new InjectionManager(plugin, this); @@ -403,7 +403,7 @@ public void teleport(Location loc) { } public void delete() { - plugin.getFileManager().remove(this.uniqueID); + plugin.getStorageManager().remove(this.uniqueID); } @Override @@ -526,7 +526,7 @@ public double getPoseOffset(Pose pose) { @Override public InternalNpc clone() { - return new NPC_v1_21_R1(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions)); + return new NPC_v1_21_R1(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions), new ArrayList<>(injectionConditions), injectionSelectionMode); } @@ -539,5 +539,30 @@ public float getYaw() { public float getPitch() { return getXRot(); } + + @Override + public void withdraw(Player player) { + loops.remove(player.getUniqueId()); + List> packets = new ArrayList<>(); + + if (holograms != null) { + for (TextDisplay hologram : holograms) { + packets.add(new ClientboundRemoveEntitiesPacket(hologram.getEntityId())); + } + } + + if (clickableHologram != null) { + packets.add(new ClientboundRemoveEntitiesPacket(clickableHologram.getEntityId())); + } + packets.add(new ClientboundRemoveEntitiesPacket(super.getId())); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + + + packets.forEach(connection::send); + if (plugin.isEnabled()) { + Bukkit.getScheduler().runTaskLater(plugin, () -> packets.forEach(connection::send), 1); + } + } } diff --git a/v1_21_R2/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R2.java b/v1_21_R2/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R2.java index 665f24dc..a3bb2179 100644 --- a/v1_21_R2/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R2.java +++ b/v1_21_R2/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R2.java @@ -26,7 +26,9 @@ import com.mojang.authlib.properties.Property; import com.mojang.datafixers.util.Pair; import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.api.Pose; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.CustomNPCs; @@ -80,6 +82,7 @@ /** * The object representing the NPC */ +@Getter public class NPC_v1_21_R2 extends ServerPlayer implements InternalNpc { // reflection for data accessors @@ -96,51 +99,44 @@ public class NPC_v1_21_R2 extends ServerPlayer implements InternalNpc { } } - @Getter private final UUID uniqueID; private final CustomNPCs plugin; - @Getter private final World world; private final Map loops = new HashMap<>(); - @Getter private final Particle spawnParticle = Particle.EXPLOSION; - @Getter @Setter private Settings settings; - @Getter @Setter private Equipment equipment; - @Getter @Setter private Location spawnLoc; private @Nullable ArmorStand seat; - @Getter private TextDisplay clickableHologram; - @Getter private List holograms; - @Getter @Setter private Player target; - @Getter @Setter private List actions; private String clickableName = "ERROR"; private InjectionManager injectionManager; + @Setter private List injectionConditions; + @Setter private Selector injectionSelectionMode; + /** *

Gets a new NPC *

* - * @param actions The actions for the NPC to execute on interaction - * @param plugin The instance of the Main class - * @param uniqueID The UUID of the NPC (Should be the same as the gameprofile's uuid) - * @param spawnLoc The location to spawn the NPC - * @param target The Entity the NPC should follow - * @param world The world to create the NPC in - * @param settings The settings for the NPC - * @param equipment The NPC's equipment + * @param actions The actions for the NPC to execute on interaction + * @param plugin The instance of the Main class + * @param uniqueID The UUID of the NPC (Should be the same as the gameprofile's uuid) + * @param spawnLoc The location to spawn the NPC + * @param target The Entity the NPC should follow + * @param world The world to create the NPC in + * @param settings The settings for the NPC + * @param equipment The NPC's equipment */ - public NPC_v1_21_R2(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uniqueID, @Nullable Player target, List actions) { + public NPC_v1_21_R2(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uniqueID, @Nullable Player target, List actions, List injectionConditions, Selector injectionSelectionMode) { super(((CraftServer) Bukkit.getServer()).getServer(), ((CraftWorld) world).getHandle(), new GameProfile(uniqueID, Utils.getNpcName(settings, uniqueID)), ClientInformation.createDefault()); this.spawnLoc = spawnLoc; this.equipment = equipment; @@ -150,7 +146,9 @@ public NPC_v1_21_R2(CustomNPCs plugin, World world, Location spawnLoc, Equipment this.target = target; this.actions = actions; super.connection = new FakeListener_v1_21_R2(((CraftServer) Bukkit.getServer()).getServer(), new FakeConnection_v1_21_R2(PacketFlow.CLIENTBOUND), this); - this.plugin = plugin; + this.plugin = plugin; + this.injectionConditions = injectionConditions; + this.injectionSelectionMode = injectionSelectionMode; } /** @@ -204,7 +202,7 @@ public void createNPC() { super.getBukkitEntity().setItemInHand(equipment.getHand()); setPose(setupPose(settings.getPose())); - if (settings.isResilient()) plugin.getFileManager().addNPC(this); + if (settings.isResilient()) plugin.getStorageManager().addNPC(this); plugin.addNPC(this, holograms); injectionManager = new InjectionManager(plugin, this); @@ -233,12 +231,7 @@ public void setupHolograms() { hologram.setBillboard(Display.Billboard.CENTER); hologram.addScoreboardTag("npcHologram"); hologram.setTeleportDuration(settings.getInterpolationDuration()); - hologram.setTransformation(new Transformation( - new Vector3f(0, (float) y, 0), - hologram.getTransformation().getLeftRotation(), - hologram.getTransformation().getScale(), - hologram.getTransformation().getRightRotation() - )); + hologram.setTransformation(new Transformation(new Vector3f(0, (float) y, 0), hologram.getTransformation().getLeftRotation(), hologram.getTransformation().getScale(), hologram.getTransformation().getRightRotation())); holograms.add(hologram); ((CraftTextDisplay) hologram).getHandle().startRiding(this, true); } @@ -254,12 +247,7 @@ public void setupClickableHologram(String name) { clickableHologram.addScoreboardTag("npcHologram"); clickableHologram.setTeleportDuration(settings.getInterpolationDuration()); - clickableHologram.setTransformation(new Transformation( - new Vector3f(0, (float) getPoseOffset(settings.getPose()), 0), - clickableHologram.getTransformation().getLeftRotation(), - clickableHologram.getTransformation().getScale(), - clickableHologram.getTransformation().getRightRotation() - )); + clickableHologram.setTransformation(new Transformation(new Vector3f(0, (float) getPoseOffset(settings.getPose()), 0), clickableHologram.getTransformation().getLeftRotation(), clickableHologram.getTransformation().getScale(), clickableHologram.getTransformation().getRightRotation())); ((CraftTextDisplay) clickableHologram).getHandle().startRiding(this, true); } @@ -293,8 +281,7 @@ public void injectPlayer(Player p) { stuffs.add(new Pair<>(net.minecraft.world.entity.EquipmentSlot.FEET, CraftItemStack.asNMSCopy(equipment.getBoots()))); ClientboundPlayerInfoUpdatePacket playerInfoAdd = new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, this); - ClientboundAddEntityPacket namedEntitySpawn = new ClientboundAddEntityPacket(getId(), uniqueID, getCurrentLocation().x(), getCurrentLocation().y(), getCurrentLocation().z(), - getYRot(), getXRot(), net.minecraft.world.entity.EntityType.PLAYER, 0, new Vec3(0, 0, 0), getYRot()); + ClientboundAddEntityPacket namedEntitySpawn = new ClientboundAddEntityPacket(getId(), uniqueID, getCurrentLocation().x(), getCurrentLocation().y(), getCurrentLocation().z(), getYRot(), getXRot(), net.minecraft.world.entity.EntityType.PLAYER, 0, new Vec3(0, 0, 0), getYRot()); ClientboundPlayerInfoRemovePacket playerInforemove = new ClientboundPlayerInfoRemovePacket(Collections.singletonList(super.getUUID())); ClientboundSetEquipmentPacket equipmentPacket = new ClientboundSetEquipmentPacket(super.getId(), stuffs); ClientboundMoveEntityPacket rotation = new ClientboundMoveEntityPacket.Rot(this.getBukkitEntity().getEntityId(), (byte) (getYRot() * 256 / 360), (byte) (getXRot() * 256 / 360), true); @@ -429,7 +416,7 @@ public void teleport(Location loc) { } public void delete() { - plugin.getFileManager().remove(this.uniqueID); + plugin.getStorageManager().remove(this.uniqueID); } @Override @@ -471,8 +458,7 @@ public void reloadSettings() { holograms.clear(); } - if (clickableHologram != null) - clickableHologram.remove(); + if (clickableHologram != null) clickableHologram.remove(); setPose(setupPose(settings.getPose())); @@ -552,7 +538,7 @@ public double getPoseOffset(Pose pose) { @Override public InternalNpc clone() { - return new NPC_v1_21_R2(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions)); + return new NPC_v1_21_R2(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions), new ArrayList<>(injectionConditions), injectionSelectionMode); } @@ -565,5 +551,30 @@ public float getYaw() { public float getPitch() { return getXRot(); } + + @Override + public void withdraw(Player player) { + loops.remove(player.getUniqueId()); + List> packets = new ArrayList<>(); + + if (holograms != null) { + for (TextDisplay hologram : holograms) { + packets.add(new ClientboundRemoveEntitiesPacket(hologram.getEntityId())); + } + } + + if (clickableHologram != null) { + packets.add(new ClientboundRemoveEntitiesPacket(clickableHologram.getEntityId())); + } + packets.add(new ClientboundRemoveEntitiesPacket(super.getId())); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + + + packets.forEach(connection::send); + if (plugin.isEnabled()) { + Bukkit.getScheduler().runTaskLater(plugin, () -> packets.forEach(connection::send), 1); + } + } } diff --git a/v1_21_R3/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R3.java b/v1_21_R3/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R3.java index fde836bf..2995bd27 100644 --- a/v1_21_R3/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R3.java +++ b/v1_21_R3/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R3.java @@ -26,7 +26,9 @@ import com.mojang.authlib.properties.Property; import com.mojang.datafixers.util.Pair; import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.api.Pose; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.CustomNPCs; @@ -81,6 +83,7 @@ /** * The object representing the NPC */ +@Getter public class NPC_v1_21_R3 extends ServerPlayer implements InternalNpc { // reflection for data accessors @@ -97,36 +100,29 @@ public class NPC_v1_21_R3 extends ServerPlayer implements InternalNpc { } } - @Getter private final UUID uniqueID; private final CustomNPCs plugin; - @Getter private final World world; private final Map loops = new HashMap<>(); - @Getter @Setter private Settings settings; - @Getter @Setter private Equipment equipment; - @Getter @Setter private Location spawnLoc; - @Getter private @Nullable TextDisplay clickableHologram; - @Getter private List holograms; private @Nullable ArmorStand seat; - @Getter @Setter private Player target; - @Getter @Setter private List actions; private String clickableName = "ERROR"; private InjectionManager injectionManager; + @Setter private List injectionConditions; + @Setter private Selector injectionSelectionMode; - public NPC_v1_21_R3(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uuid, @Nullable Player target, List actions) { + public NPC_v1_21_R3(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uuid, @Nullable Player target, List actions, List injectionConditions, Selector injectionSelectionMode) { super(((CraftServer) Bukkit.getServer()).getServer(), ((CraftWorld) world).getHandle(), new GameProfile(uuid, Utils.getNpcName(settings, uuid)), ClientInformation.createDefault()); this.spawnLoc = spawnLoc; this.equipment = equipment; @@ -137,6 +133,8 @@ public NPC_v1_21_R3(CustomNPCs plugin, World world, Location spawnLoc, Equipment this.actions = actions; super.connection = new FakeListener_v1_21_R3(((CraftServer) Bukkit.getServer()).getServer(), new FakeConnection_v1_21_R3(PacketFlow.CLIENTBOUND), this); this.plugin = plugin; + this.injectionConditions = injectionConditions; + this.injectionSelectionMode = injectionSelectionMode; } @Override @@ -183,7 +181,7 @@ public void createNPC() { super.getBukkitEntity().setItemInHand(equipment.getHand()); setPose(setupPose(settings.getPose())); - if (settings.isResilient()) plugin.getFileManager().addNPC(this); + if (settings.isResilient()) plugin.getStorageManager().addNPC(this); plugin.addNPC(this, holograms); injectionManager = new InjectionManager(plugin, this); @@ -212,12 +210,7 @@ public void setupHolograms() { hologram.setBillboard(Display.Billboard.CENTER); hologram.addScoreboardTag("npcHologram"); hologram.setTeleportDuration(settings.getInterpolationDuration()); - hologram.setTransformation(new Transformation( - new Vector3f(0, (float) y, 0), - hologram.getTransformation().getLeftRotation(), - hologram.getTransformation().getScale(), - hologram.getTransformation().getRightRotation() - )); + hologram.setTransformation(new Transformation(new Vector3f(0, (float) y, 0), hologram.getTransformation().getLeftRotation(), hologram.getTransformation().getScale(), hologram.getTransformation().getRightRotation())); holograms.add(hologram); ((CraftTextDisplay) hologram).getHandle().startRiding(this, true); } @@ -233,12 +226,7 @@ public void setupClickableHologram(String name) { clickableHologram.addScoreboardTag("npcHologram"); clickableHologram.setTeleportDuration(settings.getInterpolationDuration()); - clickableHologram.setTransformation(new Transformation( - new Vector3f(0, (float) getPoseOffset(settings.getPose()), 0), - clickableHologram.getTransformation().getLeftRotation(), - clickableHologram.getTransformation().getScale(), - clickableHologram.getTransformation().getRightRotation() - )); + clickableHologram.setTransformation(new Transformation(new Vector3f(0, (float) getPoseOffset(settings.getPose()), 0), clickableHologram.getTransformation().getLeftRotation(), clickableHologram.getTransformation().getScale(), clickableHologram.getTransformation().getRightRotation())); ((CraftTextDisplay) clickableHologram).getHandle().startRiding(this, true); } @@ -272,8 +260,7 @@ public void injectPlayer(Player p) { stuffs.add(new Pair<>(net.minecraft.world.entity.EquipmentSlot.FEET, CraftItemStack.asNMSCopy(equipment.getBoots()))); ClientboundPlayerInfoUpdatePacket playerInfoAdd = new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, this); - ClientboundAddEntityPacket namedEntitySpawn = new ClientboundAddEntityPacket(getId(), uniqueID, spawnLoc.x(), spawnLoc.y(), spawnLoc.z(), - getYRot(), getXRot(), net.minecraft.world.entity.EntityType.PLAYER, 0, new Vec3(0, 0, 0), getYRot()); + ClientboundAddEntityPacket namedEntitySpawn = new ClientboundAddEntityPacket(getId(), uniqueID, spawnLoc.x(), spawnLoc.y(), spawnLoc.z(), getYRot(), getXRot(), net.minecraft.world.entity.EntityType.PLAYER, 0, new Vec3(0, 0, 0), getYRot()); ClientboundPlayerInfoRemovePacket playerInforemove = new ClientboundPlayerInfoRemovePacket(Collections.singletonList(super.getUUID())); ClientboundSetEquipmentPacket equipmentPacket = new ClientboundSetEquipmentPacket(super.getId(), stuffs); ClientboundMoveEntityPacket rotation = new ClientboundMoveEntityPacket.Rot(this.getBukkitEntity().getEntityId(), (byte) (getYRot() * 256 / 360), (byte) (getXRot() * 256 / 360), true); @@ -411,7 +398,7 @@ public void teleport(Location loc) { } public void delete() { - plugin.getFileManager().remove(this.uniqueID); + plugin.getStorageManager().remove(this.uniqueID); } @Override @@ -453,8 +440,7 @@ public void reloadSettings() { holograms.clear(); } - if (clickableHologram != null) - clickableHologram.remove(); + if (clickableHologram != null) clickableHologram.remove(); setPose(setupPose(settings.getPose())); @@ -550,8 +536,34 @@ private double getPoseOffset(Pose pose) { case SLEEPING -> 0.10D; }; } + @Override public InternalNpc clone() { - return new NPC_v1_21_R3(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions)); + return new NPC_v1_21_R3(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions), new ArrayList<>(injectionConditions), injectionSelectionMode); + } + + @Override + public void withdraw(Player player) { + loops.remove(player.getUniqueId()); + List> packets = new ArrayList<>(); + + if (holograms != null) { + for (TextDisplay hologram : holograms) { + packets.add(new ClientboundRemoveEntitiesPacket(hologram.getEntityId())); + } + } + + if (clickableHologram != null) { + packets.add(new ClientboundRemoveEntitiesPacket(clickableHologram.getEntityId())); + } + packets.add(new ClientboundRemoveEntitiesPacket(super.getId())); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + + + packets.forEach(connection::send); + if (plugin.isEnabled()) { + Bukkit.getScheduler().runTaskLater(plugin, () -> packets.forEach(connection::send), 1); + } } } diff --git a/v1_21_R4/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R4.java b/v1_21_R4/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R4.java index 24711976..39845286 100644 --- a/v1_21_R4/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R4.java +++ b/v1_21_R4/src/main/java/dev/foxikle/customnpcs/versions/NPC_v1_21_R4.java @@ -28,7 +28,9 @@ import com.mojang.datafixers.util.Pair; import com.mojang.serialization.JsonOps; import dev.foxikle.customnpcs.actions.Action; +import dev.foxikle.customnpcs.conditions.Condition; import dev.foxikle.customnpcs.api.Pose; +import dev.foxikle.customnpcs.conditions.Selector; import dev.foxikle.customnpcs.data.Equipment; import dev.foxikle.customnpcs.data.Settings; import dev.foxikle.customnpcs.internal.CustomNPCs; @@ -82,6 +84,7 @@ /** * The object representing the NPC */ +@Getter public class NPC_v1_21_R4 extends ServerPlayer implements InternalNpc { // reflection for data accessors @@ -98,36 +101,30 @@ public class NPC_v1_21_R4 extends ServerPlayer implements InternalNpc { } } - @Getter private final UUID uniqueID; private final CustomNPCs plugin; - @Getter private final World world; private final Map loops = new HashMap<>(); - @Getter @Setter private Settings settings; - @Getter @Setter private Equipment equipment; - @Getter @Setter private Location spawnLoc; - @Getter private @Nullable TextDisplay clickableHologram; - @Getter private List holograms; private @Nullable ArmorStand seat; - @Getter @Setter private Player target; - @Getter @Setter private List actions; private String clickableName = "ERROR"; private InjectionManager injectionManager; + @Setter private List injectionConditions; + @Setter private Selector injectionSelectionMode; - public NPC_v1_21_R4(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uuid, @Nullable Player target, List actions) { + + public NPC_v1_21_R4(CustomNPCs plugin, World world, Location spawnLoc, Equipment equipment, Settings settings, UUID uuid, @Nullable Player target, List actions, List injectionConditions, Selector injectionSelectionMode) { super(((CraftServer) Bukkit.getServer()).getServer(), ((CraftWorld) world).getHandle(), new GameProfile(uuid, Utils.getNpcName(settings, uuid)), ClientInformation.createDefault()); this.spawnLoc = spawnLoc; this.equipment = equipment; @@ -138,6 +135,8 @@ public NPC_v1_21_R4(CustomNPCs plugin, World world, Location spawnLoc, Equipment this.actions = actions; super.connection = new FakeListener_v1_21_R4(((CraftServer) Bukkit.getServer()).getServer(), new FakeConnection_v1_21_R4(PacketFlow.CLIENTBOUND), this); this.plugin = plugin; + this.injectionConditions = injectionConditions; + this.injectionSelectionMode = injectionSelectionMode; } @Override @@ -184,7 +183,7 @@ public void createNPC() { super.getBukkitEntity().setItemInHand(equipment.getHand()); setPose(setupPose(settings.getPose())); - if (settings.isResilient()) plugin.getFileManager().addNPC(this); + if (settings.isResilient()) plugin.getStorageManager().addNPC(this); plugin.addNPC(this, holograms); injectionManager = new InjectionManager(plugin, this); @@ -416,7 +415,7 @@ public void teleport(Location loc) { } public void delete() { - plugin.getFileManager().remove(this.uniqueID); + plugin.getStorageManager().remove(this.uniqueID); } @Override @@ -559,6 +558,31 @@ private double getPoseOffset(Pose pose) { @Override public InternalNpc clone() { - return new NPC_v1_21_R4(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions)); + return new NPC_v1_21_R4(plugin, world, spawnLoc.clone(), equipment.clone(), settings.clone(), UUID.randomUUID(), target, new ArrayList<>(actions), new ArrayList<>(injectionConditions), injectionSelectionMode); + } + + @Override + public void withdraw(Player player) { + loops.remove(player.getUniqueId()); + List> packets = new ArrayList<>(); + + if (holograms != null) { + for (TextDisplay hologram : holograms) { + packets.add(new ClientboundRemoveEntitiesPacket(hologram.getEntityId())); + } + } + + if (clickableHologram != null) { + packets.add(new ClientboundRemoveEntitiesPacket(clickableHologram.getEntityId())); + } + packets.add(new ClientboundRemoveEntitiesPacket(super.getId())); + + ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection; + + + packets.forEach(connection::send); + if (plugin.isEnabled()) { + Bukkit.getScheduler().runTaskLater(plugin, () -> packets.forEach(connection::send), 1); + } } }