diff --git a/api/src/main/java/de/oliver/fancynpcs/api/FancyNpcsPlugin.java b/api/src/main/java/de/oliver/fancynpcs/api/FancyNpcsPlugin.java index 82faf21c..9d44ac12 100644 --- a/api/src/main/java/de/oliver/fancynpcs/api/FancyNpcsPlugin.java +++ b/api/src/main/java/de/oliver/fancynpcs/api/FancyNpcsPlugin.java @@ -1,7 +1,7 @@ package de.oliver.fancynpcs.api; -import de.oliver.fancylib.LanguageConfig; import de.oliver.fancylib.serverSoftware.schedulers.FancyScheduler; +import de.oliver.fancylib.translations.Translator; import org.bukkit.Bukkit; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; @@ -32,5 +32,5 @@ static FancyNpcsPlugin get() { AttributeManager getAttributeManager(); - LanguageConfig getLanguageConfig(); + Translator getTranslator(); } diff --git a/api/src/main/java/de/oliver/fancynpcs/api/Npc.java b/api/src/main/java/de/oliver/fancynpcs/api/Npc.java index 92d80114..1ab20eb0 100644 --- a/api/src/main/java/de/oliver/fancynpcs/api/Npc.java +++ b/api/src/main/java/de/oliver/fancynpcs/api/Npc.java @@ -2,11 +2,12 @@ import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; import de.oliver.fancylib.RandomUtils; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.api.events.NpcInteractEvent; import de.oliver.fancynpcs.api.events.NpcInteractEvent.InteractionType; +import de.oliver.fancynpcs.api.util.Interval; +import de.oliver.fancynpcs.api.util.Interval.Unit; import me.dave.chatcolorhandler.ChatColorHandler; import me.dave.chatcolorhandler.ModernChatColorHandler; import me.dave.chatcolorhandler.parsers.custom.PlaceholderAPIParser; @@ -16,7 +17,6 @@ import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; -import java.text.DecimalFormat; import java.util.List; import java.util.Map; import java.util.Random; @@ -26,13 +26,12 @@ public abstract class Npc { private static final NpcAttribute INVISIBLE_ATTRIBUTE = FancyNpcsPlugin.get().getAttributeManager().getAttributeByName(EntityType.PLAYER, "invisible"); - private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("##.##"); private static final char[] localNameChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'k', 'l', 'm', 'n', 'o', 'r'}; protected final Map isTeamCreated = new ConcurrentHashMap<>(); protected final Map isVisibleForPlayer = new ConcurrentHashMap<>(); protected final Map isLookingAtPlayer = new ConcurrentHashMap<>(); protected final Map lastPlayerInteraction = new ConcurrentHashMap<>(); - private final LanguageConfig lang = FancyNpcsPlugin.get().getLanguageConfig(); + private final Translator translator = FancyNpcsPlugin.get().getTranslator(); protected NpcData data; protected boolean saveToFile; @@ -150,22 +149,17 @@ public void interact(Player player) { public void interact(Player player, InteractionType interactionType) { if (data.getInteractionCooldown() > 0) { - if (lastPlayerInteraction.containsKey(player.getUniqueId())) { - long nextAllowedInteraction = lastPlayerInteraction.get(player.getUniqueId()) + Math.round(data.getInteractionCooldown() * 1000L); - if (nextAllowedInteraction > System.currentTimeMillis()) { - if (!FancyNpcsPlugin.get().getFancyNpcConfig().isInteractionCooldownMessageDisabled()) { - float timeLeft = (nextAllowedInteraction - System.currentTimeMillis()) / 1000F; - String cooldownMessage = lang.get("on-interaction-cooldown", "time", DECIMAL_FORMAT.format(timeLeft)); - MessageHelper.warning(player, cooldownMessage); - } - return; - } + final long interactionCooldownMillis = (long) (data.getInteractionCooldown() * 1000); + final long lastInteractionMillis = lastPlayerInteraction.getOrDefault(player.getUniqueId(), 0L); + final Interval interactionCooldownLeft = Interval.between(lastInteractionMillis + interactionCooldownMillis, System.currentTimeMillis(), Unit.MILLISECONDS); + if (interactionCooldownLeft.as(Unit.MILLISECONDS) > 0 && !FancyNpcsPlugin.get().getFancyNpcConfig().isInteractionCooldownMessageDisabled()) { + translator.translate("interaction_on_cooldown").replace("time", interactionCooldownLeft.toString()).send(player); + return; } - lastPlayerInteraction.put(player.getUniqueId(), System.currentTimeMillis()); } - NpcInteractEvent npcInteractEvent = new NpcInteractEvent(this, data.getPlayerCommands(), data.getServerCommand(), data.getOnClick(), player, interactionType); + NpcInteractEvent npcInteractEvent = new NpcInteractEvent(this, data.getPlayerCommands(), data.getServerCommands(), data.getOnClick(), player, interactionType); npcInteractEvent.callEvent(); if (npcInteractEvent.isCancelled()) { @@ -190,8 +184,7 @@ public void interact(Player player, InteractionType interactionType) { } // serverCommand - if (data.getServerCommand() != null && data.getServerCommand().length() > 0) { - String command = data.getServerCommand(); + for (String command : data.getServerCommands()) { command = command.replace("{player}", player.getName()); String finalCommand = ChatColorHandler.translate(command, player, List.of(PlaceholderAPIParser.class)); diff --git a/api/src/main/java/de/oliver/fancynpcs/api/NpcData.java b/api/src/main/java/de/oliver/fancynpcs/api/NpcData.java index 668d2d74..b40717f9 100644 --- a/api/src/main/java/de/oliver/fancynpcs/api/NpcData.java +++ b/api/src/main/java/de/oliver/fancynpcs/api/NpcData.java @@ -28,8 +28,8 @@ public class NpcData { private Map equipment; private Consumer onClick; private boolean turnToPlayer; - private String serverCommand; private List playerCommands; + private List serverCommands; private List messages; private boolean sendMessagesRandomly; private float interactionCooldown; @@ -55,7 +55,7 @@ public NpcData( Consumer onClick, List messages, boolean sendMessagesRandomly, - String serverCommand, + List serverCommands, List playerCommands, float interactionCooldown, Map attributes, @@ -76,7 +76,7 @@ public NpcData( this.equipment = equipment; this.onClick = onClick; this.turnToPlayer = turnToPlayer; - this.serverCommand = serverCommand; + this.serverCommands = serverCommands; this.playerCommands = playerCommands; this.messages = messages; this.sendMessagesRandomly = sendMessagesRandomly; @@ -105,6 +105,7 @@ public NpcData(String name, UUID creator, Location location) { }; this.turnToPlayer = false; this.messages = new ArrayList<>(); + this.serverCommands = new ArrayList<>(); this.playerCommands = new ArrayList<>(); this.sendMessagesRandomly = false; this.interactionCooldown = 0; @@ -253,16 +254,26 @@ public NpcData setTurnToPlayer(boolean turnToPlayer) { return this; } - public String getServerCommand() { - return serverCommand; + public List getServerCommands() { + return serverCommands; } - public NpcData setServerCommand(String serverCommand) { - this.serverCommand = serverCommand; + public NpcData setServerCommands(List serverCommands) { + this.serverCommands = serverCommands; isDirty = true; return this; } + public void addServerCommand(String command) { + serverCommands.add(command); + isDirty = true; + } + + public void removeServerCommand(int index) { + serverCommands.remove(index); + isDirty = true; + } + public List getPlayerCommands() { return playerCommands; } diff --git a/api/src/main/java/de/oliver/fancynpcs/api/events/NpcInteractEvent.java b/api/src/main/java/de/oliver/fancynpcs/api/events/NpcInteractEvent.java index 20c87795..17a04e64 100644 --- a/api/src/main/java/de/oliver/fancynpcs/api/events/NpcInteractEvent.java +++ b/api/src/main/java/de/oliver/fancynpcs/api/events/NpcInteractEvent.java @@ -5,13 +5,12 @@ import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.function.Consumer; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - /** * Is fired when a player interacts with a NPC */ @@ -23,7 +22,7 @@ public class NpcInteractEvent extends Event implements Cancellable { @Nullable private final List playerCommands; @Nullable - private final String serverCommand; + private final List serverCommands; @NotNull private final Consumer onClick; @NotNull @@ -31,10 +30,10 @@ public class NpcInteractEvent extends Event implements Cancellable { private final InteractionType interactionType; private boolean isCancelled; - public NpcInteractEvent(@NotNull Npc npc, @Nullable List playerCommands, @Nullable String serverCommand, @NotNull Consumer onClick, @NotNull Player player, @NotNull InteractionType interactionType) { + public NpcInteractEvent(@NotNull Npc npc, @Nullable List playerCommands, @Nullable List serverCommands, @NotNull Consumer onClick, @NotNull Player player, @NotNull InteractionType interactionType) { this.npc = npc; this.playerCommands = playerCommands; - this.serverCommand = serverCommand; + this.serverCommands = serverCommands; this.onClick = onClick; this.player = player; this.interactionType = interactionType; @@ -59,10 +58,10 @@ public static HandlerList getHandlerList() { } /** - * @return the command that the server will run + * @return the commands that the server will run */ - public @Nullable String getServerCommand() { - return serverCommand; + public @Nullable List getServerCommands() { + return serverCommands; } /** @@ -104,7 +103,9 @@ public void setCancelled(boolean cancel) { public enum InteractionType { LEFT_CLICK, RIGHT_CLICK, - /** {@link InteractionType#CUSTOM InteractionType#CUSTOM} represents interactions invoked by the API. */ + /** + * {@link InteractionType#CUSTOM InteractionType#CUSTOM} represents interactions invoked by the API. + */ CUSTOM } diff --git a/api/src/main/java/de/oliver/fancynpcs/api/events/NpcModifyEvent.java b/api/src/main/java/de/oliver/fancynpcs/api/events/NpcModifyEvent.java index 552a0594..fc8f3670 100644 --- a/api/src/main/java/de/oliver/fancynpcs/api/events/NpcModifyEvent.java +++ b/api/src/main/java/de/oliver/fancynpcs/api/events/NpcModifyEvent.java @@ -78,21 +78,38 @@ public void setCancelled(boolean cancel) { } public enum NpcModification { - LOCATION, - SKIN, + ATTRIBUTE, + COLLIDABLE, DISPLAY_NAME, EQUIPMENT, - SERVER_COMMAND, - PLAYER_COMMAND, - SHOW_IN_TAB, GLOWING, GLOWING_COLOR, - TURN_TO_PLAYER, - CUSTOM_MESSAGE, - TYPE, - ATTRIBUTE, - COLLIDABLE, INTERACTION_COOLDOWN, + LOCATION, MIRROR_SKIN, + PLAYER_COMMAND, + SERVER_COMMAND, + SHOW_IN_TAB, + SKIN, + TURN_TO_PLAYER, + TYPE, + // Messages. + MESSAGE_ADD, + MESSAGE_SET, + MESSAGE_REMOVE, + MESSAGE_CLEAR, + MESSAGE_SEND_RANDOMLY, + // Player commands. + PLAYER_COMMAND_ADD, + PLAYER_COMMAND_SET, + PLAYER_COMMAND_REMOVE, + PLAYER_COMMAND_CLEAR, + PLAYER_COMMAND_SEND_RANDOMLY, + // Server commands. + SERVER_COMMAND_ADD, + SERVER_COMMAND_SET, + SERVER_COMMAND_REMOVE, + SERVER_COMMAND_CLEAR, + SERVER_COMMAND_SEND_RANDOMLY } } diff --git a/api/src/main/java/de/oliver/fancynpcs/api/util/Interval.java b/api/src/main/java/de/oliver/fancynpcs/api/util/Interval.java new file mode 100644 index 00000000..04ef6610 --- /dev/null +++ b/api/src/main/java/de/oliver/fancynpcs/api/util/Interval.java @@ -0,0 +1,219 @@ +/* + * MIT License + * + * Copyright (c) 2023 Grabsky <44530932+Grabsky@users.noreply.github.com> + * + * 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 + * HORS 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 de.oliver.fancynpcs.api.util; + +import java.time.Instant; +import java.util.Date; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static de.oliver.fancynpcs.api.util.Interval.Unit.DAYS; +import static de.oliver.fancynpcs.api.util.Interval.Unit.HOURS; +import static de.oliver.fancynpcs.api.util.Interval.Unit.MILLISECONDS; +import static de.oliver.fancynpcs.api.util.Interval.Unit.MINUTES; +import static de.oliver.fancynpcs.api.util.Interval.Unit.MONTHS; +import static de.oliver.fancynpcs.api.util.Interval.Unit.SECONDS; +import static de.oliver.fancynpcs.api.util.Interval.Unit.YEARS; + +/** + * {@link Interval} is simple (but not very extensible) object that provides methods for + * unit conversion and creation of human-readable 'elapsed time' strings. + * + * @apiNote This API is for internal use only and can change at any time. + */ +@ApiStatus.Internal +public final class Interval { + + private final long value; + + public Interval(final long value) { + this.value = value; + } + + /** + * Returns {@link Interval} object of current time. + */ + public static @NotNull Interval now() { + return new Interval(System.currentTimeMillis()); + } + + /** + * Returns {@link Interval} object constructed from provided {@link Long long} {@code (interval)}. + * It is expected that provided value is already a difference between two timestamps. + */ + public static @NotNull Interval of(final long interval, final @NotNull Unit unit) { + return new Interval(interval * unit.factor); + } + + /** + * Returns {@link Interval} object constructed from provided {@link Double double} {@code (interval)}. + * It is expected that provided value is already a difference between two timestamps. + */ + public static @NotNull Interval of(final double interval, final @NotNull Unit unit) { + return new Interval(Math.round(interval * unit.factor)); + } + + /** + * Returns {@link Interval} of time between {@code n} and {@code m}. + */ + public static @NotNull Interval between(final long n, final long m, final @NotNull Unit unit) { + return new Interval((n - m) * unit.factor); + } + + /** + * Returns {@link Interval} of time between {@code n} and {@code m}. + */ + public static @NotNull Interval between(final double n, final double m, final @NotNull Unit unit) { + return new Interval(Math.round((n - m) * unit.factor)); + } + + /** + * Returns interval converted to specified {@link Unit} {@code (unit)}.
+ *
+     * Interval.of(1500, Interval.Unit.MILLISECONDS).as(Interval.Unit.SECONDS) // 1.5F
+     * Interval.of(300, Interval.Unit.SECONDS).as(Interval.Unit.MINUTES) // 5F
+     * 
+ */ + public double as(final @NotNull Unit unit) { + return (double) (value / unit.factor); + } + + /** + * Returns a copy of (this) {@link Interval} with {@code n} of {@link Unit} added. + */ + public @NotNull Interval add(final @NotNull Interval other) { + return new Interval(this.value + other.value); + } + + /** + * Returns a copy of (this) {@link Interval} with {@code n} of {@link Unit} added. + */ + public @NotNull Interval add(final long n, final @NotNull Unit unit) { + return new Interval(this.value + (n * unit.factor)); + } + + /** + * Returns a copy of (this) {@link Interval} with {@code n} of {@link Unit} removed. + */ + public @NotNull Interval remove(final @NotNull Interval other) { + return new Interval(this.value - other.value); + } + + /** + * Returns a copy of (this) {@link Interval} with {@code n} of {@link Unit} removed. + */ + public @NotNull Interval remove(final long n, final @NotNull Unit unit) { + return new Interval(this.value - (n * unit.factor)); + } + + /** + * Returns new {@link Date} created from (this) {@link Interval}. + */ + public @NotNull Date toDate() { + return new Date(this.value); + } + + /** + * Returns new {@link Instant} created from (this) {@link Interval}. + */ + public @NotNull Instant toInstant() { + return Instant.ofEpochMilli(this.value); + } + + /** + * Returns formatted {@link String} expressing this {@link Interval}. + *
+     * final Interval i = Interval.between(lastJoinedMillis, currentTimeMillis, Interval.Unit.MILLISECONDS);
+     * System.out.println(i.toString()) + " ago"; // eg. '1d 7h 32min 10s ago'
+     * 
+ */ + @Override + public @NotNull String toString() { + // Returning milliseconds for values below 1000. (less than one second) + if (value < 1000) + return value % YEARS.getFactor() % MONTHS.getFactor() % DAYS.getFactor() % HOURS.getFactor() % MINUTES.getFactor() % SECONDS.getFactor() / MILLISECONDS.getFactor() + "ms";; + // Calculation values, the ugly way. + final long years = value / YEARS.getFactor(); + final long months = value % YEARS.getFactor() / MONTHS.getFactor(); + final long days = value % YEARS.getFactor() % MONTHS.getFactor() / DAYS.getFactor(); + final long hours = value % YEARS.getFactor() % MONTHS.getFactor() % DAYS.getFactor() / HOURS.getFactor(); + final long minutes = value % YEARS.getFactor() % MONTHS.getFactor() % DAYS.getFactor() % HOURS.getFactor() / MINUTES.getFactor(); + final long seconds = value % YEARS.getFactor() % MONTHS.getFactor() % DAYS.getFactor() % HOURS.getFactor() % MINUTES.getFactor() / SECONDS.getFactor(); + // Creating a new output StringBuilder object. + final StringBuilder builder = new StringBuilder(); + // Appending to the StringBuilder. + if (years > 0L) builder.append(years).append("y "); + if (months > 0L) builder.append(months).append("mo "); + if (days > 0L) builder.append(days).append("d "); + if (hours > 0L) builder.append(hours).append("h "); + if (minutes > 0L) builder.append(minutes).append("min "); + if (seconds > 0L) builder.append(seconds).append("s"); + // Removing last character if a whitespace. + if (builder.charAt(builder.length() - 1) == ' ') + builder.deleteCharAt(builder.length() - 1); + // Building a String and returning. + return builder.toString(); + } + + public enum Unit { + MILLISECONDS(1L, "ms"), + TICKS(50L, "t"), + SECONDS(1_000L, "s"), + MINUTES(60_000L, "min"), + HOURS(3_600_000L, "h"), + DAYS(86_400_000L, "d"), + MONTHS(2_629_800_000L, "mo"), + YEARS(31_557_600_000L, "y"); + + private final long factor; + private final String shortCode; + + Unit(final long factor, final @NotNull String shortCode) { + this.factor = factor; + this.shortCode = shortCode; + } + + public long getFactor() { + return factor; + } + + public @NotNull String getShortCode() { + return shortCode; + } + + /** Returns {@link Unit} or {@code null} from provided short code. */ + public static @Nullable Unit fromShortCode(final @NotNull String shortCode) { + // Iterating over all units and finding one that matches provided short code. + for (final Unit unit : Unit.values()) + if (unit.shortCode.equalsIgnoreCase(shortCode) == true) + return unit; + // Unit has not been found. Returning null. + return null; + } + + } + +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e9cb58cb..dff8557b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,6 @@ allprojects { maven(url = "https://repo.papermc.io/repository/maven-public/") maven(url = "https://repo.fancyplugins.de/releases") maven(url = "https://repo.smrt-1.com/releases") - maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") } } @@ -42,6 +41,10 @@ dependencies { implementation("de.oliver:FancyLib:${findProperty("fancyLibVersion")}") compileOnly("me.dave:ChatColorHandler:${findProperty("chatcolorhandlerVersion")}") + implementation("org.incendo:cloud-core:${findProperty("cloudCoreVersion")}") + implementation("org.incendo:cloud-paper:${findProperty("cloudPaperVersion")}") + implementation("org.incendo:cloud-annotations:${findProperty("cloudAnnotationsVersion")}") + annotationProcessor("org.incendo:cloud-annotations:${findProperty("cloudAnnotationsVersion")}") compileOnly("com.intellectualsites.plotsquared:plotsquared-core:${findProperty("plotsquaredVersion")}") } @@ -115,6 +118,8 @@ tasks { compileJava { options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything options.release = 21 + // For cloud-annotations, see https://cloud.incendo.org/annotations/#command-components + options.compilerArgs.add("-parameters") } javadoc { diff --git a/gradle.properties b/gradle.properties index 1b8eaf7a..5070d8e7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,7 @@ minecraftVersion=1.20.6 -fancyLibVersion=1.0.20 +fancyLibVersion=1.0.24 plotsquaredVersion=7.2.0 -chatcolorhandlerVersion=v2.5.3 \ No newline at end of file +chatcolorhandlerVersion=v2.5.3 +cloudCoreVersion=2.0.0-rc.1 +cloudPaperVersion=2.0.0-beta.7 +cloudAnnotationsVersion=2.0.0-rc.1 \ No newline at end of file diff --git a/src/main/java/de/oliver/fancynpcs/FancyNpcs.java b/src/main/java/de/oliver/fancynpcs/FancyNpcs.java index 1e3eeb55..c15b6071 100644 --- a/src/main/java/de/oliver/fancynpcs/FancyNpcs.java +++ b/src/main/java/de/oliver/fancynpcs/FancyNpcs.java @@ -1,12 +1,15 @@ package de.oliver.fancynpcs; -import de.oliver.fancylib.*; +import de.oliver.fancylib.FancyLib; +import de.oliver.fancylib.Metrics; +import de.oliver.fancylib.VersionConfig; import de.oliver.fancylib.featureFlags.FeatureFlag; import de.oliver.fancylib.featureFlags.FeatureFlagConfig; import de.oliver.fancylib.serverSoftware.ServerSoftware; import de.oliver.fancylib.serverSoftware.schedulers.BukkitScheduler; import de.oliver.fancylib.serverSoftware.schedulers.FancyScheduler; import de.oliver.fancylib.serverSoftware.schedulers.FoliaScheduler; +import de.oliver.fancylib.translations.Language; import de.oliver.fancylib.translations.TextConfig; import de.oliver.fancylib.translations.Translator; import de.oliver.fancylib.versionFetcher.MasterVersionFetcher; @@ -15,9 +18,13 @@ import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.NpcData; import de.oliver.fancynpcs.api.NpcManager; -import de.oliver.fancynpcs.commands.FancyNpcsCMD; -import de.oliver.fancynpcs.commands.npc.NpcCMD; -import de.oliver.fancynpcs.listeners.*; +import de.oliver.fancynpcs.commands.CloudCommandManager; +import de.oliver.fancynpcs.listeners.PlayerChangedWorldListener; +import de.oliver.fancynpcs.listeners.PlayerJoinListener; +import de.oliver.fancynpcs.listeners.PlayerNpcsListener; +import de.oliver.fancynpcs.listeners.PlayerQuitListener; +import de.oliver.fancynpcs.listeners.PlayerTeleportListener; +import de.oliver.fancynpcs.listeners.PlayerUseUnknownEntityListener; import de.oliver.fancynpcs.tracker.TurnToPlayerTracker; import de.oliver.fancynpcs.tracker.VisibilityTracker; import de.oliver.fancynpcs.v1_19_4.Npc_1_19_4; @@ -29,15 +36,9 @@ import de.oliver.fancynpcs.v1_20_6.Npc_1_20_6; import org.apache.maven.artifact.versioning.ComparableVersion; import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; -import java.util.Arrays; -import java.util.Collection; import java.util.function.Function; public class FancyNpcs extends JavaPlugin implements FancyNpcsPlugin { @@ -48,10 +49,10 @@ public class FancyNpcs extends JavaPlugin implements FancyNpcsPlugin { private static FancyNpcs instance; private final FancyScheduler scheduler; private final FancyNpcsConfigImpl config; - private final LanguageConfig languageConfig; private final VersionConfig versionConfig; private final FeatureFlagConfig featureFlagConfig; private final VersionFetcher versionFetcher; + private CloudCommandManager commandManager; private TextConfig textConfig; private Translator translator; private Function npcAdapter; @@ -67,7 +68,6 @@ public FancyNpcs() { : new BukkitScheduler(instance); this.config = new FancyNpcsConfigImpl(); this.versionFetcher = new MasterVersionFetcher(getName()); - this.languageConfig = new LanguageConfig(this); this.versionConfig = new VersionConfig(this, versionFetcher); this.featureFlagConfig = new FeatureFlagConfig(this); } @@ -107,8 +107,6 @@ public void onLoad() { pluginManager.disablePlugin(this); return; } - - new FileUtils().saveFile(this, "lang.yml"); } @Override @@ -125,21 +123,13 @@ public void onEnable() { attributeManager = new AttributeManagerImpl(); - // Load language file - String defaultLang = new FileUtils().readResource("lang.yml"); - if (defaultLang != null) { - // Update language file - try { - FileConfiguration defaultLangConfig = new YamlConfiguration(); - defaultLangConfig.loadFromString(defaultLang); - for (String key : defaultLangConfig.getConfigurationSection("messages").getKeys(false)) { - languageConfig.addDefaultLang(key, defaultLangConfig.getString("messages." + key)); - } - } catch (InvalidConfigurationException e) { - e.printStackTrace(); - } - } - languageConfig.load(); + textConfig = new TextConfig("#E33239", "#AD1D23", "#81E366", "#E3CA66", "#E36666", ""); + translator = new Translator(textConfig); + translator.loadLanguages(getDataFolder().getAbsolutePath()); + final Language selectedLanguage = translator.getLanguages().stream() + .filter(language -> language.getLanguageName().equals(config.getLanguage())) + .findFirst().orElse(translator.getFallbackLanguage()); + translator.setSelectedLanguage(selectedLanguage); versionConfig.load(); @@ -176,15 +166,6 @@ public void onEnable() { PluginManager pluginManager = Bukkit.getPluginManager(); usingPlotSquared = pluginManager.isPluginEnabled("PlotSquared"); - // register commands - final Collection commands = Arrays.asList(new FancyNpcsCMD(), new NpcCMD()); - if (config.isRegisterCommands()) { - commands.forEach(command -> getServer().getCommandMap().register("fancynpcs", command)); - } else { - commands.stream().filter(Command::isRegistered).forEach(command -> - command.unregister(getServer().getCommandMap())); - } - // register listeners pluginManager.registerEvents(new PlayerJoinListener(), instance); pluginManager.registerEvents(new PlayerQuitListener(), instance); @@ -217,6 +198,12 @@ public void onEnable() { if (config.isEnableAutoSave() && config.getAutoSaveInterval() > 0) { scheduler.runTaskTimerAsynchronously(60L * 20L, autosaveInterval * 60L * 20L, () -> npcManager.saveNpcs(false)); } + // Creating new instance of CloudCommandManager and registering all needed components. + // NOTE: Brigadier is disabled by default. More detailed information about that can be found in CloudCommandManager class. + commandManager = new CloudCommandManager(this, false) + .registerArguments() + .registerExceptionHandlers() + .registerCommands(); } @Override @@ -256,14 +243,15 @@ public FancyNpcsConfigImpl getFancyNpcConfig() { return config; } - public LanguageConfig getLanguageConfig() { - return languageConfig; - } - public VersionConfig getVersionConfig() { return versionConfig; } + public CloudCommandManager getCommandManager() { + return commandManager; + } + + @Override public Translator getTranslator() { return translator; } diff --git a/src/main/java/de/oliver/fancynpcs/FancyNpcsConfigImpl.java b/src/main/java/de/oliver/fancynpcs/FancyNpcsConfigImpl.java index 9d5baaf3..ca19e7b2 100644 --- a/src/main/java/de/oliver/fancynpcs/FancyNpcsConfigImpl.java +++ b/src/main/java/de/oliver/fancynpcs/FancyNpcsConfigImpl.java @@ -8,6 +8,12 @@ import java.util.stream.Collectors; public class FancyNpcsConfigImpl implements FancyNpcsConfig { + + /** + * Currently active/selected language. + */ + private String language; + /** * Indicates whether interaction cooldown messages are disabled. */ @@ -59,6 +65,9 @@ public void reload() { FancyNpcs.getInstance().reloadConfig(); FileConfiguration config = FancyNpcs.getInstance().getConfig(); + language = (String) ConfigHelper.getOrDefault(config, "language", "default"); + config.setInlineComments("language", List.of("Language to use for translatable messages.")); + disabledInteractionCooldownMessage = (boolean) ConfigHelper.getOrDefault(config, "disable_interaction_cooldown_message", false); config.setInlineComments("disable_interaction_cooldown_message", List.of("Whether interaction cooldown messages are disabled.")); @@ -102,6 +111,10 @@ public void reload() { FancyNpcs.getInstance().saveConfig(); } + public String getLanguage() { + return language; + } + public boolean isInteractionCooldownMessageDisabled() { return disabledInteractionCooldownMessage; } diff --git a/src/main/java/de/oliver/fancynpcs/NpcManagerImpl.java b/src/main/java/de/oliver/fancynpcs/NpcManagerImpl.java index 4a5eba6d..62a04287 100644 --- a/src/main/java/de/oliver/fancynpcs/NpcManagerImpl.java +++ b/src/main/java/de/oliver/fancynpcs/NpcManagerImpl.java @@ -153,9 +153,11 @@ public void saveNpcs(boolean force) { npcConfig.set("npcs." + data.getId() + ".glowingColor", data.getGlowingColor().toString()); npcConfig.set("npcs." + data.getId() + ".turnToPlayer", data.isTurnToPlayer()); npcConfig.set("npcs." + data.getId() + ".messages", data.getMessages()); - npcConfig.set("npcs." + data.getId() + ".message", null); //TODO: remove in 2.0.9 + npcConfig.set("npcs." + data.getId() + ".message", null); //TODO: remove in 2.1.1 npcConfig.set("npcs." + data.getId() + ".playerCommands", data.getPlayerCommands()); - npcConfig.set("npcs." + data.getId() + ".playerCommand", null); //TODO: remove in 2.0.9 + npcConfig.set("npcs." + data.getId() + ".playerCommand", null); //TODO: remove in 2.1.1 + npcConfig.set("npcs." + data.getId() + ".serverCommands", data.getServerCommands()); + npcConfig.set("npcs." + data.getId() + ".serverCommand", null); //TODO: remove in 2.1.1 npcConfig.set("npcs." + data.getId() + ".sendMessagesRandomly", data.isSendMessagesRandomly()); npcConfig.set("npcs." + data.getId() + ".interactionCooldown", data.getInteractionCooldown()); npcConfig.set("npcs." + data.getId() + ".mirrorSkin", data.isMirrorSkin()); @@ -172,10 +174,6 @@ public void saveNpcs(boolean force) { } } - if (data.getServerCommand() != null) { - npcConfig.set("npcs." + data.getId() + ".serverCommand", data.getServerCommand()); - } - for (NpcAttribute attribute : FancyNpcs.getInstance().getAttributeManager().getAllAttributesForEntityType(data.getType())) { String value = data.getAttributes().getOrDefault(attribute, null); npcConfig.set("npcs." + data.getId() + ".attributes." + attribute.getName(), value); @@ -255,14 +253,16 @@ public void loadNpcs() { NamedTextColor glowingColor = NamedTextColor.NAMES.value(npcConfig.getString("npcs." + id + ".glowingColor", "white")); boolean turnToPlayer = npcConfig.getBoolean("npcs." + id + ".turnToPlayer"); boolean sendMessagesRandomly = npcConfig.getBoolean("npcs." + id + ".sendMessagesRandomly", false); - String serverCommand = npcConfig.getString("npcs." + id + ".serverCommand"); - @Deprecated(since = "2.0.8") String playerCommand = npcConfig.getString("npcs." + id + ".playerCommand"); //TODO: remove in 2.0.9 + @Deprecated(since = "2.0.8") String playerCommand = npcConfig.getString("npcs." + id + ".playerCommand"); //TODO: remove in 2.1.1 List playerCommands = npcConfig.getStringList("npcs." + id + ".playerCommands"); - @Deprecated(since = "2.0.7") String message = npcConfig.getString("npcs." + id + ".message"); // TODO: remove in 2.0.9 + @Deprecated(since = "2.0.7") String message = npcConfig.getString("npcs." + id + ".message"); // TODO: remove in 2.1.1 List messages = npcConfig.getStringList("npcs." + id + ".messages"); + @Deprecated(since = "2.0.10") String serverCommand = npcConfig.getString("npcs." + id + ".serverCommand"); // TODO: remove in 2.1.1 + List serverCommands = npcConfig.getStringList("npcs." + id + ".serverCommands"); + float interactionCooldown = (float) npcConfig.getDouble("npcs." + id + ".interactionCooldown", 0); boolean mirrorSkin = npcConfig.getBoolean("npcs." + id + ".mirrorSkin"); @@ -295,7 +295,13 @@ public void loadNpcs() { playerCommands.add(playerCommand); } - NpcData data = new NpcData(id, name, creator, displayName, skin, location, showInTab, spawnEntity, collidable, glowing, glowingColor, type, new HashMap<>(), turnToPlayer, null, messages, sendMessagesRandomly, serverCommand, playerCommands, interactionCooldown, attributes, mirrorSkin); + // TODO: remove when the 'serverCommand' field is removed, and just pass in the 'serverCommands' + if (serverCommands.isEmpty() && serverCommand != null && !serverCommand.isEmpty()) { + serverCommands = new ArrayList<>(); + serverCommands.add(serverCommand); + } + + NpcData data = new NpcData(id, name, creator, displayName, skin, location, showInTab, spawnEntity, collidable, glowing, glowingColor, type, new HashMap<>(), turnToPlayer, null, messages, sendMessagesRandomly, serverCommands, playerCommands, interactionCooldown, attributes, mirrorSkin); Npc npc = npcAdapter.apply(data); if (npcConfig.isConfigurationSection("npcs." + id + ".equipment")) { diff --git a/src/main/java/de/oliver/fancynpcs/commands/CloudCommandManager.java b/src/main/java/de/oliver/fancynpcs/commands/CloudCommandManager.java new file mode 100644 index 00000000..be57289f --- /dev/null +++ b/src/main/java/de/oliver/fancynpcs/commands/CloudCommandManager.java @@ -0,0 +1,232 @@ +package de.oliver.fancynpcs.commands; + +import de.oliver.fancylib.translations.Translator; +import de.oliver.fancylib.translations.message.Message; +import de.oliver.fancynpcs.FancyNpcs; +import de.oliver.fancynpcs.commands.arguments.LocationArgument; +import de.oliver.fancynpcs.commands.arguments.NpcArgument; +import de.oliver.fancynpcs.commands.exceptions.ReplyingParseException; +import de.oliver.fancynpcs.commands.npc.AttributeCMD; +import de.oliver.fancynpcs.commands.npc.CollidableCMD; +import de.oliver.fancynpcs.commands.npc.CopyCMD; +import de.oliver.fancynpcs.commands.npc.CreateCMD; +import de.oliver.fancynpcs.commands.npc.DisplayNameCMD; +import de.oliver.fancynpcs.commands.npc.EquipmentCMD; +import de.oliver.fancynpcs.commands.npc.FixCMD; +import de.oliver.fancynpcs.commands.npc.GlowingCMD; +import de.oliver.fancynpcs.commands.npc.InfoCMD; +import de.oliver.fancynpcs.commands.npc.InteractionCooldownCMD; +import de.oliver.fancynpcs.commands.npc.ListCMD; +import de.oliver.fancynpcs.commands.npc.MessageCMD; +import de.oliver.fancynpcs.commands.npc.MoveHereCMD; +import de.oliver.fancynpcs.commands.npc.MoveToCMD; +import de.oliver.fancynpcs.commands.npc.NearbyCMD; +import de.oliver.fancynpcs.commands.npc.HelpCMD; +import de.oliver.fancynpcs.commands.npc.PlayerCommandCMD; +import de.oliver.fancynpcs.commands.npc.RemoveCMD; +import de.oliver.fancynpcs.commands.npc.ServerCommandCMD; +import de.oliver.fancynpcs.commands.npc.ShowInTabCMD; +import de.oliver.fancynpcs.commands.npc.SkinCMD; +import de.oliver.fancynpcs.commands.npc.TeleportCMD; +import de.oliver.fancynpcs.commands.npc.TurnToPlayerCMD; +import de.oliver.fancynpcs.commands.npc.TypeCMD; +import de.oliver.fancynpcs.utils.GlowingColor; +import io.leangen.geantyref.TypeToken; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; +import org.incendo.cloud.annotations.AnnotationParser; +import org.incendo.cloud.bukkit.CloudBukkitCapabilities; +import org.incendo.cloud.bukkit.parser.WorldParser; +import org.incendo.cloud.bukkit.parser.location.LocationParser; +import org.incendo.cloud.component.CommandComponent; +import org.incendo.cloud.exception.ArgumentParseException; +import org.incendo.cloud.exception.InvalidCommandSenderException; +import org.incendo.cloud.exception.InvalidSyntaxException; +import org.incendo.cloud.exception.NoPermissionException; +import org.incendo.cloud.exception.handling.ExceptionHandlerRegistration; +import org.incendo.cloud.exception.parsing.NumberParseException; +import org.incendo.cloud.exception.parsing.ParserException; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.paper.PaperCommandManager; +import org.incendo.cloud.parser.standard.BooleanParser; +import org.incendo.cloud.parser.standard.EnumParser; + +import java.util.Optional; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.incendo.cloud.exception.handling.ExceptionHandler.unwrappingHandler; + +// DEV NOTES: +// - For the time being, due to the reasons below, Brigadier integration should be OFF by default: +// a) Argument pop-ups don't work properly, they don't appear most of the time. +// b) Suggestions are supplied per-whitespace, and not per-argument. +public final class CloudCommandManager { + + private final @NotNull FancyNpcs plugin; + + private final @NotNull PaperCommandManager commandManager; + private final @NotNull AnnotationParser annotationParser; + + public CloudCommandManager(final @NotNull FancyNpcs plugin, final boolean isBrigadier) { + this.plugin = plugin; + // Creating instance of Cloud's PaperCommandManager, which is used for anything command-related. + this.commandManager = PaperCommandManager.createNative(plugin, ExecutionCoordinator.simpleCoordinator()); + // Registering Brigadier, if available. + if (isBrigadier && commandManager.hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) + commandManager.registerBrigadier(); + // Creating instance of AnnotationParser, which is used for parsing and registering commands. + this.annotationParser = new AnnotationParser<>(commandManager, CommandSender.class); + } + + /** + * Registers arguments (parsers and suggestion providers) to the {@link PaperCommandManager}. + */ + public @NotNull CloudCommandManager registerArguments() { + annotationParser.parse(NpcArgument.INSTANCE); + annotationParser.parse(LocationArgument.INSTANCE); + // Returning this instance of CloudCommandManager to keep "builder-like" flow. + return this; + } + + /** + * Registers exception handlers to the {@link PaperCommandManager}. + */ + public @NotNull CloudCommandManager registerExceptionHandlers() { + final Translator translator = plugin.getTranslator(); + // Unwrapping some causes of ArgumentParseException to be handled in standalone exception handlers. + commandManager.exceptionController().registerHandler(ArgumentParseException.class, unwrappingHandler(NumberParseException.class)); + commandManager.exceptionController().registerHandler(ArgumentParseException.class, unwrappingHandler(BooleanParser.BooleanParseException.class)); + commandManager.exceptionController().registerHandler(ArgumentParseException.class, unwrappingHandler(EnumParser.EnumParseException.class)); + commandManager.exceptionController().registerHandler(ArgumentParseException.class, unwrappingHandler(WorldParser.WorldParseException.class)); + commandManager.exceptionController().registerHandler(ArgumentParseException.class, unwrappingHandler(ReplyingParseException.class)); + // Overriding some default handlers to send specialized messages. + commandManager.exceptionController().registerHandler(NoPermissionException.class, (exceptionContext) -> { + translator.translate("command_missing_permissions").send(exceptionContext.context().sender()); + }); + // DEV NOTE: No need to compare sender types until we decide to make a console-only command. Should get the job done for the time being. + commandManager.exceptionController().registerHandler(InvalidCommandSenderException.class, (exceptionContext) -> { + translator.translate("command_player_only").send(exceptionContext.context().sender()); + }); + commandManager.exceptionController().registerHandler(NumberParseException.class, (exceptionContext) -> { + translator.translate("command_invalid_number") + .replaceStripped("input", exceptionContext.exception().input()) + .replace("min", exceptionContext.exception().range().min().toString()) + .replace("max", exceptionContext.exception().range().max().toString()) + .send(exceptionContext.context().sender()); + }); + commandManager.exceptionController().registerHandler(BooleanParser.BooleanParseException.class, (exceptionContext) -> { + translator.translate("command_invalid_boolean") + .replaceStripped("input", exceptionContext.exception().input()) + .send(exceptionContext.context().sender()); + }); + commandManager.exceptionController().registerHandler(WorldParser.WorldParseException.class, (exceptionContext) -> { + translator.translate("command_invalid_world") + .replaceStripped("input", exceptionContext.exception().input()) + .send(exceptionContext.context().sender()); + }); + // DEV NOTE: Temporary solution util https://github.com/Incendo/cloud-minecraft/pull/70 is merged. + commandManager.exceptionController().register(ExceptionHandlerRegistration.builder(TypeToken.get(ArgumentParseException.class)) + .exceptionFilter(exception -> exception.getCause() instanceof ParserException parserException && parserException.argumentParserClass() == LocationParser.class) + .exceptionHandler(exceptionContext -> { + final ParserException exception = (ParserException) exceptionContext.exception().getCause(); + final String input = exception.captionVariables()[0].value(); // Should never throw. + translator.translate("command_invalid_location") + .replaceStripped("input", !input.isBlank() ? input : "N/A") // Under certain conditions, input is not passed to the exception. + .send(exceptionContext.context().sender()); + }).build() + ); + commandManager.exceptionController().registerHandler(EnumParser.EnumParseException.class, (exceptionContext) -> { + String translationKey = "command_invalid_enum_generic"; + // Comparing exception enum class and choosing specialized messages. + if (exceptionContext.exception().enumClass() == ListCMD.SortType.class) + translationKey = "command_invalid_list_sort_type"; + else if (exceptionContext.exception().enumClass() == NearbyCMD.SortType.class) + translationKey = "command_invalid_nearby_sort_type"; + else if (exceptionContext.exception().enumClass() == EntityType.class) + translationKey = "command_invalid_entity_type"; + else if (exceptionContext.exception().enumClass() == GlowingColor.class) + translationKey = "command_invalid_glowing_color"; + // Sending error message to the sender. In case no specialized message has been found, a generic one is used instead. + translator.translate(translationKey) + .replaceStripped("input", exceptionContext.exception().input()) + .replace("enum", exceptionContext.exception().enumClass().getSimpleName().toLowerCase()) + .send(exceptionContext.context().sender()); + }); + // ReplyingParseException is thrown from custom argument types and is handled there. + commandManager.exceptionController().registerHandler(ReplyingParseException.class, context -> context.exception().runnable().run()); + // InvalidSyntaxException is thrown when user specified syntax don't match any command. + commandManager.exceptionController().registerHandler(InvalidSyntaxException.class, (exceptionContext) -> { + // Creating a StringBuilder which is then appended with (known/existing) command literals. + final StringBuilder translationKeyBuilder = new StringBuilder("command_syntax."); + // Iterating over current command chain and appending literals, as described above. + exceptionContext.exception().currentChain().stream() + .filter(c -> c.type() == CommandComponent.ComponentType.LITERAL) + .forEach(literal -> translationKeyBuilder.append(literal.name()).append(' ')); + // Trimming input (last character ends up being blank) and replacing whitespaces with underscores, as that's how translations are defined inside the language file. + final String translationKey = translationKeyBuilder.toString().trim().replace(' ', '_'); + // Getting the message, it's not finished as there we need to handle fallback language etc. + final @Nullable Message message = Optional.ofNullable(plugin.getTranslator().getSelectedLanguage().getMessage(translationKey)) + .orElse(plugin.getTranslator().getFallbackLanguage().getMessage(translationKey)); + // "Fall-backing" to generic syntax error, if no specialized syntax message has been defined in the language file. + if (message == null) { + plugin.getTranslator().translate("command_invalid_syntax_generic") + .replace("syntax", exceptionContext.exception().correctSyntax()) + .send(exceptionContext.context().sender()); + return; + } + message.send(exceptionContext.context().sender()); + }); + // Returning this instance of CloudCommandManager to keep "builder-like" flow. + return this; + } + + /** + * Registers plugin commands to the {@link PaperCommandManager}. + */ + public @NotNull CloudCommandManager registerCommands() { + annotationParser.parse(AttributeCMD.INSTANCE); + annotationParser.parse(CollidableCMD.INSTANCE); + annotationParser.parse(CopyCMD.INSTANCE); + annotationParser.parse(CreateCMD.INSTANCE); + annotationParser.parse(DisplayNameCMD.INSTANCE); + annotationParser.parse(EquipmentCMD.INSTANCE); + annotationParser.parse(FancyNpcsCMD.INSTANCE); + annotationParser.parse(FixCMD.INSTANCE); + annotationParser.parse(GlowingCMD.INSTANCE); + annotationParser.parse(InfoCMD.INSTANCE); + annotationParser.parse(InteractionCooldownCMD.INSTANCE); + annotationParser.parse(ListCMD.INSTANCE); + annotationParser.parse(MessageCMD.INSTANCE); + annotationParser.parse(MoveHereCMD.INSTANCE); + annotationParser.parse(MoveToCMD.INSTANCE); + annotationParser.parse(NearbyCMD.INSTANCE); + annotationParser.parse(HelpCMD.INSTANCE); + annotationParser.parse(PlayerCommandCMD.INSTANCE); + annotationParser.parse(RemoveCMD.INSTANCE); + annotationParser.parse(ServerCommandCMD.INSTANCE); + annotationParser.parse(ShowInTabCMD.INSTANCE); + annotationParser.parse(SkinCMD.INSTANCE); + annotationParser.parse(TeleportCMD.INSTANCE); + annotationParser.parse(TurnToPlayerCMD.INSTANCE); + annotationParser.parse(TypeCMD.INSTANCE); + // Returning this instance of CloudCommandManager to keep "builder-like" flow. + return this; + } + + /** + * Returns the internal {@link PaperCommandManager} associated with this instance of {@link CloudCommandManager}. + */ + public @NotNull PaperCommandManager getCommandManager() { + return commandManager; + } + + /** + * Returns the internal {@link AnnotationParser} associated with this instance of {@link CloudCommandManager}. + */ + public @NotNull AnnotationParser getAnnotationParser() { + return annotationParser; + } + +} diff --git a/src/main/java/de/oliver/fancynpcs/commands/FancyNpcsCMD.java b/src/main/java/de/oliver/fancynpcs/commands/FancyNpcsCMD.java index 817172ce..0db98ffb 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/FancyNpcsCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/FancyNpcsCMD.java @@ -1,67 +1,75 @@ package de.oliver.fancynpcs.commands; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Language; +import de.oliver.fancylib.translations.Translator; +import de.oliver.fancylib.translations.message.SimpleMessage; import de.oliver.fancynpcs.FancyNpcs; -import org.bukkit.command.Command; import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; -import java.util.Collections; -import java.util.List; -import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; -public class FancyNpcsCMD extends Command { +public enum FancyNpcsCMD { + INSTANCE; // SINGLETON - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); + private final FancyNpcs plugin = FancyNpcs.getInstance(); + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - public FancyNpcsCMD() { - super("fancynpcs"); - setPermission("fancynpcs.admin"); + @Command("fancynpcs version") + @Permission("fancynpcs.command.fancynpcs.version") + public void onVersion(final CommandSender sender) { + plugin.getVersionConfig().checkVersionAndDisplay(sender, false); } - @Override - public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) { - - if (args.length == 1) { - return Stream.of("version", "reload", "save", "featureFlags") - .filter(input -> input.toLowerCase().startsWith(args[0].toLowerCase())) - .toList(); - } - - return Collections.emptyList(); + @Command("fancynpcs reload") + @Permission("fancynpcs.command.fancynpcs.reload") + public void onReload(final CommandSender sender) { + // Reloading all defined languages. + translator.loadLanguages(plugin.getDataFolder().getAbsolutePath()); + // Reloading plugin configuration. + plugin.getFancyNpcConfig().reload(); + // Getting the selected language from configuration. Defaults to fallback language. + final Language selectedLanguage = translator.getLanguages().stream() + .filter(language -> language.getLanguageName().equals(plugin.getFancyNpcConfig().getLanguage())) + .findFirst().orElse(translator.getFallbackLanguage()); + translator.setSelectedLanguage(selectedLanguage); + // Reloading all NPCs. + // NOTE: This sometimes creates duplicated NPCs on the client-side. + plugin.getNpcManagerImpl().reloadNpcs(); + // Sending success message to the sender. + translator.translate("fancynpcs_reload_success").send(sender); } - @Override - public boolean execute(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) { - if (!testPermission(sender)) { - return false; - } - - FancyNpcs plugin = FancyNpcs.getInstance(); - - if (args.length >= 1 && args[0].equalsIgnoreCase("version")) { - FancyNpcs.getInstance().getVersionConfig().checkVersionAndDisplay(sender, false); - - } else if (args.length >= 1 && args[0].equalsIgnoreCase("reload")) { - plugin.getLanguageConfig().load(); - plugin.getFancyNpcConfig().reload(); - plugin.getNpcManagerImpl().reloadNpcs(); - MessageHelper.success(sender, lang.get("reloaded-config")); - - } else if (args.length >= 1 && args[0].equalsIgnoreCase("save")) { - plugin.getNpcManagerImpl().saveNpcs(true); - MessageHelper.success(sender, lang.get("saved-npcs")); - - } else if (args.length >= 1 && args[0].equalsIgnoreCase("featureFlags")) { - MessageHelper.info(sender, "Feature flags:"); - MessageHelper.info(sender, " - player-npcs: " + FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled()); + @Command("fancynpcs save") + @Permission("fancynpcs.command.fancynpcs.save") + public void onSave(final CommandSender sender) { + plugin.getNpcManagerImpl().saveNpcs(true); + translator.translate("fancynpcs_save_success").send(sender); + } - } else { - MessageHelper.info(sender, lang.get("fancynpcs-syntax")); - return false; - } + // NOTE: In the future, if there is more than a few feature flags, we might consider listing entries automatically by iterating, just like in 'list' sub-command. + @Command("fancynpcs feature_flags") + @Permission("fancynpcs.command.fancynpcs.feature_flags") + public void onFeatureFlags(final CommandSender sender) { + translator.translate("fancynpcs_feature_flags_header").send(sender); + translator.translate("fancynpcs_feature_flags_entry") + .replace("number", "1") + .replace("name", "Player NPCs") + .replace("id", FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.getName()) + .replace("state", getTranslatedState(FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled())) + .send(sender); + translator.translate("fancynpcs_feature_flags_footer") + .replace("count", "1") + .replace("count_formatted", "· · 1") + .replace("total", String.valueOf(FancyNpcs.getInstance().getNpcManager().getAllNpcs().size())) + .replace("total_formatted", "· · 1") + .send(sender); + } - return true; + // NOTE: Might need to be improved later down the line, should get work done for now. + private @NotNull String getTranslatedState(final boolean bool) { + return (bool) ? ((SimpleMessage) translator.translate("enabled")).getMessage() : ((SimpleMessage) translator.translate("disabled")).getMessage(); } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/Subcommand.java b/src/main/java/de/oliver/fancynpcs/commands/Subcommand.java deleted file mode 100644 index f9764f71..00000000 --- a/src/main/java/de/oliver/fancynpcs/commands/Subcommand.java +++ /dev/null @@ -1,17 +0,0 @@ -package de.oliver.fancynpcs.commands; - -import de.oliver.fancynpcs.api.Npc; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public interface Subcommand { - - List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args); - - boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args); - -} diff --git a/src/main/java/de/oliver/fancynpcs/commands/arguments/LocationArgument.java b/src/main/java/de/oliver/fancynpcs/commands/arguments/LocationArgument.java new file mode 100644 index 00000000..01dec3fd --- /dev/null +++ b/src/main/java/de/oliver/fancynpcs/commands/arguments/LocationArgument.java @@ -0,0 +1,37 @@ +package de.oliver.fancynpcs.commands.arguments; + +import org.bukkit.FluidCollisionMode; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.util.RayTraceResult; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; + +import java.text.DecimalFormat; +import java.util.Collections; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +public enum LocationArgument { + INSTANCE; // SINGLETON + + private static final DecimalFormat COORDS_FORMAT = new DecimalFormat("#.##"); + + @Suggestions("relative_location") + public List suggestLocation(final CommandContext context, final CommandInput input) { + if (context.sender() instanceof Player player) { + final @Nullable RayTraceResult raytrace = player.rayTraceBlocks(32.0, FluidCollisionMode.ALWAYS); + if (raytrace != null) + return List.of( + COORDS_FORMAT.format(raytrace.getHitPosition().getX()) + " " + + COORDS_FORMAT.format(raytrace.getHitPosition().getY()) + " " + + COORDS_FORMAT.format(raytrace.getHitPosition().getZ()), + "~ ~ ~" + ); + return List.of("~ ~ ~"); + } + return Collections.emptyList(); + } +} diff --git a/src/main/java/de/oliver/fancynpcs/commands/arguments/NpcArgument.java b/src/main/java/de/oliver/fancynpcs/commands/arguments/NpcArgument.java new file mode 100644 index 00000000..a1c0f24e --- /dev/null +++ b/src/main/java/de/oliver/fancynpcs/commands/arguments/NpcArgument.java @@ -0,0 +1,72 @@ +package de.oliver.fancynpcs.commands.arguments; + +import de.oliver.fancylib.translations.Translator; +import de.oliver.fancynpcs.FancyNpcs; +import de.oliver.fancynpcs.api.Npc; +import de.oliver.fancynpcs.commands.exceptions.ReplyingParseException; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.checkerframework.checker.units.qual.C; +import org.incendo.cloud.annotations.parser.Parser; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; + +import java.util.List; +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public enum NpcArgument { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + // This parser does not specify a name, making it default parser for the returned type. + @Parser(name = "", suggestions = "npc") + public @NotNull Npc parse(final CommandContext context, final CommandInput input) { + // Reading next argument as single/literal String. + final String value = input.readString(); + // Getting the NPC from the manager. This can be name or optionally (under certain circumstances) UUID of the NPC. + final @Nullable Npc npc = !isUUID(value) + // Not an UUID, getting NPC from name. + ? FancyNpcs.getInstance().getNpcManager().getNpc(value) + // Input is UUID, getting the NPC that way. If PLAYER NPCS FLAG is enabled, sender is required to have 'fancynpcs.admin' permission. + : !FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() || context.sender().hasPermission("fancynpcs.admin") + ? FancyNpcs.getInstance().getNpcManager().getNpcById(value) + : null; + // Throwing exception if no NPC with given name or UUID exist. + if (npc == null) + throw ReplyingParseException.replying(() -> translator.translate("command_invalid_npc").replaceStripped("input", value).send(context.sender())); + // Throwing exception if PLAYER NPCS FLAG is enabled and sender is not creator of the specified NPC. + if (FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() && context.sender() instanceof Player sender && !npc.getData().getCreator().equals(sender.getUniqueId())) + throw ReplyingParseException.replying(() -> translator.translate("command_invalid_npc").replaceStripped("input", value).send(context.sender())); + return npc; + } + + @Suggestions("npc") // NOTE: Consider caching, might not be necessary but should be kept in mind. + public List suggestions(final CommandContext context, final CommandInput input) { + return (FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() && context.sender() instanceof Player sender) + // PLAYER NPCS FLAG is enabled and sender is player; Filtering NPCs and showing only those that are created by the sender. + ? FancyNpcs.getInstance().getNpcManager().getAllNpcs().stream() + .filter(npc -> npc.getData().getCreator().equals(sender.getUniqueId())) + .map(npc -> npc.getData().getName()) + .toList() + // PLAYER NPCS FLAG is disabled or sender is console; Showing all NPCs. + : FancyNpcs.getInstance().getNpcManager().getAllNpcs().stream().map(npc -> npc.getData().getName()).toList(); + } + + /** + * Returns {@code true} if provided {@link String} can be converted to a valid {@link UUID}. Otherwise {@code false} is returned. + * */ + private static boolean isUUID(final @NotNull String string) { + try { + UUID.fromString(string); + return true; + } catch (final IllegalArgumentException e) { + return false; + } + } + +} diff --git a/src/main/java/de/oliver/fancynpcs/commands/exceptions/ReplyingParseException.java b/src/main/java/de/oliver/fancynpcs/commands/exceptions/ReplyingParseException.java new file mode 100644 index 00000000..79452da0 --- /dev/null +++ b/src/main/java/de/oliver/fancynpcs/commands/exceptions/ReplyingParseException.java @@ -0,0 +1,21 @@ +package de.oliver.fancynpcs.commands.exceptions; + +import org.jetbrains.annotations.NotNull; + +public final class ReplyingParseException extends RuntimeException { + + private final @NotNull Runnable runnable; + + private ReplyingParseException(final @NotNull Runnable runnable) { + this.runnable = runnable; + } + + public static ReplyingParseException replying(final Runnable runnable) { + return new ReplyingParseException(runnable); + } + + public @NotNull Runnable runnable() { + return runnable; + } + +} diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/AttributeCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/AttributeCMD.java index 1f7999ae..102940cc 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/AttributeCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/AttributeCMD.java @@ -1,104 +1,112 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; -import de.oliver.fancynpcs.AttributeManagerImpl; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; +import de.oliver.fancynpcs.api.AttributeManager; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.NpcAttribute; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; +import de.oliver.fancynpcs.commands.exceptions.ReplyingParseException; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.incendo.cloud.annotations.Argument; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; +import org.incendo.cloud.annotations.parser.Parser; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; import java.util.List; -public class AttributeCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - private final AttributeManagerImpl attributeManager = FancyNpcs.getInstance().getAttributeManager(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (npc == null) { - return null; - } - - if (args.length == 3) { - List attributes = attributeManager.getAllAttributesForEntityType(npc.getData().getType()); - - return attributes.stream() - .map(NpcAttribute::getName) - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); - } - - if (args.length == 4) { - String attributeName = args[2]; - NpcAttribute attribute = attributeManager.getAttributeByName(npc.getData().getType(), attributeName); - if (attribute == null) { - return null; - } +import org.jetbrains.annotations.NotNull; - return attribute.getPossibleValues().stream() - .filter(input -> input.toLowerCase().startsWith(args[3].toLowerCase())) - .toList(); +public enum AttributeCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + private final AttributeManager attributeManager = FancyNpcs.getInstance().getAttributeManager(); + + @Command("npc attribute set ") + @Permission("fancynpcs.command.npc.attribute.set") + public void onAttributeSet( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull NpcAttribute attribute, + final @NotNull @Argument(parserName = "AttributeCMD/attribute_value") String attributeValue + ) { + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.ATTRIBUTE, new Object[]{attribute, attributeValue}, sender).callEvent()) { + npc.getData().addAttribute(attribute, attributeValue); + npc.updateForAll(); + translator.translate("npc_attribute_set").replace("attribute", attribute.getName()).replaceStripped("value", attributeValue.toLowerCase()).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); } - - return null; } - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - if (args.length < 4) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - String attributeName = args[2]; - String value = ""; - - for (int i = 3; i < args.length; i++) { - value += args[i] + " "; - } - value = value.substring(0, value.length() - 1); - - NpcAttribute attribute = attributeManager.getAttributeByName(npc.getData().getType(), attributeName); - if (attribute == null) { - MessageHelper.error(receiver, lang.get("npc-command-attribute-attribute-not-found")); - return false; - } - - if (!attribute.getTypes().contains(npc.getData().getType())) { - MessageHelper.error(receiver, lang.get("npc-command-attribute-wrong-entity-type")); - return false; - } - - if (!attribute.isValidValue(value)) { - MessageHelper.error(receiver, lang.get("npc-command-attribute-invalid-value")); - return false; + @Command("npc attribute list") + @Permission("fancynpcs.command.npc.attribute.list") + public void onAttributeList( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + // Sending error message if the list is empty. + if (npc.getData().getAttributes().isEmpty()) { + translator.translate("npc_attribute_list_failure_empty").send(sender); + return; } + translator.translate("npc_attribute_list_header").send(sender); + // Iterating over all attributes set on this NPC and sending them to the sender. + npc.getData().getAttributes().forEach((attribute, value) -> { + translator.translate("npc_attribute_list_entry") + .replace("attribute", attribute.getName()) + .replace("value", value) + .send(sender); + }); + translator.translate("npc_attribute_list_footer").send(sender); + } - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.ATTRIBUTE, new Object[]{attribute, value}, receiver); - npcModifyEvent.callEvent(); - - if (npcModifyEvent.isCancelled()) { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - return false; - } + /* PARSERS AND SUGGESTIONS */ + + // This parser does not specify a name, making it default parser for the returned type. + @Parser(name = "", suggestions = "AttributeCMD/attribute") + public NpcAttribute parseAttribute(final CommandContext context, final CommandInput input) { + // Getting the 'npc' argument that should already exist within the command context. + final Npc npc = context.get("npc"); + // Reading the string, which is supposed to be an attribute name. + final String value = input.readString(); + // Getting the NpcAttribute from the name and npc type. + final NpcAttribute attribute = attributeManager.getAttributeByName(npc.getData().getType(), value); + // Throwing exception when non-existent attribute has been provided. + if (attribute == null) + throw ReplyingParseException.replying(() -> translator.translate("command_invalid_attribute").replaceStripped("input", value).send(context.sender())); + // Otherwise, returning the attribute from the parser. + return attribute; + } - npc.getData().addAttribute(attribute, value); - npc.updateForAll(); + @Parser(name = "AttributeCMD/attribute_value", suggestions = "AttributeCMD/attribute_value") + public String parseAttributeValue(final CommandContext context, final CommandInput input) { + // Getting the 'attribute' argument that should already exist within the command context. + final NpcAttribute attribute = context.get("attribute"); + // Reading the string, which is supposed to be an attribute name. + final String value = input.readString(); + // Sending error message if attribute is null or cannot accept provided value. + if (!attribute.isValidValue(value)) + throw ReplyingParseException.replying(() -> translator.translate("command_invalid_attribute_value").replaceStripped("input", value).send(context.sender())); + // Otherwise, returning the attribute from the parser. + return value; + } - MessageHelper.success(receiver, lang.get("npc-command-attribute-success")); + @Suggestions("AttributeCMD/attribute") + public List suggestAttribute(final CommandContext context, final CommandInput input) { + final Npc npc = context.getOrDefault("npc", null); + return attributeManager.getAllAttributesForEntityType(npc.getData().getType()).stream().map(NpcAttribute::getName).toList(); + } - return false; + @Suggestions("AttributeCMD/attribute_value") + public List suggestAttributeValue(final CommandContext context, final CommandInput input) { + final Npc npc = context.get("npc"); + final NpcAttribute attribute = context.get("attribute"); + return attributeManager.getAttributeByName(npc.getData().getType(), attribute.getName()).getPossibleValues(); } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/CollidableCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/CollidableCMD.java index 009619b4..4dffd8e8 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/CollidableCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/CollidableCMD.java @@ -1,70 +1,38 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.stream.Stream; - -public class CollidableCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return Stream.of("true", "false") - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); +public enum CollidableCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + @Command("npc collidable [state]") + @Permission("fancynpcs.command.npc.collidable") + public void onCollidable( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @Nullable Boolean state + ) { + // Finalizing the state. If no state has been specified, the current one is inverted. + final boolean finalState = (state == null) ? !npc.getData().isCollidable() : state; + // Calling the event and updating the state if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.COLLIDABLE, finalState, sender).callEvent()) { + npc.getData().setCollidable(finalState); + translator.translate(finalState ? "npc_collidable_set_true" : "npc_collidable_set_false").replace("npc", npc.getData().getName()).send(sender); + return; } - - return null; + // Otherwise, sending error message to the sender. + translator.translate("command_npc_modification_cancelled").send(sender); } - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - boolean collidable; - try { - collidable = Boolean.parseBoolean(args[2]); - } catch (Exception e) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.COLLIDABLE, collidable, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().setCollidable(collidable); - npc.updateForAll(); - - if (collidable) { - MessageHelper.success(receiver, lang.get("npc-command-collidable-true")); - } else { - MessageHelper.success(receiver, lang.get("npc-command-collidable-false")); - } - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - } - - return true; - } } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/CopyCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/CopyCMD.java index 59e85fe1..f700302e 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/CopyCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/CopyCMD.java @@ -1,56 +1,48 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.NpcData; import de.oliver.fancynpcs.api.events.NpcCreateEvent; -import de.oliver.fancynpcs.commands.Subcommand; -import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; -import java.util.List; import java.util.UUID; +import java.util.regex.Pattern; -public class CopyCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); +import org.jetbrains.annotations.NotNull; - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - return null; - } +// TO-DO: Console support with --position and --world parameter flags. +public enum CopyCMD { + INSTANCE; // SINGLETON - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (!(receiver instanceof Player player)) { - MessageHelper.error(receiver, lang.get("npc-command.only-players")); - return false; - } + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } + private static final Pattern NPC_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9/_-]*$"); - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; + @Command(value = "npc copy ", requiredSender = Player.class) + @Permission("fancynpcs.command.npc.copy") + public void onCopy( + final @NotNull Player sender, + final @NotNull Npc npc, + final @NotNull String name + ) { + // Sending error message if name does not match configured pattern. + if (!NPC_NAME_PATTERN.matcher(name).find()) { + translator.translate("npc_create_failure_invalid_name").replaceStripped("name", name).send(sender); + return; } - - String newName = args[2]; - - Npc copied = FancyNpcs.getInstance().getNpcAdapter().apply( + // Creating a copy of an NPC and all it's data. The only different thing is it's UUID. + final Npc copied = FancyNpcs.getInstance().getNpcAdapter().apply( new NpcData( UUID.randomUUID().toString(), - newName, - player.getUniqueId(), + name, + sender.getUniqueId(), npc.getData().getDisplayName(), npc.getData().getSkin(), - player.getLocation(), + sender.getLocation(), npc.getData().isShowInTab(), npc.getData().isSpawnEntity(), npc.getData().isCollidable(), @@ -62,25 +54,20 @@ public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull npc.getData().getOnClick(), npc.getData().getMessages(), npc.getData().isSendMessagesRandomly(), - npc.getData().getServerCommand(), + npc.getData().getServerCommands(), npc.getData().getPlayerCommands(), npc.getData().getInteractionCooldown(), npc.getData().getAttributes(), npc.getData().isMirrorSkin() )); - - NpcCreateEvent npcCreateEvent = new NpcCreateEvent(copied, player); - npcCreateEvent.callEvent(); - if (!npcCreateEvent.isCancelled()) { + // Calling the event and creating + registering copied NPC if not cancelled. + if (new NpcCreateEvent(copied, sender).callEvent()) { copied.create(); FancyNpcs.getInstance().getNpcManagerImpl().registerNpc(copied); copied.spawnForAll(); - - MessageHelper.success(receiver, lang.get("npc-command-copy-success")); + translator.translate("npc_command_copy_success").replace("npc", npc.getData().getName()).replace("new_npc", copied.getData().getName()).send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-copy-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/CreateCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/CreateCMD.java index 645eef8a..5b7ddd47 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/CreateCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/CreateCMD.java @@ -1,69 +1,83 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.NpcData; import de.oliver.fancynpcs.api.events.NpcCreateEvent; -import de.oliver.fancynpcs.commands.Subcommand; +import org.bukkit.Location; +import org.bukkit.World; import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Flag; +import org.incendo.cloud.annotations.Permission; + +import java.util.UUID; +import java.util.regex.Pattern; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; +public enum CreateCMD { + INSTANCE; // SINGLETON -public class CreateCMD implements Subcommand { + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - return null; - } + private static final Pattern NPC_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9/_-]*$"); + private static final UUID EMPTY_UUID = new UUID(0,0); - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (!(receiver instanceof Player player)) { - MessageHelper.error(receiver, lang.get("npc-command.only-players")); - return false; + @Command("npc create ") + @Permission("fancynpcs.command.npc.create") + public void onCreate( + final @NotNull CommandSender sender, + final @NotNull String name, + final @Nullable @Flag("type") EntityType type, + final @Nullable @Flag(value = "location", suggestions = "relative_location") Location location, + final @Nullable @Flag("world") World world + ) { + // Sending error message if name does not match configured pattern. + if (!NPC_NAME_PATTERN.matcher(name).find()) { + translator.translate("npc_create_failure_invalid_name").replaceStripped("name", name).send(sender); + return; } - - String name = args[1]; - - if (FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled()) { - if (FancyNpcs.getInstance().getNpcManagerImpl().getNpc(name, player.getUniqueId()) != null) { - MessageHelper.error(receiver, lang.get("npc-command-create-name-already-exists")); - return false; - } - } else { - if (FancyNpcs.getInstance().getNpcManagerImpl().getNpc(name) != null) { - MessageHelper.error(receiver, lang.get("npc-command-create-name-already-exists")); - return false; - } + // Getting the NPC creator unique identifier. The UUID is always empty (all zeroes) for non-player senders. + final UUID creator = (sender instanceof Player player) ? player.getUniqueId() : EMPTY_UUID; + // Sending error message if NPC with such name already exist. + if ((FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() && FancyNpcs.getInstance().getNpcManager().getNpc(name, creator) != null) || FancyNpcs.getInstance().getNpcManager().getNpc(name) != null) { + translator.translate("npc_create_failure_already_exists").replace("npc", FancyNpcs.getInstance().getNpcManager().getNpc(name).getData().getName()).send(sender); + return; } - - - if (name.contains(".")) { - name = name.replace('.', '_'); + // Sending error message if sender is console and location has not been specified. + if (sender instanceof ConsoleCommandSender && location == null) { + translator.translate("npc_create_failure_must_specify_location").send(sender); + return; } - - Npc createdNpc = FancyNpcs.getInstance().getNpcAdapter().apply(new NpcData(name, player.getUniqueId(), player.getLocation())); - createdNpc.getData().setLocation(player.getLocation()); - - NpcCreateEvent npcCreateEvent = new NpcCreateEvent(createdNpc, player); - npcCreateEvent.callEvent(); - if (!npcCreateEvent.isCancelled()) { - createdNpc.create(); - FancyNpcs.getInstance().getNpcManagerImpl().registerNpc(createdNpc); - createdNpc.spawnForAll(); - - MessageHelper.success(receiver, lang.get("npc-command-create-created")); + // Sending error message if sender is console and world has not been specified. + if (sender instanceof ConsoleCommandSender && world == null) { + translator.translate("npc_create_failure_must_specify_world").send(sender); + return; + } + // Finalizing Location argument. This argument is optional and defaults to player's current location. + final Location finalLocation = (location == null && sender instanceof Player player) ? player.getLocation() : location; + // Updating World of the Location argument if '--world' flag has been specified. + if (world != null) + finalLocation.setWorld(world); + // Creating new NPC and applying data. + final Npc npc = FancyNpcs.getInstance().getNpcAdapter().apply(new NpcData(name, creator, finalLocation)); + // Setting the type of NPC. Flag '--type' is optional and defaults to EntityType.PLAYER. + npc.getData().setType(type != null ? type : EntityType.PLAYER); + // Calling the event and creating NPC if not cancelled. + if (new NpcCreateEvent(npc, sender).callEvent()) { + npc.create(); + FancyNpcs.getInstance().getNpcManagerImpl().registerNpc(npc); + npc.spawnForAll(); + translator.translate("npc_create_success").replace("npc", name).send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-create-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/DisplayNameCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/DisplayNameCMD.java index 1c78fd06..2d17110a 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/DisplayNameCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/DisplayNameCMD.java @@ -1,62 +1,94 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; +import me.dave.chatcolorhandler.ModernChatColorHandler; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentIteratorType; +import net.kyori.adventure.text.event.ClickEvent; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.incendo.cloud.annotation.specifier.Greedy; +import org.incendo.cloud.annotations.Argument; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; import java.util.List; -import java.util.stream.Stream; +import java.util.stream.StreamSupport; -public class DisplayNameCMD implements Subcommand { +import org.jetbrains.annotations.NotNull; - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); +public enum DisplayNameCMD { + INSTANCE; // SINGLETON - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return Stream.of("") - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); - } - return null; - } + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } + // Storing in a static variable to avoid re-creating the array each time suggestion is requested. + private static final List NONE_SUGGESTIONS = List.of("@none"); - String displayName = ""; - for (int i = 2; i < args.length; i++) { - displayName += args[i] + " "; + @Command("npc displayname ") + @Permission("fancynpcs.command.npc.displayname") + public void onDisplayName( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull @Argument(suggestions = "DisplayNameCMD/none") @Greedy String name + ) { + // Finalizing the name. In case input is '@none', it gets replaced with '' for backwards compatibility. + final String finalName = name.equalsIgnoreCase("@none") ? "" : name; + // Sending error message in case banned command has been found in the input. + if (hasBlockedCommands(finalName)) { + translator.translate("command_input_contains_blocked_command").send(sender); + return; } - displayName = displayName.substring(0, displayName.length() - 1); - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.DISPLAY_NAME, displayName, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().setDisplayName(displayName.toString()); + // Calling the event and updating the state if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.DISPLAY_NAME, finalName, sender).callEvent()) { + npc.getData().setDisplayName(finalName); npc.updateForAll(); - MessageHelper.success(receiver, lang.get("npc-command-displayName-updated")); + translator.translate(finalName.equalsIgnoreCase("") ? "npc_displayname_set_empty" : "npc_displayname_set_name") + .replace("npc", npc.getData().getName()) + .replace("name", finalName) + .send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } + } - return true; + /* PARSERS AND SUGGESTIONS */ + + @Suggestions("DisplayNameCMD/none") + public List suggestNone(final CommandContext sender, CommandInput input) { + return NONE_SUGGESTIONS; } + + /* UTILITY METHODS */ + + /** Returns {@code true} if specified component contains blocked command, {@code false} otherwise. */ + private boolean hasBlockedCommands(final @NotNull String message) { + // Converting message to a Component. + final Component component = ModernChatColorHandler.translate(message); + // Getting the list of all blocked commands. + final List blockedCommands = FancyNpcs.getInstance().getFancyNpcConfig().getBlockedCommands(); + // Iterating over all elements of the component. + return StreamSupport.stream(component.iterable(ComponentIteratorType.DEPTH_FIRST).spliterator(), false).anyMatch(it -> { + final ClickEvent event = it.clickEvent(); + // We only care about click events with run_command as an action. Continuing if not found. + if (event == null || event.action() != ClickEvent.Action.RUN_COMMAND) + return false; + // Iterating over list of blocked commands... + for (final String blockedCommand : blockedCommands) { + // Transforming the command to a base command with trailed whitespaces and slashes. This also removes namespaced part from the beginning of the command. + final String transformedBaseCommand = blockedCommand.replace('/', ' ').strip().split(" ")[0].replaceAll(".*?:+", ""); + // Comparing click event value with the transformed base command. Returning the result. + if (event.value().replace('/', ' ').strip().split(" ")[0].replaceAll(".*?:+", "").equalsIgnoreCase(transformedBaseCommand)) + return true; + } + // Returning false as no blocked commands has been found. + return false; + }); + } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/EquipmentCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/EquipmentCMD.java index 9a68c441..e249892a 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/EquipmentCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/EquipmentCMD.java @@ -1,75 +1,178 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; +import de.oliver.fancylib.translations.message.SimpleMessage; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; import de.oliver.fancynpcs.api.utils.NpcEquipmentSlot; -import de.oliver.fancynpcs.commands.Subcommand; +import de.oliver.fancynpcs.commands.exceptions.ReplyingParseException; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.incendo.cloud.annotations.Argument; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; +import org.incendo.cloud.annotations.parser.Parser; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class EquipmentCMD implements Subcommand { +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public enum EquipmentCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); + // Storing in a static variable to avoid re-creating the array each time suggestion is requested. + private static final List SLOT_SUGGESTIONS = Arrays.stream(NpcEquipmentSlot.values()).map(slot -> slot.name().toLowerCase()).toList(); + private static final List MATERIAL_SUGGESTIONS = Registry.MATERIAL.stream().map(material -> material.key().asString()).toList(); - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return Arrays.stream(NpcEquipmentSlot.values()) - .map(Enum::name) - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); + @Command("npc equipment set ") + @Permission("fancynpcs.command.npc.equipment.set") + public void onEquipmentSet( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull NpcEquipmentSlot slot, + final @NotNull @Argument(parserName = "EquipmentCMD/item") ItemStack item + ) { + // Calling the event and updating equipment if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.EQUIPMENT, new Object[]{slot, item}, sender).callEvent()) { + npc.getData().addEquipment(slot, item); + npc.updateForAll(); + translator.translate(item.getType() != Material.AIR ? "npc_equipment_set_item" : "npc_equipment_set_empty") + .replace("npc", npc.getData().getName()) + .replace("slot", getTranslatedSlot(slot)) + .addTagResolver(Placeholder.component("item", (item.getType() != Material.AIR) ? item.displayName().hoverEvent(item.asHoverEvent()) : Component.empty())) + .send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); } - return null; } - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (!(receiver instanceof Player player)) { - MessageHelper.error(receiver, lang.get("npc-command.only-players")); - return false; + @Command("npc equipment clear") + @Permission("fancynpcs.command.npc.equipment.clear") + public void onEquipmentClear( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + // Calling the event and clearing equipment if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.EQUIPMENT, null, sender).callEvent()) { + // Entries must be set to null manually because clearing the map would prevent equipment from being updated. (Npc#update checks if map is empty) + for (final NpcEquipmentSlot slot : NpcEquipmentSlot.values()) + npc.getData().getEquipment().put(slot, null); + npc.updateForAll(); + translator.translate("npc_equipment_clear_success").replace("npc", npc.getData().getName()).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); } + } - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; + @Command("npc equipment list") + @Permission("fancynpcs.command.npc.equipment.list") + public void onEquipmentList( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + // Sending error message if the list is empty or all items are Material.AIR. + if (npc.getData().getEquipment().isEmpty() || npc.getData().getEquipment().values().stream().allMatch(item -> item == null || item.getType() == Material.AIR)) { + translator.translate("npc_equipment_list_failure_empty").send(sender); + return; } + translator.translate("npc_equipment_list_header").send(sender); + // Iterating over all equipment slots of this NPC and sending them to the sender. + npc.getData().getEquipment().forEach((slot, item) -> { + // Skipping null entries and Material.AIR, no need to display that. + if (item == null || item.getType() == Material.AIR) + return; + translator.translate("npc_equipment_list_entry") + .replace("slot", getTranslatedSlot(slot)) + .addTagResolver(Placeholder.component("item", item.displayName().hoverEvent(item.asHoverEvent()))) + .send(sender); + }); + translator.translate("npc_equipment_list_footer").send(sender); + } + /* PARSERS AND SUGGESTIONS */ - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - String slot = args[2]; + // This parser does not specify a name, making it default parser for the returned type. + @Parser(name = "", suggestions = "EquipmentCMD/slot") + public NpcEquipmentSlot parseSlot(final CommandContext context, final CommandInput input) { + final String value = input.readString().toLowerCase(); + final @Nullable NpcEquipmentSlot slot = NpcEquipmentSlot.parse(value); + // Sending error message if input is not a valid NpcEquipmentSlot. + if (slot == null) + throw ReplyingParseException.replying(() -> translator.translate("command_invalid_equipment_slot").replaceStripped("input", value).send(context.sender())); + return slot; + } - NpcEquipmentSlot equipmentSlot = NpcEquipmentSlot.parse(slot); - if (equipmentSlot == null) { - MessageHelper.error(receiver, lang.get("npc-command-equipment-invalid-slot")); - return false; + @Parser(name = "EquipmentCMD/item", suggestions = "EquipmentCMD/item") + public ItemStack parseItem(final CommandContext context, final CommandInput input) { + final String value = input.readString().toLowerCase(); + // Handling '@none', which returns air (and effectively disables) + if (value.equals("@none")) + return new ItemStack(Material.AIR); + // Handling '@hand', which returns item player currently have in their hand. + else if (value.equals("@hand") && context.sender() instanceof Player player) + return player.getInventory().getItemInMainHand().clone(); + // Otherwise, trying to parse input as an material. + else { + // Converting input to NamespacedKey. Defaults to 'minecraft:' namespace if missing from input. + final @Nullable NamespacedKey key = NamespacedKey.fromString(value); + // Sending error message if input is not a valid NamespacedKey. + if (key == null) + throw ReplyingParseException.replying(() -> translator.translate("command_invalid_material").replaceStripped("input", value).send(context.sender())); + // Getting material from the registry. + final @Nullable Material material = Registry.MATERIAL.get(key); + // Sending error message if no material was found. + if (material == null) + throw ReplyingParseException.replying(() -> translator.translate("command_invalid_material").replaceStripped("input", value).send(context.sender())); + // Returning new ItemStack object from the specified Material. + return new ItemStack(material); } + } - ItemStack item = player.getInventory().getItemInMainHand().clone(); + @Suggestions("EquipmentCMD/item") + public List suggestItem(final CommandContext context, final CommandInput input) { + return new ArrayList<>(MATERIAL_SUGGESTIONS) {{ + // Adding '@none' placeholder which is replaced with 'minecraft:air'. + add("@none"); + // If applicable, adding '@hand' placeholder which is replaced with item player currently have in their hand. + if (context.sender() instanceof Player) + add("@hand"); + }}; + } - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.EQUIPMENT, new Object[]{equipmentSlot, item}, receiver); - npcModifyEvent.callEvent(); + @Suggestions("EquipmentCMD/slot") + public List suggestSlot(final CommandContext context, final CommandInput input) { + return SLOT_SUGGESTIONS; + } - if (!npcModifyEvent.isCancelled()) { - npc.getData().addEquipment(equipmentSlot, item); - npc.updateForAll(); - MessageHelper.success(receiver, lang.get("npc-command-equipment-updated")); - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - } + /* UTILITY METHODS */ - return true; + // NOTE: Might need to be improved later down the line, should get work done for now. + private @NotNull String getTranslatedSlot(final @NotNull NpcEquipmentSlot slot) { + return ((SimpleMessage) translator.translate( + switch (slot) { + case MAINHAND -> "main_hand"; + case OFFHAND -> "off_hand"; + case HEAD -> "head"; + case CHEST -> "chest"; + case LEGS -> "legs"; + case FEET -> "feet"; + } + )).getMessage(); } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/FixCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/FixCMD.java index 016a0841..940b98f5 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/FixCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/FixCMD.java @@ -1,39 +1,30 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; -import de.oliver.fancynpcs.commands.Subcommand; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; -public class FixCMD implements Subcommand { +import org.jetbrains.annotations.NotNull; - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); +public enum FixCMD { + INSTANCE; // SINGLETON - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - return null; - } - - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + @Command("npc fix ") + @Permission("fancynpcs.command.npc.fix") + public void onFix( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { npc.removeForAll(); npc.create(); Bukkit.getOnlinePlayers().forEach(npc::checkAndUpdateVisibility); - - MessageHelper.success(receiver, lang.get("npc-command-fix-success")); - return true; + translator.translate("npc_fix_success").replace("npc", npc.getData().getName()).send(sender); } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/GlowingCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/GlowingCMD.java index 366ab2ef..900040ce 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/GlowingCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/GlowingCMD.java @@ -1,71 +1,72 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; +import de.oliver.fancylib.translations.message.SimpleMessage; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; +import de.oliver.fancynpcs.utils.GlowingColor; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.stream.Stream; - -public class GlowingCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return Stream.of("true", "false") - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); - } - - return null; - } - - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } +public enum GlowingCMD { + INSTANCE; // SINGLETON + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - boolean glowing; - try { - glowing = Boolean.parseBoolean(args[2]); - } catch (Exception e) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; + @Command("npc glowing [color]") + @Permission("fancynpcs.command.npc.glowing") + public void onGlowing( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @Nullable GlowingColor color + ) { + // Handling 'toggle' state, which means inverting the current state. + if (color == null) { + // Inverting the current glowing state, so the command works like a toggle. + final boolean isGlowingToggled = !npc.getData().isGlowing(); + // Calling the event and updating the state if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.GLOWING, isGlowingToggled, sender).callEvent()) { + npc.getData().setGlowing(isGlowingToggled); + npc.updateForAll(); + translator.translate(isGlowingToggled ? "npc_glowing_set_true" : "npc_glowing_set_false").replace("npc", npc.getData().getName()).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); + } } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.GLOWING, glowing, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().setGlowing(glowing); - npc.updateForAll(); - - if (glowing) { - MessageHelper.success(receiver, lang.get("npc-command-glowing-true")); + // Handling 'disabled' state, which means disabling glowing state. + else if (color == GlowingColor.DISABLED) { + // Calling the event and updating the state if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.GLOWING, false, sender).callEvent()) { + npc.getData().setGlowing(false); + npc.updateForAll(); + translator.translate("npc_glowing_set_false").replace("npc", npc.getData().getName()).send(sender); } else { - MessageHelper.success(receiver, lang.get("npc-command-glowing-false")); + translator.translate("command_npc_modification_cancelled").send(sender); + } + // Handling 'color' state, which means enabling glowing and changing the color to desired one. + } else if (npc.getData().isGlowing() || new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.GLOWING, true, sender).callEvent()) { + // Calling the event and updating the glowing color if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.GLOWING_COLOR, color.getColor(), sender).callEvent()) { + npc.getData().setGlowingColor(color.getColor()); + // Updating the glowing state, if previously disabled. + if (!npc.getData().isGlowing()) + npc.getData().setGlowing(true); + npc.updateForAll(); + translator.translate("npc_glowing_set_color_success") + .replace("npc", npc.getData().getName()) + .replace("color", ((SimpleMessage) translator.translate(color.getTranslationKey())).getMessage()) + .send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); } } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/GlowingColorCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/GlowingColorCMD.java deleted file mode 100644 index 82d6e321..00000000 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/GlowingColorCMD.java +++ /dev/null @@ -1,64 +0,0 @@ -package de.oliver.fancynpcs.commands.npc; - -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; -import de.oliver.fancynpcs.FancyNpcs; -import de.oliver.fancynpcs.api.Npc; -import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public class GlowingColorCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return NamedTextColor.NAMES.keys().stream() - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); - } - - return null; - } - - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - NamedTextColor color = NamedTextColor.NAMES.value(args[2]); - if (color == null) { - MessageHelper.error(receiver, lang.get("npc-command-glowingColor-invalid")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.GLOWING_COLOR, color, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().setGlowingColor(color); - npc.updateForAll(); - MessageHelper.success(receiver, lang.get("npc-command-glowingColor-updated")); - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - } - - return true; - } -} diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/HelpCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/HelpCMD.java new file mode 100644 index 00000000..42d321ce --- /dev/null +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/HelpCMD.java @@ -0,0 +1,58 @@ +package de.oliver.fancynpcs.commands.npc; + +import de.oliver.fancylib.translations.Translator; +import de.oliver.fancylib.translations.message.MultiMessage; +import de.oliver.fancynpcs.FancyNpcs; +import org.bukkit.command.CommandSender; +import org.incendo.cloud.annotations.Argument; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Default; +import org.incendo.cloud.annotations.Permission; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; + +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.NotNull; + +public enum HelpCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + @Command("npc help [page]") + @Permission("fancynpcs.command.npc") + public void onHelp( + final @NotNull CommandSender sender, + final @Argument(suggestions = "HelpCMD/page") @Default("1") int page + ) { + // Getting the (full) help contents. + final MultiMessage contents = (MultiMessage) translator.translate("npc_help_contents"); + // Calculating max page number. + final int maxPage = (int) Math.ceil(contents.getRawMessages().size() / 6F); + // Getting the requested page. Defaults to 1 for invalid input and is capped by number of the last page. + final int finalPage = Math.clamp(page, 1, maxPage); + // Sending help contents to the sender. + translator.translate("npc_help_page_header").replace("page", String.valueOf(finalPage)).replace("max_page", String.valueOf(maxPage)).send(sender); + contents.page(finalPage, 6).send(sender); + translator.translate("npc_help_page_footer").replace("page", String.valueOf(finalPage)).replace("max_page", String.valueOf(maxPage)).send(sender); + } + + /* PARSERS AND SUGGESTIONS */ + + @Suggestions("HelpCMD/page") + public List suggestPage(final CommandContext context, final CommandInput input) { + // Getting the (full) help contents. + final MultiMessage contents = (MultiMessage) translator.translate("npc_help_contents"); + // Calculating max page number. + final int maxPage = contents.getRawMessages().size() / 6 + 1; + // Returning suggestions... + return new ArrayList<>() {{ + for (int i = 1; i <= maxPage; i++) + add(String.valueOf(i)); + }}; + } + +} diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/InfoCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/InfoCMD.java index e5399eb1..7955bd3e 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/InfoCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/InfoCMD.java @@ -1,73 +1,73 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; +import de.oliver.fancylib.translations.message.Message; +import de.oliver.fancylib.translations.message.SimpleMessage; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; -import de.oliver.fancynpcs.commands.Subcommand; +import de.oliver.fancynpcs.api.util.Interval; +import de.oliver.fancynpcs.api.util.Interval.Unit; +import de.oliver.fancynpcs.utils.GlowingColor; import org.bukkit.Location; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public class InfoCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - return null; - } +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } +import java.text.DecimalFormat; - Location loc = npc.getData().getLocation(); - - MessageHelper.info(receiver, "NPC: " + npc.getData().getName()); - MessageHelper.info(receiver, " - Id: " + npc.getData().getId()); - MessageHelper.info(receiver, " - Creator: " + npc.getData().getCreator()); - MessageHelper.info(receiver, " - Display name: " + npc.getData().getDisplayName()); - MessageHelper.info(receiver, " - Location: " + loc.getWorld().getName() + " " + loc.getBlockX() + "/" + loc.getBlockY() + "/" + loc.getBlockZ()); - MessageHelper.info(receiver, " - Type: " + npc.getData().getType().name()); - MessageHelper.info(receiver, " - Show in tab: " + npc.getData().isShowInTab()); - MessageHelper.info(receiver, " - Turn to player: " + npc.getData().isTurnToPlayer()); - MessageHelper.info(receiver, " - Is glowing: " + npc.getData().isGlowing()); - MessageHelper.info(receiver, " - Glowing color: " + npc.getData().getGlowingColor().toString()); - MessageHelper.info(receiver, " - Is collidable: " + npc.getData().isCollidable()); - MessageHelper.info(receiver, " - Interaction cooldown: " + npc.getData().getInteractionCooldown() + " seconds"); - MessageHelper.info(receiver, " - Server Command: " + npc.getData().getServerCommand()); - - if (!npc.getData().getPlayerCommands().isEmpty()) { - MessageHelper.info(receiver, " - Player commands:"); - for (int i = 0; i < npc.getData().getMessages().size(); i++) { - MessageHelper.info(receiver, " " + (i + 1) + ": " + npc.getData().getPlayerCommands().get(i)); - } - } +import org.jetbrains.annotations.NotNull; - if (!npc.getData().getMessages().isEmpty()) { - MessageHelper.info(receiver, " - Messages:"); - for (int i = 0; i < npc.getData().getMessages().size(); i++) { - MessageHelper.info(receiver, " " + (i + 1) + ": " + npc.getData().getMessages().get(i)); - } - } +public enum InfoCMD { + INSTANCE; // SINGLETON + private static final DecimalFormat COORDS_FORMAT = new DecimalFormat("#.##"); + private static final DecimalFormat SECONDS_FORMAT = new DecimalFormat("#,###.#"); + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - npc.getData().getEquipment().forEach((slot, item) -> - MessageHelper.info(receiver, " - Equipment: " + slot.name() + " -> " + item.getType().name()) - ); + @Command("npc info ") + @Permission("fancynpcs.command.npc.info") + public void onInfo( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + final Location loc = npc.getData().getLocation(); + final Interval interactionCooldown = Interval.of(npc.getData().getInteractionCooldown(), Unit.SECONDS); + // Getting the translated glowing state. This should never throw because all supported NamedTextColor objects has their mapping in GlowingColor enum. + final String glowingStateTranslated = (!npc.getData().isGlowing() || npc.getData().getGlowingColor() != null) + ? ((SimpleMessage) translator.translate(GlowingColor.fromAdventure(npc.getData().getGlowingColor()).getTranslationKey())).getMessage() + : ((SimpleMessage) translator.translate("disabled")).getMessage(); + final Message message = translator.translate("npc_info_general") + .replace("name", npc.getData().getName()) + .replace("id", npc.getData().getId()) + .replace("id_short", npc.getData().getId().substring(0, 13) + "...") + .replace("creator", npc.getData().getCreator().toString()) + .replace("creator_short", npc.getData().getCreator().toString().substring(0, 13) + "...") + .replace("displayname", npc.getData().getDisplayName()) + .replace("type", "") // Not ideal solution but should work fine for now. + .replace("location_x", COORDS_FORMAT.format(loc.x())) + .replace("location_y", COORDS_FORMAT.format(loc.y())) + .replace("location_z", COORDS_FORMAT.format(loc.z())) + .replace("world", loc.getWorld().getName()) + .replace("glow", glowingStateTranslated) + .replace("is_collidable", getTranslatedBoolean(npc.getData().isCollidable())) + .replace("is_turn_to_player", getTranslatedBoolean(npc.getData().isTurnToPlayer())) + .replace("is_show_in_tab", getTranslatedBoolean(npc.getData().isShowInTab())) + .replace("is_skin_mirror", getTranslatedBoolean(npc.getData().isMirrorSkin())) + .replace("interaction_cooldown", npc.getData().getInteractionCooldown() <= 0 ? getTranslatedState(false) : interactionCooldown.toString()) + .replace("messages_total", String.valueOf(npc.getData().getMessages().size())) + .replace("player_commands_total", String.valueOf(npc.getData().getPlayerCommands().size())) + .replace("server_commands_total", String.valueOf(npc.getData().getServerCommands().size())); + message.send(sender); + } - npc.getData().getAttributes().forEach((attribute, value) -> - MessageHelper.info(receiver, " - Attribute: " + attribute.getName() + " -> " + value) - ); + // NOTE: Might need to be improved later down the line, should get work done for now. + private @NotNull String getTranslatedBoolean(final boolean bool) { + return (bool) ? ((SimpleMessage) translator.translate("true")).getMessage() : ((SimpleMessage) translator.translate("false")).getMessage(); + } - return false; + // NOTE: Might need to be improved later down the line, should get work done for now. + private @NotNull String getTranslatedState(final boolean bool) { + return (bool) ? ((SimpleMessage) translator.translate("enabled")).getMessage() : ((SimpleMessage) translator.translate("disabled")).getMessage(); } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/InteractionCooldownCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/InteractionCooldownCMD.java index d5c32ed3..8254a116 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/InteractionCooldownCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/InteractionCooldownCMD.java @@ -1,57 +1,103 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; +import de.oliver.fancynpcs.api.util.Interval; +import de.oliver.fancynpcs.api.util.Interval.Unit; +import de.oliver.fancynpcs.commands.exceptions.ReplyingParseException; +import it.unimi.dsi.fastutil.Pair; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; +import org.incendo.cloud.annotations.Argument; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; +import org.incendo.cloud.annotations.parser.Parser; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Stream; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; +public enum InteractionCooldownCMD { + INSTANCE; // SINGLETON -public class InteractionCooldownCMD implements Subcommand { + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); + private static final Pattern SPLIT_PATTERN = Pattern.compile("(?<=\\d)(?=\\D)"); - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - return null; + @Command("npc interaction_cooldown ") + @Permission("fancynpcs.command.npc.interaction_cooldown") + public void onInteractionCooldown( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull @Argument(parserName = "InteractionCooldownCMD/cooldown") Interval cooldown + ) { + // Calling the event and updating the cooldown if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.INTERACTION_COOLDOWN, cooldown, sender).callEvent()) { + npc.getData().setInteractionCooldown((float) cooldown.as(Unit.MILLISECONDS) / 1000F); + translator.translate(cooldown.as(Unit.MILLISECONDS) != 0 ? "npc_interaction_cooldown_set" : "npc_interaction_cooldown_disabled") + .replace("npc", npc.getData().getName()) + .replace("time", cooldown.toString()) + .send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); + } } - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } + /* PARSERS AND SUGGESTIONS */ - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } + @Parser(name = "InteractionCooldownCMD/cooldown", suggestions = "InteractionCooldownCMD/cooldown") + public @NotNull Interval parse(final CommandContext context, final CommandInput input) { + final String value = input.readString(); + // Handling 'disabled' as interval of 0 milliseconds. This is how plugin determines whether interaction cooldown is enabled or not. + if (value.equalsIgnoreCase("disabled")) + return Interval.of(0, Unit.MILLISECONDS); + // Splitting user input between a digit and a letter. + final String[] split = SPLIT_PATTERN.split(value); + final @Nullable Long num = (split.length == 2) ? parseLong(split[0]) : null; + final @Nullable Unit unit = (split.length == 2) ? Unit.fromShortCode(split[1].toLowerCase()) : null; + // Sending error message to the sender if input cannot be converted to a valid interval. + if (num == null || unit == null) + throw ReplyingParseException.replying(() -> translator.translate("command_invalid_interval").replaceStripped("input", value).send(context.sender())); + return Interval.of(Math.max(0, num), unit); + } - float cooldown; - try { - cooldown = Float.parseFloat(args[2]); - } catch (NumberFormatException e) { - MessageHelper.error(receiver, lang.get("could-not-parse-number")); - return false; - } + @Suggestions(value = "InteractionCooldownCMD/cooldown") + public @NotNull Collection suggest(final CommandContext context, final CommandInput input) { + final String value = input.readString(); + // Splitting user input between a digit and a letter. + final String[] split = SPLIT_PATTERN.split(value); + final @Nullable Long num = parseLong(split[0]); + // Checking that the number is not null. + return (num == null || num <= 0) + ? List.of("30s", "5min", "8h", "disabled") + : new ArrayList<>() {{ + add("disabled"); + addAll(Stream.of( + Pair.of(Interval.of(num, Unit.MILLISECONDS), Unit.MILLISECONDS), + Pair.of(Interval.of(num, Unit.SECONDS), Unit.SECONDS), + Pair.of(Interval.of(num, Unit.MINUTES), Unit.MINUTES), + Pair.of(Interval.of(num, Unit.HOURS), Unit.HOURS) + ).map(pair -> num + pair.second().getShortCode()).toList()); + }}; + } - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.INTERACTION_COOLDOWN, cooldown, receiver); - npcModifyEvent.callEvent(); + /* UTILITY METHODS */ - if (!npcModifyEvent.isCancelled()) { - npc.getData().setInteractionCooldown(cooldown); - MessageHelper.success(receiver, lang.get("npc-command-interactioncooldown-updated")); - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + private @Nullable Long parseLong(final @NotNull String value) { + try { + return Long.parseLong(value); + } catch (final NumberFormatException e) { + return null; } - - return true; } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/ListCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/ListCMD.java index 48d84344..d1e7b0ca 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/ListCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/ListCMD.java @@ -1,61 +1,81 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; -import de.oliver.fancynpcs.commands.Subcommand; import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Flag; +import org.incendo.cloud.annotations.Permission; import java.text.DecimalFormat; -import java.util.Collection; -import java.util.List; +import java.util.Comparator; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; -public class ListCMD implements Subcommand { +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); +public enum ListCMD { + INSTANCE; // SINGLETON - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - return null; - } + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (!receiver.hasPermission("fancynpcs.npc.list") && !receiver.hasPermission("fancynpcs.npc.*")) { - MessageHelper.error(receiver, lang.get("no-permission-subcommand")); - return false; - } + private static final DecimalFormat COORDS_FORMAT = new DecimalFormat("#.##"); - MessageHelper.info(receiver, lang.get("npc-command-list-header")); - - Collection allNpcs = FancyNpcs.getInstance().getNpcManagerImpl().getAllNpcs(); - if (FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() && receiver instanceof Player player) { - allNpcs = allNpcs.stream() - .filter(n -> n.getData().getCreator().equals(player.getUniqueId())) - .toList(); - } + static { + COORDS_FORMAT.setMinimumFractionDigits(2); + } - if (allNpcs.isEmpty()) { - MessageHelper.warning(receiver, lang.get("npc-command-list-no-npcs")); - } else { - final DecimalFormat df = new DecimalFormat("#########.##"); - for (Npc n : allNpcs) { - MessageHelper.info(receiver, lang.get( - "npc-command-list-tp-hover", - "name", n.getData().getName(), - "x", df.format(n.getData().getLocation().x()), - "y", df.format(n.getData().getLocation().y()), - "z", df.format(n.getData().getLocation().z()), - "tp_cmd", "/tp " + n.getData().getLocation().x() + " " + n.getData().getLocation().y() + " " + n.getData().getLocation().z() - ) - ); - } + @Command("npc list") + @Permission("fancynpcs.command.npc.list") + public void onCommand( + final @NotNull CommandSender sender, + final @Nullable @Flag("type") EntityType type, + final @Nullable @Flag("sort") SortType sort + ) { + Stream stream = FancyNpcs.getInstance().getNpcManagerImpl().getAllNpcs().stream(); + // Excluding NPCs not created by the sender, if PLAYER_NPCS_FEATURE_FLAG is enabled and sender is a player. + if (FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() && sender instanceof Player player) + stream = stream.filter(npc -> npc.getData().getCreator().equals(player.getUniqueId())); + // Excluding NPCs that are not of a specified type, if desired. + if (type != null) + stream = stream.filter(npc -> npc.getData().getType() == type); + // Sorting based on SortType choice. Defaults to SortType.NAME. There might be more sort types in the future which should be handled here accordingly. + switch (sort != null ? sort : SortType.NAME) { + case NAME -> stream = stream.sorted(Comparator.comparing(npc -> npc.getData().getName())); + case NAME_REVERSED -> stream = stream.sorted(Comparator.comparing(npc -> ((Npc) npc).getData().getName()).reversed()); // This needs a cast for some reason. } + translator.translate("npc_list_header").send(sender); + // Using AtomicInteger counter because streams don't expose entry index. + final AtomicInteger count = new AtomicInteger(0); + // Iterating over each NPC referenced in the stream. Usage of forEachOrdered should presumably preserve element order. + stream.forEachOrdered(npc -> { + translator.translate("npc_list_entry") + .replace("number", String.valueOf(count.incrementAndGet())) + .replace("npc", npc.getData().getName()) + .replace("location_x", COORDS_FORMAT.format(npc.getData().getLocation().x())) + .replace("location_y", COORDS_FORMAT.format(npc.getData().getLocation().y())) + .replace("location_z", COORDS_FORMAT.format(npc.getData().getLocation().z())) + .replace("world", npc.getData().getLocation().getWorld().getName()) + .send(sender); + }); + final int totalCount = FancyNpcs.getInstance().getNpcManager().getAllNpcs().size(); + translator.translate("npc_list_footer") + .replace("count", String.valueOf(count)) + .replace("count_formatted", "· ".repeat(3 - String.valueOf(count).length()) + count) + .replace("total", String.valueOf(FancyNpcs.getInstance().getNpcManager().getAllNpcs().size())) + .replace("total_formatted", "· ".repeat(3 - String.valueOf(totalCount).length()) + totalCount) + .send(sender); + } - return true; + /** + * {@link SortType ListCMD.SortType} enum contains all possible sort types for the {@code /npc list} command. + */ + public enum SortType { + NAME, NAME_REVERSED } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/MessageCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/MessageCMD.java index 0cc95a78..f1ded244 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/MessageCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/MessageCMD.java @@ -1,307 +1,244 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; +import me.dave.chatcolorhandler.ModernChatColorHandler; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentIteratorType; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.ClickEvent.Action; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.LinkedList; +import org.incendo.cloud.annotation.specifier.Greedy; +import org.incendo.cloud.annotations.Argument; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; + +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Queue; - -public class MessageCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return List.of("add", "set", "remove", "clear", "sendRandomly"); - } else if (args.length == 4) { - if (args[2].equalsIgnoreCase("set") || args[2].equalsIgnoreCase("remove")) { - List messages = new LinkedList<>(); - for (int i = 0; i < npc.getData().getMessages().size(); i++) { - messages.add(String.valueOf(i + 1)); - } - return messages; - } else if (args[2].equalsIgnoreCase("sendRandomly")) { - return List.of("true", "false"); - } - } else if (args.length == 5) { - if (args[2].equalsIgnoreCase("set")) { - int index; - try { - index = Integer.parseInt(args[3]); - } catch (NumberFormatException e) { - return null; - } - - if (index < 1 || index > npc.getData().getMessages().size()) { - return null; - } - - return List.of(npc.getData().getMessages().get(index - 1)); - } - } - - return null; - } - - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - if (args.length == 3 && args[2].equalsIgnoreCase("clear")) { - return clearMessages(receiver, npc, args); - } - - if (args.length == 4 && args[2].equalsIgnoreCase("sendRandomly")) { - return sendMessagesRandomly(receiver, npc, args); - } - - if (args.length == 4 && args[2].equalsIgnoreCase("remove")) { - return removeMessage(receiver, npc, args); - } +import java.util.stream.StreamSupport; - if (args.length >= 4 && args[2].equalsIgnoreCase("add")) { - return addMessage(receiver, npc, args); - } +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; - if (args.length >= 5 && args[2].equalsIgnoreCase("set")) { - return setMessage(receiver, npc, args); +public enum MessageCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + // Storing in a static variable to avoid re-creating the array each time suggestion is requested. + private static final List NONE_SUGGESTIONS = List.of("@none"); + + @Command("npc message add ") + @Permission("fancynpcs.command.npc.message.add") + public void onMessageAdd( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull @Argument(suggestions = "MessageCMD/none") @Greedy String message + ) { + // Handling '@none' as an empty message. + final String finalMessage = message.equalsIgnoreCase("@none") ? "" : message; + // Sending error message in case banned command has been found in the input. + if (hasBlockedCommands(finalMessage)) { + translator.translate("command_input_contains_blocked_command").send(sender); + return; + } + // Calling the event and adding message if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.MESSAGE_ADD, finalMessage, sender).callEvent()) { + npc.getData().getMessages().add(finalMessage); + translator.translate("npc_message_add_success").replace("total", String.valueOf(npc.getData().getMessages().size())).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); } - - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; } - private boolean sendMessagesRandomly(CommandSender receiver, Npc npc, String[] args) { - if (args.length < 4) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - boolean sendMessagesRandomly; - try { - sendMessagesRandomly = Boolean.parseBoolean(args[3]); - } catch (Exception e) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.CUSTOM_MESSAGE, sendMessagesRandomly, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().setSendMessagesRandomly(sendMessagesRandomly); - - if (sendMessagesRandomly) { - MessageHelper.success(receiver, lang.get("npc-command-message-sendMessagesRandomly-true")); - } else { - MessageHelper.success(receiver, lang.get("npc-command-message-sendMessagesRandomly-false")); - npc.updateForAll(); // move to default pos - } + @Command("npc message set [number] [message]") + @Permission("fancynpcs.command.npc.message.set") + public void onMessageSet( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull @Argument(suggestions = "MessageCMD/number_range") Integer number, + final @NotNull @Argument(suggestions = "MessageCMD/none") @Greedy String message + ) { + // Handling '@none' as an empty message. + final String finalMessage = message.equalsIgnoreCase("@none") ? "" : message; + // Sending error message in case banned command has been found in the input. + if (hasBlockedCommands(finalMessage)) { + translator.translate("command_input_contains_blocked_command").send(sender); + return; + } + // Getting the total count of messages that are currently in the list. + final int totalCount = npc.getData().getMessages().size(); + // Sending error message if the list is empty. + if (totalCount == 0) { + translator.translate("npc_message_set_failure_list_is_empty").send(sender); + return; + } + // Sending error message if provided number is lower than 0 or higher than the list size. + if (number < 1 || number > totalCount) { + translator.translate("npc_message_set_failure_not_in_range").replace("input", String.valueOf(number)).replace("max", String.valueOf(totalCount)).send(sender); + return; + } + // User-specified number starts from 1, while index starts from 0. Subtracting 1 from the provided number to get the list index. + final int index = number - 1; + // Calling the event and setting message if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.MESSAGE_SET, new Object[]{index, finalMessage}, sender).callEvent()) { + npc.getData().getMessages().set(index, finalMessage); + translator.translate("npc_message_set_success") + .replace("number", String.valueOf(number)) + .replace("total", String.valueOf(totalCount)) // Total count remains the same, no entry has been added/removed from the list. + .send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } - private boolean addMessage(CommandSender receiver, Npc npc, String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - String message = ""; - for (int i = 3; i < args.length; i++) { - message += args[i] + " "; - } - - message = message.substring(0, message.length() - 1); - - if (message.equalsIgnoreCase("none")) { - message = ""; - } - - if (FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() && hasIllegalCommand(message.toLowerCase())) { - MessageHelper.error(receiver, lang.get("illegal-command")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.CUSTOM_MESSAGE, message, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().addMessage(message); - MessageHelper.success(receiver, lang.get("npc-command-message-updated")); + @Command("npc message remove ") + @Permission("fancynpcs.command.npc.message.remove") + public void onMessageRemove( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @Argument(suggestions = "MessageCMD/number_range") int number + ) { + // Getting the total count of messages that are currently in the list. + final int totalCount = npc.getData().getMessages().size(); + // Sending error message if the list is empty. + if (totalCount == 0) { + translator.translate("npc_message_remove_failure_list_is_empty").send(sender); + return; + } + // Sending error message if provided number is lower than 0 or higher than the list size. + if (number < 1 || number > totalCount) { + translator.translate("npc_message_remove_failure_not_in_range").replace("input", String.valueOf(number)).replace("max", String.valueOf(totalCount)).send(sender); + return; + } + // User-specified number starts from 1, while index starts from 0. Subtracting 1 from the provided number to get the list index. + final int index = number - 1; + // Getting the message to pass to the NpcModifyEvent. + final String message = npc.getData().getMessages().get(index); + // Calling the event and removing message if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.MESSAGE_REMOVE, new Object[]{index, message}, sender).callEvent()) { + npc.getData().getMessages().remove(index); + translator.translate("npc_message_remove_success") + .replace("number", String.valueOf(number)) + .replace("total", String.valueOf(totalCount)) // Total count remains the same, no entry has been added/removed from the list. + .send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } - private boolean setMessage(CommandSender receiver, Npc npc, String[] args) { - if (args.length < 4) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - int index; - try { - index = Integer.parseInt(args[3]); - } catch (NumberFormatException e) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - if (index < 1 || index > npc.getData().getMessages().size()) { - MessageHelper.error(receiver, lang.get("npc-command-message-invalid-index")); - return false; - } - - String message = ""; - for (int i = 4; i < args.length; i++) { - message += args[i] + " "; - } - - message = message.substring(0, message.length() - 1); - - if (message.equalsIgnoreCase("none")) { - message = ""; - } - - if (hasIllegalCommand(message.toLowerCase())) { - MessageHelper.error(receiver, lang.get("illegal-command")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.CUSTOM_MESSAGE, message, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().getMessages().set(index - 1, message); - MessageHelper.success(receiver, lang.get("npc-command-message-updated")); + @Command("npc message clear") + @Permission("fancynpcs.command.npc.message.clear") + public void onMessageClear( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + final int total = npc.getData().getMessages().size(); + // Calling the event and clearing messages if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.MESSAGE_CLEAR, null, sender).callEvent()) { + npc.getData().getMessages().clear(); + translator.translate("npc_message_clear_success").replace("total", String.valueOf(total)).send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } - private boolean removeMessage(CommandSender receiver, Npc npc, String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - int index; - try { - index = Integer.parseInt(args[3]); - } catch (NumberFormatException e) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - if (index < 1 || index > npc.getData().getMessages().size()) { - MessageHelper.error(receiver, lang.get("npc-command-message-invalid-index")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.CUSTOM_MESSAGE, "", receiver); - npcModifyEvent.callEvent(); + @Command("npc message list") + @Permission("fancynpcs.command.npc.message.list") + public void onMessageList( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + // Sending error message if the list is empty. + if (npc.getData().getMessages().isEmpty()) { + translator.translate("npc_message_list_failure_empty").send(sender); + return; + } + translator.translate("npc_message_list_header").send(sender); + // Iterating over all messages attached to this NPC and sending them to the sender. + for (int i = 0; i < npc.getData().getMessages().size(); i++) { + final String message = npc.getData().getMessages().get(i); + translator.translate("npc_message_list_entry") + .replace("number", String.valueOf(i + 1)) + .replace("message", message) + .send(sender); + } + final int totalCount = npc.getData().getMessages().size(); + translator.translate("npc_message_list_footer") + .replace("total", String.valueOf(totalCount)) + .replace("total_formatted", "· ".repeat(3 - String.valueOf(totalCount).length()) + totalCount) + .send(sender); + } - if (!npcModifyEvent.isCancelled()) { - npc.getData().removeMessage(index - 1); - MessageHelper.success(receiver, lang.get("npc-command-message-updated")); + @Command("npc message send_randomly [state]") + @Permission("fancynpcs.command.npc.message.send_randomly") + public void onMessageSendRandomly( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @Nullable Boolean state + ) { + final boolean finalState = state != null ? state : !npc.getData().isSendMessagesRandomly(); + // Calling the event and setting send_randomly state if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.MESSAGE_SEND_RANDOMLY, finalState, sender).callEvent()) { + npc.getData().setSendMessagesRandomly(finalState); + npc.updateForAll(); + translator.translate(finalState ? "npc_message_send_randomly_set_true" : "npc_message_send_randomly_set_false").replace("npc", npc.getData().getName()).send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } - private boolean clearMessages(CommandSender receiver, Npc npc, String[] args) { - if (args.length < 2) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.CUSTOM_MESSAGE, "", receiver); - npcModifyEvent.callEvent(); - if (!npcModifyEvent.isCancelled()) { - npc.getData().getMessages().clear(); - MessageHelper.success(receiver, lang.get("npc-command-message-updated")); - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - } + /* PARSERS AND SUGGESTIONS */ - return true; + @Suggestions("MessageCMD/none") + public List suggestNone(final CommandContext context, final CommandInput input) { + return NONE_SUGGESTIONS; } - // TEST - private boolean hasIllegalCommand(String message) { - message = message.replace("/", ""); + @Suggestions("MessageCMD/number_range") // Generates number range suggestions based on the number of messages. + public List suggestNumber(final CommandContext context, final CommandInput input) { + final Npc npc = context.getOrDefault("npc", null); + return npc == null || npc.getData().getMessages().isEmpty() + ? Collections.emptyList() + : new ArrayList<>() {{ + for (int i = 0; i < npc.getData().getMessages().size(); i++) + add(String.valueOf(i + 1)); + }}; + } - char[] chars = message.toCharArray(); - Queue tokens = new LinkedList<>(); - List blockedCommands = FancyNpcs.getInstance().getFancyNpcConfig().getBlockedCommands(); - String currentWord = ""; - for (int i = 0; i < chars.length; i++) { - char c = chars[i]; - if (c == ' ') { - if (!currentWord.equals(" ") && !currentWord.equals("")) - tokens.add(currentWord); - currentWord = ""; - } else if (c == '<' || c == '>' || c == ':') { - if (!currentWord.equals(" ") && !currentWord.equals("")) - tokens.add(currentWord); - tokens.add(String.valueOf(c)); - currentWord = ""; - } else { - currentWord = currentWord + c; - } - } - if (currentWord.length() > 0 && !currentWord.equals(" ")) - tokens.add(currentWord); - while (!tokens.isEmpty()) { - if (((String) tokens.poll()).equalsIgnoreCase("run_command") && ((String) tokens.poll()).equalsIgnoreCase(":")) { - String command = tokens.poll(); - command = command.replace("\"", ""); - command = command.replace("'", ""); - command = command.replace("´", ""); - command = command.replace("`", ""); - for (String blockedCommand : blockedCommands) { - if (command.toLowerCase().startsWith(blockedCommand.toLowerCase())) { - return true; - } - } + /* UTILITY METHODS */ + + /** Returns {@code true} if specified component contains blocked command, {@code false} otherwise. */ + private boolean hasBlockedCommands(final @NotNull String message) { + // Converting message to a Component. + final Component component = ModernChatColorHandler.translate(message); + // Getting the list of all blocked commands. + final List blockedCommands = FancyNpcs.getInstance().getFancyNpcConfig().getBlockedCommands(); + // Iterating over all elements of the component. + return StreamSupport.stream(component.iterable(ComponentIteratorType.DEPTH_FIRST).spliterator(), false).anyMatch(it -> { + final ClickEvent event = it.clickEvent(); + // We only care about click events with run_command as an action. Continuing if not found. + if (event == null || event.action() != Action.RUN_COMMAND) + return false; + // Iterating over list of blocked commands... + for (final String blockedCommand : blockedCommands) { + // Transforming the command to a base command with trailed whitespaces and slashes. This also removes namespaced part from the beginning of the command. + final String transformedBaseCommand = blockedCommand.replace('/', ' ').strip().split(" ")[0].replaceAll(".*?:+", ""); + // Comparing click event value with the transformed base command. Returning the result. + if (event.value().replace('/', ' ').strip().split(" ")[0].replaceAll(".*?:+", "").equalsIgnoreCase(transformedBaseCommand)) + return true; } - } - - return false; + // Returning false as no blocked commands has been found. + return false; + }); } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/MirrorSkinCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/MirrorSkinCMD.java deleted file mode 100644 index d1acd79f..00000000 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/MirrorSkinCMD.java +++ /dev/null @@ -1,73 +0,0 @@ -package de.oliver.fancynpcs.commands.npc; - -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; -import de.oliver.fancynpcs.FancyNpcs; -import de.oliver.fancynpcs.api.Npc; -import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.stream.Stream; - -public class MirrorSkinCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return Stream.of("true", "false") - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); - } - - return null; - } - - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - boolean mirrorSkin; - try { - mirrorSkin = Boolean.parseBoolean(args[2]); - } catch (Exception e) { - MessageHelper.error(receiver, lang.get("npc-command-wrong_usage")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.MIRROR_SKIN, mirrorSkin, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().setMirrorSkin(mirrorSkin); - npc.removeForAll(); - npc.create(); - npc.spawnForAll(); - - if (mirrorSkin) { - MessageHelper.success(receiver, lang.get("npc-command-mirrorSkin-true")); - } else { - MessageHelper.success(receiver, lang.get("npc-command-mirrorSkin-false")); - } - } else { - MessageHelper.error(receiver, lang.get("npc-command-mirrorSkin-cancelled")); - } - - return true; - } -} diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/MoveHereCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/MoveHereCMD.java index cc772e94..67861c83 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/MoveHereCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/MoveHereCMD.java @@ -1,62 +1,41 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; import org.bukkit.Location; -import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public class MoveHereCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - return null; - } - - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (!(receiver instanceof Player player)) { - MessageHelper.error(receiver, lang.get("npc-command.only-players")); - return false; - } +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - Location location = player.getLocation(); - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.LOCATION, location, receiver); - npcModifyEvent.callEvent(); - - String oldWorld = npc.getData().getLocation().getWorld().getName(); +import org.jetbrains.annotations.NotNull; - if (!npcModifyEvent.isCancelled()) { +public enum MoveHereCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + @Command(value = "npc move_here ", requiredSender = Player.class) + @Permission("fancynpcs.command.npc.move_here") + public void onCommand( + final @NotNull Player sender, + final @NotNull Npc npc + ) { + final Location location = sender.getLocation(); + final String oldWorld = npc.getData().getLocation().getWorld().getName(); + // Calling the event and moving the NPc to location of the sender, if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.LOCATION, location, sender).callEvent()) { npc.getData().setLocation(location); - if (oldWorld.equals(location.getWorld().getName())) { npc.updateForAll(); } else { npc.removeForAll(); npc.spawnForAll(); } - - MessageHelper.success(receiver, lang.get("npc-command-moveHere-moved")); + translator.translate("npc_move_here_success").replace("npc", npc.getData().getName()).send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/MoveToCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/MoveToCMD.java new file mode 100644 index 00000000..29d0a4ed --- /dev/null +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/MoveToCMD.java @@ -0,0 +1,64 @@ +package de.oliver.fancynpcs.commands.npc; + +import de.oliver.fancylib.translations.Translator; +import de.oliver.fancynpcs.FancyNpcs; +import de.oliver.fancynpcs.api.Npc; +import de.oliver.fancynpcs.api.events.NpcModifyEvent; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.incendo.cloud.annotations.Argument; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Flag; +import org.incendo.cloud.annotations.Permission; + +import java.text.DecimalFormat; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public enum MoveToCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + private static final DecimalFormat COORDS_FORMAT = new DecimalFormat("#.##"); + + @Command("npc move_to [world]") + @Permission("fancynpcs.command.npc.move_to") + public void onCommand( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull @Argument(suggestions = "relative_location") Location location, + final @Nullable World world, + final @Flag("look-in-my-direction") boolean shouldLookInSenderDirection + ) { + // Finalizing World argument. Player-like senders don't have to specify the 'world' argument which then defaults to the World sender is currently in. + final World finalWorld = (world == null && sender instanceof Player player) ? player.getWorld() : world; + // Sending error message if finalized World argument ended up being null. This can happen when command is executed by console and 'world' argument was not specified. + if (finalWorld == null) { + translator.translate("npc_move_to_failure_must_specify_world").send(sender); + return; + } + // Updating World of the finalized Location. This should never pass a null value. + location.setWorld(finalWorld); + // Updating direction NPC will be looking at. Only if '--look-in-my-direction' is present and sender is player. + if (shouldLookInSenderDirection && sender instanceof Player player) + location.setDirection(player.getLocation().subtract(location).toVector()); + // Calling the event and re-locating NPC if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.LOCATION, location, sender).callEvent()) { + npc.getData().setLocation(location); + npc.updateForAll(); + translator.translate("npc_move_to_success") + .replace("npc", npc.getData().getName()) + .replace("x", COORDS_FORMAT.format(location.x())) + .replace("y", COORDS_FORMAT.format(location.y())) + .replace("z", COORDS_FORMAT.format(location.z())) + .replace("world", finalWorld.getName()) + .send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); + } + } +} diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/NearbyCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/NearbyCMD.java new file mode 100644 index 00000000..7a29eb12 --- /dev/null +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/NearbyCMD.java @@ -0,0 +1,98 @@ +package de.oliver.fancynpcs.commands.npc; + +import de.oliver.fancylib.translations.Translator; +import de.oliver.fancynpcs.FancyNpcs; +import de.oliver.fancynpcs.api.Npc; +import org.bukkit.Location; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Flag; +import org.incendo.cloud.annotations.Permission; + +import java.text.DecimalFormat; +import java.util.Comparator; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public enum NearbyCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + private static final DecimalFormat COORDS_FORMAT = new DecimalFormat("#.##"); + private static final DecimalFormat DISTANCE_FORMAT = new DecimalFormat("#.#"); + + static { + COORDS_FORMAT.setMinimumFractionDigits(2); + } + + @Command(value = "npc nearby", requiredSender = Player.class) + @Permission("fancynpcs.command.npc.nearby") + public void onCommand( + final @NotNull Player sender, + final @Nullable @Flag("radius") Long radius, + final @Nullable @Flag("type") EntityType type, + final @Nullable @Flag("sort") SortType sort + ) { + Stream stream = FancyNpcs.getInstance().getNpcManagerImpl().getAllNpcs().stream(); + // Getting senderLocation of the sender. + final Location senderLocation = sender.getLocation(); + // Creating a counter which is increased by 1 for every NPC present in player's world. + final AtomicInteger totalCount = new AtomicInteger(0); + // Excluding NPCs from different worlds. This also increments the counter defined above. + stream = stream.filter(npc -> { + if (npc.getData().getLocation().getWorld().equals(senderLocation.getWorld())) { + totalCount.incrementAndGet(); + return true; + } + return false; + }); + // Excluding NPCs not created by the sender, if PLAYER_NPCS_FEATURE_FLAG is enabled and sender is a player. + if (FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled()) + stream = stream.filter(npc -> npc.getData().getCreator().equals(sender.getUniqueId())); + // Excluding NPCs that are not in radius, if specified and sender is a player. (radius is calculated from the senderLocation of player) + if (radius != null) + stream = stream.filter(npc -> npc.getData().getLocation().distance(senderLocation) <= radius); + // Excluding NPCs that are not of a specified type, if desired. + if (type != null) + stream = stream.filter(npc -> npc.getData().getType() == type); + // Sorting based on SortType choice. Defaults to SortType.NEAREST. There might be more sort types in the future which should be handled here accordingly. + switch (sort != null ? sort : SortType.NEAREST) { // This should never produce NPE. + case NAME -> stream = stream.sorted(Comparator.comparing(npc -> npc.getData().getName())); + case NAME_REVERSED -> stream = stream.sorted(Comparator.comparing(npc -> ((Npc) npc).getData().getName()).reversed()); + case NEAREST -> stream = stream.sorted(Comparator.comparingDouble(npc -> npc.getData().getLocation().distance(senderLocation))); + case FARTHEST -> stream = stream.sorted(Comparator.comparingDouble(npc -> ((Npc) npc).getData().getLocation().distance(senderLocation)).reversed()); + } + translator.translate("npc_nearby_header").send(sender); + // Using AtomicInteger counter because streams don't expose entry index. + final AtomicInteger count = new AtomicInteger(0); + // Iterating over each NPC referenced in the stream. Usage of forEachOrdered should presumably preserve element order. + stream.forEachOrdered(npc -> { + translator.translate("npc_nearby_entry") + .replace("number", String.valueOf(count.incrementAndGet())) + .replace("npc", npc.getData().getName()) + .replace("distance", DISTANCE_FORMAT.format(npc.getData().getLocation().distance(senderLocation))) + .replace("location_x", COORDS_FORMAT.format(npc.getData().getLocation().x())) + .replace("location_y", COORDS_FORMAT.format(npc.getData().getLocation().y())) + .replace("location_z", COORDS_FORMAT.format(npc.getData().getLocation().z())) + .replace("world", npc.getData().getLocation().getWorld().getName()) + .send(sender); + }); + translator.translate("npc_nearby_footer") + .replace("count", String.valueOf(count)) + .replace("count_formatted", "· ".repeat(3 - String.valueOf(count).length()) + count) + .replace("total", String.valueOf(totalCount)) + .replace("total_formatted", "· ".repeat(3 - String.valueOf(totalCount).length()) + totalCount) + .send(sender); + } + + // SortType enum contains all possible sort types for the '/npc nearby' command. + public enum SortType { + NAME, NAME_REVERSED, NEAREST, FARTHEST + } + +} diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/NpcCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/NpcCMD.java deleted file mode 100644 index 03144b67..00000000 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/NpcCMD.java +++ /dev/null @@ -1,255 +0,0 @@ -package de.oliver.fancynpcs.commands.npc; - -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; -import de.oliver.fancynpcs.FancyNpcs; -import de.oliver.fancynpcs.api.Npc; -import de.oliver.fancynpcs.commands.Subcommand; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.stream.Stream; - -public class NpcCMD extends Command { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - private final Subcommand attributeCMD = new AttributeCMD(); - private final Subcommand collidableCMD = new CollidableCMD(); - private final Subcommand displayNameCMD = new DisplayNameCMD(); - private final Subcommand equipmentCMD = new EquipmentCMD(); - private final Subcommand glowingCMD = new GlowingCMD(); - private final Subcommand glowingColorCMD = new GlowingColorCMD(); - private final Subcommand messageCMD = new MessageCMD(); - private final Subcommand playerCommandCMD = new PlayerCommandCMD(); - private final Subcommand serverCommandCMD = new ServerCommandCMD(); - private final Subcommand showInTabCMD = new ShowInTabCMD(); - private final Subcommand teleportCMD = new TeleportCMD(); - private final Subcommand turnToPlayerCMD = new TurnToPlayerCMD(); - private final Subcommand typeCMD = new TypeCMD(); - private final Subcommand mirrorSkinCMD = new MirrorSkinCMD(); - private final Subcommand fixCMD = new FixCMD(); - - public NpcCMD() { - super("npc"); - setPermission("fancynpcs.npc"); - } - - @Override - public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player p)) { - MessageHelper.error(sender, lang.get("only-players")); - return null; - } - - List suggestions = new ArrayList<>(); - - if (args.length == 1) { - suggestions.addAll(Stream.of("help", "info", "message", "create", "remove", "copy", "fix", "skin", "movehere", "teleport", "displayName", "equipment", "playerCommand", "serverCommand", "showInTab", "glowing", "glowingColor", "collidable", "list", "turnToPlayer", "type", "attribute", "interactionCooldown", "mirrorSkin") - .filter(input -> input.toLowerCase().startsWith(args[0].toLowerCase())) - .toList()); - - } else if (args.length == 2 && !args[0].equalsIgnoreCase("create")) { - suggestions.addAll(FancyNpcs.getInstance().getNpcManagerImpl().getAllNpcs() - .stream() - .filter(npc -> !FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() || npc.getData().getCreator().equals(p.getUniqueId())) - .map(npc -> npc.getData().getName()) - .filter(input -> input.toLowerCase().startsWith(args[1].toLowerCase())) - .toList()); - } - - if (!suggestions.isEmpty()) return suggestions; - - if (args.length < 3) { - return Collections.emptyList(); - } - - String subcommand = args[0]; - String name = args[1]; - Npc npc = FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() ? - FancyNpcs.getInstance().getNpcManagerImpl().getNpc(name, p.getUniqueId()) : - FancyNpcs.getInstance().getNpcManagerImpl().getNpc(name); - - return switch (subcommand.toLowerCase()) { - case "attribute" -> attributeCMD.tabcompletion(p, npc, args); - case "collidable" -> collidableCMD.tabcompletion(p, npc, args); - case "displayname" -> displayNameCMD.tabcompletion(p, npc, args); - case "equipment" -> equipmentCMD.tabcompletion(p, npc, args); - case "glowing" -> glowingCMD.tabcompletion(p, npc, args); - case "glowingcolor" -> glowingColorCMD.tabcompletion(p, npc, args); - case "message" -> messageCMD.tabcompletion(p, npc, args); - case "playercommand" -> playerCommandCMD.tabcompletion(p, npc, args); - case "servercommand" -> serverCommandCMD.tabcompletion(p, npc, args); - case "showintab" -> showInTabCMD.tabcompletion(p, npc, args); - case "teleport" -> teleportCMD.tabcompletion(p, npc, args); - case "turntoplayer" -> turnToPlayerCMD.tabcompletion(p, npc, args); - case "type" -> typeCMD.tabcompletion(p, npc, args); - case "mirrorskin" -> mirrorSkinCMD.tabcompletion(p, npc, args); - case "fix" -> fixCMD.tabcompletion(p, npc, args); - - default -> Collections.emptyList(); - }; - } - - @Override - public boolean execute(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) { - if (!testPermission(sender)) { - return false; - } - - if (args.length >= 1 && args[0].equalsIgnoreCase("help")) { - if (!sender.hasPermission("fancynpcs.npc.help") && !sender.hasPermission("fancynpcs.npc.*")) { - MessageHelper.error(sender, lang.get("no-permission-subcommand")); - return false; - } - - MessageHelper.info(sender, lang.get("npc-command-help-header")); - MessageHelper.info(sender, lang.get("npc-command-help-create")); - MessageHelper.info(sender, lang.get("npc-command-help-remove")); - MessageHelper.info(sender, lang.get("npc-command-help-copy")); - MessageHelper.info(sender, lang.get("npc-command-help-list")); - MessageHelper.info(sender, lang.get("npc-command-help-skin")); - MessageHelper.info(sender, lang.get("npc-command-help-type")); - MessageHelper.info(sender, lang.get("npc-command-help-moveHere")); - MessageHelper.info(sender, lang.get("npc-command-help-teleport")); - MessageHelper.info(sender, lang.get("npc-command-help-displayName")); - MessageHelper.info(sender, lang.get("npc-command-help-equipment")); - MessageHelper.info(sender, lang.get("npc-command-help-message")); - MessageHelper.info(sender, lang.get("npc-command-help-playerCommand")); - MessageHelper.info(sender, lang.get("npc-command-help-serverCommand")); - MessageHelper.info(sender, lang.get("npc-command-help-showInTab")); - MessageHelper.info(sender, lang.get("npc-command-help-glowing")); - MessageHelper.info(sender, lang.get("npc-command-help-glowingColor")); - MessageHelper.info(sender, lang.get("npc-command-help-collidable")); - MessageHelper.info(sender, lang.get("npc-command-help-turnToPlayer")); - MessageHelper.info(sender, lang.get("npc-command-help-attribute")); - MessageHelper.info(sender, lang.get("npc-command-help-interactionCooldown")); - MessageHelper.info(sender, lang.get("npc-command-help-mirrorSkin")); - - return true; - } - - if (args.length >= 1 && args[0].equalsIgnoreCase("list")) { - return new ListCMD().run(sender, null, args); - } - - if (args.length < 2) { - MessageHelper.error(sender, lang.get("wrong-usage")); - return false; - } - - String subcommand = args[0]; - String name = args[1]; - Npc npc; - if (FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() && sender instanceof Player player) { - npc = FancyNpcs.getInstance().getNpcManagerImpl().getNpc(name, player.getUniqueId()); - } else { - npc = FancyNpcs.getInstance().getNpcManagerImpl().getNpc(name); - } - - - if (!sender.hasPermission("fancynpcs.npc." + subcommand) && !sender.hasPermission("fancynpcs.npc.*")) { - MessageHelper.error(sender, lang.get("no-permission-subcommand")); - return false; - } - - switch (subcommand.toLowerCase()) { - case "create" -> { - return new CreateCMD().run(sender, null, args); - } - - case "remove" -> { - return new RemoveCMD().run(sender, npc, args); - } - - case "copy" -> { - return new CopyCMD().run(sender, npc, args); - } - - case "info" -> { - return new InfoCMD().run(sender, npc, args); - } - - case "movehere" -> { - return new MoveHereCMD().run(sender, npc, args); - } - - case "teleport" -> { - return teleportCMD.run(sender, npc, args); - } - - case "message" -> { - return messageCMD.run(sender, npc, args); - } - - case "skin" -> { - return new SkinCMD().run(sender, npc, args); - } - - case "displayname" -> { - return displayNameCMD.run(sender, npc, args); - } - - case "equipment" -> { - return equipmentCMD.run(sender, npc, args); - } - - case "servercommand" -> { - return serverCommandCMD.run(sender, npc, args); - } - - case "playercommand" -> { - return playerCommandCMD.run(sender, npc, args); - } - - case "interactioncooldown" -> { - return new InteractionCooldownCMD().run(sender, npc, args); - } - - case "showintab" -> { - return showInTabCMD.run(sender, npc, args); - } - - case "glowing" -> { - return glowingCMD.run(sender, npc, args); - } - - case "glowingcolor" -> { - return glowingColorCMD.run(sender, npc, args); - } - - case "collidable" -> { - return collidableCMD.run(sender, npc, args); - } - - case "turntoplayer" -> { - return turnToPlayerCMD.run(sender, npc, args); - } - - case "type" -> { - return typeCMD.run(sender, npc, args); - } - - case "attribute" -> { - return attributeCMD.run(sender, npc, args); - } - - case "mirrorskin" -> { - return mirrorSkinCMD.run(sender, npc, args); - } - - case "fix" -> { - return fixCMD.run(sender, npc, args); - } - - default -> { - MessageHelper.error(sender, lang.get("wrong-usage")); - return false; - } - } - } -} diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/PlayerCommandCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/PlayerCommandCMD.java index b3d98098..fd96d141 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/PlayerCommandCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/PlayerCommandCMD.java @@ -1,228 +1,207 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.LinkedList; +import org.incendo.cloud.annotation.specifier.Greedy; +import org.incendo.cloud.annotations.Argument; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; -public class PlayerCommandCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player playedr, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return List.of("add", "set", "remove", "clear"); - } else if (args.length == 4) { - if (args[2].equalsIgnoreCase("set") || args[2].equalsIgnoreCase("remove")) { - List commands = new LinkedList<>(); - for (int i = 0; i < npc.getData().getPlayerCommands().size(); i++) { - commands.add(String.valueOf(i + 1)); - } - return commands; - } - } else if (args.length == 5) { - if (args[2].equalsIgnoreCase("set")) { - int index; - try { - index = Integer.parseInt(args[3]); - } catch (NumberFormatException e) { - return null; - } - - if (index < 1 || index > npc.getData().getPlayerCommands().size()) { - return null; - } - - return List.of(npc.getData().getPlayerCommands().get(index - 1)); - } - } - - return null; - } - - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - if (args.length == 3 && args[2].equalsIgnoreCase("clear")) { - return clearCommand(receiver, npc, args); - } - - if (args.length == 4 && args[2].equalsIgnoreCase("remove")) { - return removeCommand(receiver, npc, args); - } - - if (args.length >= 4 && args[2].equalsIgnoreCase("add")) { - return addCommand(receiver, npc, args); - } +import org.jetbrains.annotations.NotNull; - if (args.length >= 5 && args[2].equalsIgnoreCase("set")) { - return setCommand(receiver, npc, args); +public enum PlayerCommandCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + @Command("npc player_command add ") + @Permission("fancynpcs.command.npc.player_command.add") + public void onPlayerCommandAdd( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull @Argument(suggestions = "PlayerCommandCMD/commands") @Greedy String command + ) { + // Sending error message in case banned command has been found in the input. + if (hasBlockedCommands(command)) { + translator.translate("command_input_contains_blocked_command").send(sender); + return; + } + // Calling the event and adding player command if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.PLAYER_COMMAND_ADD, command, sender).callEvent()) { + npc.getData().getPlayerCommands().add(command); + translator.translate("npc_player_command_add_success").replace("total", String.valueOf(npc.getData().getPlayerCommands().size())).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); } - - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; } - private boolean addCommand(CommandSender receiver, Npc npc, String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - String command = ""; - for (int i = 3; i < args.length; i++) { - command += args[i] + " "; - } - - command = command.substring(0, command.length() - 1); - - if (command.equalsIgnoreCase("none")) { - command = ""; - } - - if (FancyNpcs.PLAYER_NPCS_FEATURE_FLAG.isEnabled() && isBlockedCommand(command.toLowerCase())) { - MessageHelper.error(receiver, lang.get("illegal-command")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.PLAYER_COMMAND, command, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().addPlayerCommand(command); - MessageHelper.success(receiver, lang.get("npc-command-playercommand-updated")); + @Command("npc player_command set ") + @Permission("fancynpcs.command.npc.player_command.set") + public void onPlayerCommandSet( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @Argument(suggestions = "PlayerCommandCMD/number_range") int number, + final @NotNull @Argument(suggestions = "PlayerCommandCMD/commands") @Greedy String command + ) { + // Sending error message in case banned command has been found in the input. + if (hasBlockedCommands(command)) { + translator.translate("command_input_contains_blocked_command").send(sender); + return; + } + // Getting the total count of player commands that are currently in the list. + final int totalCount = npc.getData().getPlayerCommands().size(); + // Sending error message if the list is empty. + if (totalCount == 0) { + translator.translate("npc_player_command_set_failure_list_is_empty").send(sender); + return; + } + // Sending error message if provided number is lower than 0 or higher than the list size. + if (number < 1 || number > totalCount) { + translator.translate("npc_player_command_set_failure_not_in_range").replace("input", String.valueOf(number)).replace("max", String.valueOf(totalCount)).send(sender); + return; + } + // User-specified number starts from 1, while index starts from 0. Subtracting 1 from the provided number to get the list index. + final int index = number - 1; + // Calling the event and setting player command if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.PLAYER_COMMAND_SET, new Object[]{index, command}, sender).callEvent()) { + npc.getData().getPlayerCommands().set(index, command); + translator.translate("npc_player_command_set_success") + .replace("number", String.valueOf(number)) + .replace("total", String.valueOf(totalCount)) // Total count remains the same, no entry has been added/removed from the list. + .send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } - private boolean setCommand(CommandSender receiver, Npc npc, String[] args) { - if (args.length < 4) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - int index; - try { - index = Integer.parseInt(args[3]); - } catch (NumberFormatException e) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - if (index < 1 || index > npc.getData().getPlayerCommands().size()) { - MessageHelper.error(receiver, lang.get("npc-command-playercommand-invalid-index")); - return false; - } - - String command = ""; - for (int i = 4; i < args.length; i++) { - command += args[i] + " "; - } - - command = command.substring(0, command.length() - 1); - - if (command.equalsIgnoreCase("none")) { - command = ""; - } - - if (isBlockedCommand(command.toLowerCase())) { - MessageHelper.error(receiver, lang.get("illegal-command")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.PLAYER_COMMAND, command, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().getPlayerCommands().set(index - 1, command); - MessageHelper.success(receiver, lang.get("npc-command-playercommand-updated")); + @Command("npc player_command remove ") + @Permission("fancynpcs.command.npc.player_command.remove") + public void onPlayerCommandRemove( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @Argument(suggestions = "PlayerCommandCMD/number_range") int number + ) { + // Getting the total count of player commands that are currently in the list. + final int totalCount = npc.getData().getPlayerCommands().size(); + // Sending error message if the list is empty. + if (totalCount == 0) { + translator.translate("npc_player_command_remove_failure_list_is_empty").send(sender); + return; + } + // Sending error message if provided number is lower than 0 or higher than the list size. + if (number < 1 || number > totalCount) { + translator.translate("npc_player_command_remove_failure_not_in_range").replace("input", String.valueOf(number)).replace("max", String.valueOf(totalCount)).send(sender); + return; + } + // User-specified number starts from 1, while index starts from 0. Subtracting 1 from the provided number to get the list index. + final int index = number - 1; + // Getting the message to pass to the NpcModifyEvent. + final String command = npc.getData().getPlayerCommands().get(index); + // Calling the event and removing player command if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.PLAYER_COMMAND_REMOVE, new Object[]{index, command}, sender).callEvent()) { + npc.getData().getPlayerCommands().remove(index); + // Sending success message to the sender. + translator.translate("npc_player_command_remove_success") + .replace("number", String.valueOf(number)) + .replace("total", String.valueOf(totalCount)) // Total count remains the same, no entry has been added/removed from the list. + .send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } - private boolean removeCommand(CommandSender receiver, Npc npc, String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - int index; - try { - index = Integer.parseInt(args[3]); - } catch (NumberFormatException e) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - if (index < 1 || index > npc.getData().getPlayerCommands().size()) { - MessageHelper.error(receiver, lang.get("npc-command-playercommand-invalid-index")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.PLAYER_COMMAND, "", receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().removePlayerCommand(index - 1); - MessageHelper.success(receiver, lang.get("npc-command-playercommand-updated")); + @Command("npc player_command clear") + @Permission("fancynpcs.command.npc.player_command.clear") + public void onPlayerCommandClear( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + final int total = npc.getData().getPlayerCommands().size(); + // Calling the event and clearing player commands if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.PLAYER_COMMAND_CLEAR, null, sender).callEvent()) { + npc.getData().getPlayerCommands().clear(); + translator.translate("npc_player_command_clear_success").replace("total", String.valueOf(total)).send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } - private boolean clearCommand(CommandSender receiver, Npc npc, String[] args) { - if (args.length < 2) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.PLAYER_COMMAND, "", receiver); - npcModifyEvent.callEvent(); + @Command("npc player_command list") + @Permission("fancynpcs.command.npc.player_command.list") + public void onPlayerCommandList( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + // Sending error message if the list is empty. + if (npc.getData().getPlayerCommands().isEmpty()) { + translator.translate("npc_player_command_list_failure_empty").send(sender); + return; + } + translator.translate("npc_player_command_list_header").send(sender); + // Iterating over all player commands attached to this NPC and sending them to the sender. + for (int i = 0; i < npc.getData().getPlayerCommands().size(); i++) { + final String command = npc.getData().getPlayerCommands().get(i); + translator.translate("npc_player_command_list_entry") + .replace("number", String.valueOf(i + 1)) + .replace("command", command) + .send(sender); + } + final int totalCount = npc.getData().getPlayerCommands().size(); + translator.translate("npc_player_command_list_footer") + .replace("total", String.valueOf(totalCount)) + .replace("total_formatted", "· ".repeat(3 - String.valueOf(totalCount).length()) + totalCount) + .send(sender); + } - if (!npcModifyEvent.isCancelled()) { - npc.getData().getPlayerCommands().clear(); - MessageHelper.success(receiver, lang.get("npc-command-playercommand-updated")); - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - } + /* PARSERS AND SUGGESTIONS */ + + @Suggestions("PlayerCommandCMD/number_range") // Generates number range suggestions based on the number of player commands. + public List suggestNumber(final CommandContext context, final CommandInput input) { + final Npc npc = context.getOrDefault("npc", null); + return npc == null || npc.getData().getPlayerCommands().isEmpty() + ? Collections.emptyList() + : new ArrayList<>() {{ + for (int i = 0; i < npc.getData().getPlayerCommands().size(); i++) + add(String.valueOf(i + 1)); + }}; + } - return true; + @Suggestions("PlayerCommandCMD/commands") // Suggests allowed (non-blocked) commands accessible by the command sender. + public Collection suggestCommand(final CommandContext context, final CommandInput input) { + return Bukkit.getServer().getCommandMap().getKnownCommands().values().stream() + .filter(command -> !command.getName().contains(":") && command.testPermission(context.sender()) && !hasBlockedCommands(command.getName())) + .map(org.bukkit.command.Command::getName) + .toList(); } - private boolean isBlockedCommand(String cmd) { - for (String blockedCommand : FancyNpcs.getInstance().getFancyNpcConfig().getBlockedCommands()) { - if (cmd.equalsIgnoreCase(blockedCommand) || cmd.toLowerCase().startsWith(blockedCommand.toLowerCase() + " ")) { + /* UTILITY METHODS */ + + /** Returns {@code true} if specified string contains a blocked command, {@code false} otherwise. */ + private boolean hasBlockedCommands(final @NotNull String string) { + // Getting the list of all blocked commands. + final List blockedCommands = FancyNpcs.getInstance().getFancyNpcConfig().getBlockedCommands(); + // Iterating over all elements of the component. + for (final String blockedCommand : blockedCommands) { + // Transforming the command to a base command with trailed whitespaces and slashes. This also removes namespaced part from the beginning of the command. + final String transformedBaseCommand = blockedCommand.replace('/', ' ').strip().split(" ")[0].replaceAll(".*?:+", ""); + // Comparing click event value with the transformed base command. Returning the result. + if (string.replace('/', ' ').strip().split(" ")[0].replaceAll(".*?:+", "").equalsIgnoreCase(transformedBaseCommand)) return true; - } } - + // Returning false as no blocked commands has been found. return false; } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/RemoveCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/RemoveCMD.java index a0886396..5fa009b2 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/RemoveCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/RemoveCMD.java @@ -1,39 +1,31 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcRemoveEvent; import de.oliver.fancynpcs.api.events.NpcStopLookingEvent; -import de.oliver.fancynpcs.commands.Subcommand; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.List; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; -public class RemoveCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); +import org.jetbrains.annotations.NotNull; - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - return null; - } +public enum RemoveCMD { + INSTANCE; // SINGLETON - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - NpcRemoveEvent npcRemoveEvent = new NpcRemoveEvent(npc, receiver); - npcRemoveEvent.callEvent(); - if (!npcRemoveEvent.isCancelled()) { + @Command("npc remove ") + @Permission("fancynpcs.command.npc.remove") + public void onRemove( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + // Calling the event and removing the NPC if not cancelled. + if (new NpcRemoveEvent(npc, sender).callEvent()) { npc.removeForAll(); // Iterating over all online players that the NPC is currently looking at. for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { @@ -45,11 +37,10 @@ public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull } } FancyNpcs.getInstance().getNpcManagerImpl().removeNpc(npc); - MessageHelper.success(receiver, lang.get("npc-command-remove-removed")); + translator.translate("npc_remove_success").replace("npc", npc.getData().getName()).send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-remove-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return false; } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/ServerCommandCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/ServerCommandCMD.java index 0ef13755..3230f0e7 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/ServerCommandCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/ServerCommandCMD.java @@ -1,74 +1,210 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.incendo.cloud.annotation.specifier.Greedy; +import org.incendo.cloud.annotations.Argument; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; -import java.util.stream.Stream; -public class ServerCommandCMD implements Subcommand { +import org.jetbrains.annotations.NotNull; + +public enum ServerCommandCMD { + INSTANCE; // SINGLETON - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return Stream.of("none") - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); + @Command("npc server_command add ") + @Permission("fancynpcs.command.npc.server_command.add") + public void onServerCommandAdd( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull @Argument(suggestions = "ServerCommandCMD/commands") @Greedy String command + ) { + // Sending error message in case banned command has been found in the input. + if (hasBlockedCommands(command)) { + translator.translate("command_input_contains_blocked_command").send(sender); + return; + } + // Calling the event and adding server command if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SERVER_COMMAND_ADD, command, sender).callEvent()) { + npc.getData().getServerCommands().add(command); + translator.translate("npc_server_command_add_success").replace("total", String.valueOf(npc.getData().getServerCommands().size())).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); } - - return null; } - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; + @Command("npc server_command set ") + @Permission("fancynpcs.command.npc_server_command.set") + public void onServerCommandSet( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @Argument(suggestions = "ServerCommandCMD/number_range") int number, + final @NotNull @Argument(suggestions = "ServerCommandCMD/commands") @Greedy String command + ) { + // Sending error message in case banned command has been found in the input. + if (hasBlockedCommands(command)) { + translator.translate("command_input_contains_blocked_command").send(sender); + return; } - - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; + // Getting the total count of server commands that are currently in the list. + final int totalCount = npc.getData().getServerCommands().size(); + // Sending error message if the list is empty. + if (totalCount == 0) { + translator.translate("npc_server_command_set_failure_list_is_empty").send(sender); + return; + } + // Sending error message if provided number is lower than 0 or higher than the list size. + if (number < 1 || number > totalCount) { + translator.translate("npc_server_command_set_failure_not_in_range").replace("input", String.valueOf(number)).replace("max", String.valueOf(totalCount)).send(sender); + return; + } + // User-specified number starts from 1, while index starts from 0. Subtracting 1 from the provided number to get the list index. + final int index = number - 1; + // Calling the event and setting server command if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SERVER_COMMAND_SET, new Object[]{index, command}, sender).callEvent()) { + npc.getData().getServerCommands().set(index, command); + translator.translate("npc_server_command_set_success") + .replace("number", String.valueOf(number)) + .replace("total", String.valueOf(totalCount)) // Total count remains the same, no entry has been added/removed from the list. + .send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); } + } - String cmd = ""; - for (int i = 2; i < args.length; i++) { - cmd += args[i] + " "; + @Command("npc server_command remove ") + @Permission("fancynpcs.command.npc_server_command.remove") + public void onServerCommandRemove( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @Argument(suggestions = "ServerCommandCMD/number_range") int number + ) { + // Getting the total count of server commands that are currently in the list. + final int totalCount = npc.getData().getServerCommands().size(); + // Sending error message if the list is empty. + if (totalCount == 0) { + translator.translate("npc_server_command_remove_failure_list_is_empty").send(sender); + return; + } + // Sending error message if provided number is lower than 0 or higher than the list size. + if (number < 1 || number > totalCount) { + translator.translate("npc_server_command_remove_failure_not_in_range").replace("input", String.valueOf(number)).replace("max", String.valueOf(totalCount)).send(sender); + return; + } + // User-specified number starts from 1, while index starts from 0. Subtracting 1 from the provided number to get the list index. + final int index = number - 1; + // Getting the message to pass to the NpcModifyEvent. + final String command = npc.getData().getServerCommands().get(index); + // Calling the event and removing server command if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SERVER_COMMAND_REMOVE, new Object[]{index, command}, sender).callEvent()) { + npc.getData().getServerCommands().remove(index); + // Sending success message to the sender. + translator.translate("npc_server_command_remove_success") + .replace("number", String.valueOf(number)) + .replace("total", String.valueOf(totalCount)) // Total count remains the same, no entry has been added/removed from the list. + .send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); } - cmd = cmd.substring(0, cmd.length() - 1); + } - if (cmd.equalsIgnoreCase("none")) { - cmd = ""; + @Command("npc server_command clear") + @Permission("fancynpcs.command.npc_server_command.clear") + public void onServerCommandClear( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + final int total = npc.getData().getServerCommands().size(); + // Calling the event and clearing server commands if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SERVER_COMMAND_CLEAR, null, sender).callEvent()) { + npc.getData().getServerCommands().clear(); + translator.translate("npc_server_command_clear_success").replace("total", String.valueOf(total)).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); } + } - for (String blockedCommand : FancyNpcs.getInstance().getFancyNpcConfig().getBlockedCommands()) { - if (cmd.equalsIgnoreCase(blockedCommand) || cmd.toLowerCase().startsWith(blockedCommand.toLowerCase() + " ")) { - MessageHelper.error(receiver, lang.get("illegal-command")); - return false; - } + @Command("npc server_command list") + @Permission("fancynpcs.command.npc_server_command.list") + public void onServerCommandList( + final @NotNull CommandSender sender, + final @NotNull Npc npc + ) { + // Sending error message if the list is empty. + if (npc.getData().getServerCommands().isEmpty()) { + translator.translate("npc_server_command_list_failure_empty").send(sender); + return; } + translator.translate("npc_server_command_list_header").send(sender); + // Iterating over all server commands attached to this NPC and sending them to the sender. + for (int i = 0; i < npc.getData().getServerCommands().size(); i++) { + final String command = npc.getData().getServerCommands().get(i); + translator.translate("npc_server_command_list_entry") + .replace("number", String.valueOf(i + 1)) + .replace("command", command) + .send(sender); + } + final int totalCount = npc.getData().getServerCommands().size(); + translator.translate("npc_server_command_list_footer") + .replace("total", String.valueOf(totalCount)) + .replace("total_formatted", "· ".repeat(3 - String.valueOf(totalCount).length()) + totalCount) + .send(sender); + } - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SERVER_COMMAND, cmd, receiver); - npcModifyEvent.callEvent(); + /* PARSERS AND SUGGESTIONS */ - if (!npcModifyEvent.isCancelled()) { - npc.getData().setServerCommand(cmd); - MessageHelper.success(receiver, lang.get("npc-command-serverCommand-updated")); - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - } + @Suggestions("ServerCommandCMD/number_range") + // Generates number range suggestions based on the number of server commands. + public List suggestNumber(final CommandContext context, final CommandInput input) { + final Npc npc = context.getOrDefault("npc", null); + return npc == null || npc.getData().getServerCommands().isEmpty() + ? Collections.emptyList() + : new ArrayList<>() {{ + for (int i = 0; i < npc.getData().getServerCommands().size(); i++) + add(String.valueOf(i + 1)); + }}; + } - return true; + @Suggestions("ServerCommandCMD/commands") // Suggests allowed (non-blocked) commands accessible by the command sender. + public Collection suggestCommand(final CommandContext context, final CommandInput input) { + return Bukkit.getServer().getCommandMap().getKnownCommands().values().stream() + .filter(command -> !command.getName().contains(":") && command.testPermission(context.sender()) && !hasBlockedCommands(command.getName())) + .map(org.bukkit.command.Command::getName) + .toList(); } + + /* UTILITY METHODS */ + + /** + * Returns {@code true} if specified string contains a blocked command, {@code false} otherwise. + */ + private boolean hasBlockedCommands(final @NotNull String string) { + // Getting the list of all blocked commands. + final List blockedCommands = FancyNpcs.getInstance().getFancyNpcConfig().getBlockedCommands(); + // Iterating over all elements of the component. + for (final String blockedCommand : blockedCommands) { + // Transforming the command to a base command with trailed whitespaces and slashes. This also removes namespaced part from the beginning of the command. + final String transformedBaseCommand = blockedCommand.replace('/', ' ').strip().split(" ")[0].replaceAll(".*?:+", ""); + // Comparing click event value with the transformed base command. Returning the result. + if (string.replace('/', ' ').strip().split(" ")[0].replaceAll(".*?:+", "").equalsIgnoreCase(transformedBaseCommand)) + return true; + } + // Returning false as no blocked commands has been found. + return false; + } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/ShowInTabCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/ShowInTabCMD.java index 42fa9da3..7530aa92 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/ShowInTabCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/ShowInTabCMD.java @@ -1,90 +1,40 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; import org.bukkit.command.CommandSender; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.stream.Stream; - -public class ShowInTabCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return Stream.of("true", "false") - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); +public enum ShowInTabCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + @Command("npc show_in_tab [state]") + @Permission("fancynpcs.command.npc.show_in_tab") + public void onCommand( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @Nullable Boolean state + ) { + final boolean finalState = (state == null) ? !npc.getData().isShowInTab() : state; + // Calling the event and updating the state if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SHOW_IN_TAB, finalState, sender).callEvent()) { + npc.getData().setShowInTab(finalState); + npc.removeForAll(); + npc.create(); + npc.spawnForAll(); + translator.translate(finalState ? "npc_show_in_tab_set_true" : "npc_show_in_tab_set_false").replace("npc", npc.getData().getName()).send(sender); + return; } - - return null; + // Otherwise, sending error message to the sender. + translator.translate("command_npc_modification_cancelled").send(sender); } - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - if (npc.getData().getType() != EntityType.PLAYER) { - MessageHelper.error(receiver, lang.get("npc-must-be-player")); - return false; - } - - boolean showInTab; - switch (args[2].toLowerCase()) { - case "true" -> showInTab = true; - case "false" -> showInTab = false; - default -> { - MessageHelper.error(receiver, lang.get("npc-command-showInTab-invalid-argument")); - return false; - } - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SHOW_IN_TAB, showInTab, receiver); - npcModifyEvent.callEvent(); - - if (showInTab == npc.getData().isShowInTab()) { - MessageHelper.warning(receiver, lang.get("npc-command-showInTab-same")); - return false; - } - - if (!npcModifyEvent.isCancelled()) { - npc.getData().setShowInTab(showInTab); - - if (showInTab) { - npc.updateForAll(); - } else { - npc.removeForAll(); - npc.spawnForAll(); - } - - if (showInTab) { - MessageHelper.success(receiver, lang.get("npc-command-showInTab-true")); - } else { - MessageHelper.success(receiver, lang.get("npc-command-showInTab-false")); - } - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - } - - return true; - } } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java index d66370ab..cc21f7c3 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/SkinCMD.java @@ -1,82 +1,152 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; import de.oliver.fancylib.UUIDFetcher; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; import de.oliver.fancynpcs.api.utils.SkinFetcher; -import de.oliver.fancynpcs.commands.Subcommand; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.incendo.cloud.annotations.Argument; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; +import org.incendo.cloud.annotations.suggestion.Suggestions; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.regex.Pattern; -public class SkinCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - return null; - } - - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length != 3 && args.length != 2) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; - String skinName = args.length == 3 ? args[2] : receiver instanceof Player player ? player.getName() : "Steve"; +public enum SkinCMD { + INSTANCE; // SINGLETON + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } + private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{3,16}$"); + @Command("npc skin ") + @Permission("fancynpcs.command.npc.skin") + public void onSkin( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull @Argument(suggestions = "SkinCMD/skin") String skin + ) { + // Sending error message if NPC cannot have skin applied. Only players can have skins. if (npc.getData().getType() != EntityType.PLAYER) { - MessageHelper.error(receiver, lang.get("npc-must-be-player")); - return false; + translator.translate("command_unsupported_npc_type").send(sender); + return; } - - if (SkinFetcher.SkinType.getType(skinName) == SkinFetcher.SkinType.UUID) { - UUID uuid = UUIDFetcher.getUUID(skinName); + // Getting some information about input to handle command accordingly and improve message accuracy. + final boolean isMirror = skin.equalsIgnoreCase("@mirror"); + final boolean isNone = skin.equalsIgnoreCase("@none"); + final boolean isURL = isURL(skin); + if (isMirror) { + // Calling event and updating the skin if not cancelled, sending error message otherwise. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.MIRROR_SKIN, true, sender).callEvent()) { + npc.getData().setMirrorSkin(true); + npc.removeForAll(); + npc.create(); + npc.spawnForAll(); + translator.translate("npc_skin_set_mirror").replace("npc", npc.getData().getName()).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); + } + } else if (isNone) { + // Calling events and updating the skin if not cancelled, sending error message otherwise. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.MIRROR_SKIN, false, sender).callEvent() && new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, null, sender).callEvent()) { + npc.getData().setMirrorSkin(false); + npc.getData().setSkin(null); + npc.removeForAll(); + npc.create(); + npc.spawnForAll(); + translator.translate("npc_skin_set_none").replace("npc", npc.getData().getName()).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); + } + } else if (isURL) { + // Creating SkinFetcher from the specified texture URL. + final SkinFetcher skinFetcher = new SkinFetcher(skin); + // Sending error message if SkinFetcher has failed to load the skin. + if (!skinFetcher.isLoaded()) { + translator.translate("npc_skin_failure_invalid_url").replaceStripped("input", skin).send(sender); + return; + } + // Calling events and updating the skin if not cancelled, sending error message otherwise. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.MIRROR_SKIN, false, sender).callEvent() && new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, skinFetcher, sender).callEvent()) { + npc.getData().setMirrorSkin(false); + npc.getData().setSkin(skinFetcher); + npc.removeForAll(); + npc.create(); + npc.spawnForAll(); + translator.translate("npc_skin_set_url").replace("npc", npc.getData().getName()).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); + } + // Handling as if user input is a player name. Because this may require sending a web request, we should (?) match against valid username pattern to make it somewhat injection-proof. + } else if (USERNAME_PATTERN.matcher(skin).find()) { + // Fetching UUID from the specified player name. + final @Nullable UUID uuid = UUIDFetcher.getUUID(skin); + // Sending error message if message if UUID fetch has (for whatever reason) failed. This can happen eg. when being rate limited. if (uuid == null) { - MessageHelper.error(receiver, lang.get("npc-command-skin-invalid")); - return false; + translator.translate("npc_skin_failure_invalid_name_or_rate_limit").send(sender); + return; + } + // Creating SkinFetcher from the fetched UUID. + final SkinFetcher skinFetcher = new SkinFetcher(uuid.toString()); + // Sending error message if SkinFetcher has failed to load the skin. + if (!skinFetcher.isLoaded()) { + translator.translate("npc_skin_failure_invalid_name_or_rate_limit").send(sender); + return; } - skinName = uuid.toString(); + // Calling events and updating the skin if not cancelled, sending error message otherwise. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.MIRROR_SKIN, false, sender).callEvent() && new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, skinFetcher, sender).callEvent()) { + npc.getData().setMirrorSkin(false); + npc.getData().setSkin(skinFetcher); + npc.removeForAll(); + npc.create(); + npc.spawnForAll(); + translator.translate("npc_skin_set_name").replace("npc", npc.getData().getName()).replace("name", skin).send(sender); + } else { + translator.translate("command_npc_modification_cancelled").send(sender); + } + } else { + translator.translate("npc_skin_failure_invalid_name_or_url").replaceStripped("input", skin).send(sender); } + } - SkinFetcher skinFetcher = new SkinFetcher(skinName); - if (!skinFetcher.isLoaded()) { - MessageHelper.error(receiver, lang.get("npc-command-message-failed_header")); - MessageHelper.error(receiver, lang.get("npc-command-skin-failed_url")); - MessageHelper.error(receiver, lang.get("npc-command-skin-failed_limited")); - return false; - } + /* PARSERS AND SUGGESTIONS */ - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.SKIN, skinFetcher, receiver); - npcModifyEvent.callEvent(); + @Suggestions("SkinCMD/skin") + public List suggestSkin(final CommandContext context, final CommandInput input) { + return new ArrayList<>() {{ + add("@none"); + add("@mirror"); + Bukkit.getOnlinePlayers().stream().map(Player::getName).forEach(this::add); + }}; + } - if (!npcModifyEvent.isCancelled()) { - npc.getData().setSkin(skinFetcher); - npc.getData().setMirrorSkin(false); - npc.removeForAll(); - npc.create(); - npc.spawnForAll(); - MessageHelper.success(receiver, lang.get("npc-command-skin-updated")); - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - } + /* UTILITY METHODS */ - return true; + /** + * Returns {@code true} if provided string can be parsed to an {@link URL} object. + */ + private static boolean isURL(final @NotNull String url) { + try { + new URL(url); + return true; + } catch (final MalformedURLException e) { + return false; + } } + } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/TeleportCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/TeleportCMD.java index 79ac9f70..60598031 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/TeleportCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/TeleportCMD.java @@ -1,104 +1,39 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; -import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; -import org.bukkit.Bukkit; import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.bukkit.util.RayTraceResult; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.text.DecimalFormat; -import java.util.List; - -public class TeleportCMD implements Subcommand { +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - private final DecimalFormat DF = new DecimalFormat(".###"); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - RayTraceResult rayTraceResult = player.rayTraceBlocks(50); +import org.jetbrains.annotations.NotNull; - if (args.length == 3) { - return List.of(String.valueOf(rayTraceResult != null ? - DF.format(rayTraceResult.getHitPosition().getX()) : - DF.format(player.getLocation().getX())).replace(',', '.') - ); - } else if (args.length == 4) { - return List.of(String.valueOf(rayTraceResult != null ? - DF.format(rayTraceResult.getHitPosition().getY()) : - DF.format(player.getLocation().getY())).replace(',', '.') - ); - } else if (args.length == 5) { - return List.of(String.valueOf(rayTraceResult != null ? - DF.format(rayTraceResult.getHitPosition().getZ()) : - DF.format(player.getLocation().getZ())).replace(',', '.') - ); - } else if (args.length == 6) { - return List.of(player.getLocation().getWorld().getName()); +public enum TeleportCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + @Command(value = "npc teleport ", requiredSender = Player.class) + @Permission("fancynpcs.command.npc.teleport") + public void onTeleport( + final @NotNull Player sender, + final @NotNull Npc npc + ) { + final Location location = npc.getData().getLocation(); + // Checking if the world is still loaded. + if (location.getWorld() == null) { + translator.translate("npc_teleport_failure_world_not_loaded").send(sender); + return; } - - return null; + // Teleporting and sending message to the sender. This operation can occasionally fail. + sender.teleportAsync(location).whenComplete((isSuccess, thr) -> { + translator.translate(isSuccess ? "npc_teleport_success" : "npc_teleport_failure_exception").replace("npc", npc.getData().getName()).send(sender); + // Printing stacktrace to the console in case an exception occurred. + if (thr != null) + thr.printStackTrace(); + }); } - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - if (args.length < 5) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - double x, y, z; - - try { - x = Double.parseDouble(args[2]); - y = Double.parseDouble(args[3]); - z = Double.parseDouble(args[4]); - } catch (NumberFormatException e) { - MessageHelper.error(receiver, "wrong-usage"); - MessageHelper.error(receiver, "could-not-parse-number"); - return false; - } - - World world = null; - - if (args.length == 6) { - world = Bukkit.getWorld(args[5]); - } else if (receiver instanceof Player p) { - world = p.getWorld(); - } - - if (world == null) { - MessageHelper.error(receiver, "world-not-found"); - return false; - } - - Location location = new Location(world, x, y, z); - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.LOCATION, location, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().setLocation(location); - npc.updateForAll(); - MessageHelper.success(receiver, lang.get("npc-command-teleport-success")); - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - } - - return true; - } } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/TurnToPlayerCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/TurnToPlayerCMD.java index add66e57..4d939494 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/TurnToPlayerCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/TurnToPlayerCMD.java @@ -1,71 +1,36 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.stream.Stream; - -public class TurnToPlayerCMD implements Subcommand { - - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return Stream.of("true", "false") - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); +public enum TurnToPlayerCMD { + INSTANCE; // SINGLETON + + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + + @Command("npc turn_to_player [state]") + @Permission("fancynpcs.command.npc.turn_to_player") + public void onTurnToPlayer( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @Nullable Boolean state + ) { + final boolean finalState = (state == null) ? !npc.getData().isTurnToPlayer() : state; + // Calling the event and updating the state if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.TURN_TO_PLAYER, finalState, sender).callEvent()) { + npc.getData().setTurnToPlayer(finalState); + translator.translate(finalState ? "npc_turn_to_player_set_true" : "npc_turn_to_player_set_false").replace("npc", npc.getData().getName()).send(sender); + return; } - - return null; + translator.translate("command_npc_modification_cancelled").send(sender); } - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - boolean turnToPlayer; - try { - turnToPlayer = Boolean.parseBoolean(args[2]); - } catch (Exception e) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.TURN_TO_PLAYER, turnToPlayer, receiver); - npcModifyEvent.callEvent(); - - if (!npcModifyEvent.isCancelled()) { - npc.getData().setTurnToPlayer(turnToPlayer); - - if (turnToPlayer) { - MessageHelper.success(receiver, lang.get("npc-command-turnToPlayer-true")); - } else { - MessageHelper.success(receiver, lang.get("npc-command-turnToPlayer-false")); - npc.updateForAll(); // move to default pos - } - } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); - } - - return true; - } } diff --git a/src/main/java/de/oliver/fancynpcs/commands/npc/TypeCMD.java b/src/main/java/de/oliver/fancynpcs/commands/npc/TypeCMD.java index 39a34d89..5e184d15 100644 --- a/src/main/java/de/oliver/fancynpcs/commands/npc/TypeCMD.java +++ b/src/main/java/de/oliver/fancynpcs/commands/npc/TypeCMD.java @@ -1,77 +1,43 @@ package de.oliver.fancynpcs.commands.npc; -import de.oliver.fancylib.LanguageConfig; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcModifyEvent; -import de.oliver.fancynpcs.commands.Subcommand; import org.bukkit.command.CommandSender; import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Arrays; -import java.util.List; - -public class TypeCMD implements Subcommand { +import org.incendo.cloud.annotations.Command; +import org.incendo.cloud.annotations.Permission; - private final LanguageConfig lang = FancyNpcs.getInstance().getLanguageConfig(); - - @Override - public List tabcompletion(@NotNull Player player, @Nullable Npc npc, @NotNull String[] args) { - if (args.length == 3) { - return Arrays.stream(EntityType.values()) - .map(Enum::name) - .filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase())) - .toList(); - } - - return null; - } - - @Override - public boolean run(@NotNull CommandSender receiver, @Nullable Npc npc, @NotNull String[] args) { - if (args.length < 3) { - MessageHelper.error(receiver, lang.get("wrong-usage")); - return false; - } - - if (npc == null) { - MessageHelper.error(receiver, lang.get("npc-not-found")); - return false; - } - - EntityType type = EntityType.fromName(args[2].toLowerCase()); +import org.jetbrains.annotations.NotNull; - if (type == null) { - MessageHelper.error(receiver, lang.get("npc-command-type-invalid")); - return false; - } +public enum TypeCMD { + INSTANCE; // SINGLETON - NpcModifyEvent npcModifyEvent = new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.TYPE, type, receiver); - npcModifyEvent.callEvent(); + private final Translator translator = FancyNpcs.getInstance().getTranslator(); - if (!npcModifyEvent.isCancelled()) { + @Command("npc type ") + @Permission("fancynpcs.command.npc.type") + public void onType( + final @NotNull CommandSender sender, + final @NotNull Npc npc, + final @NotNull EntityType type + ) { + // Calling the event and updating the type if not cancelled. + if (new NpcModifyEvent(npc, NpcModifyEvent.NpcModification.TYPE, type, sender).callEvent()) { npc.getData().setType(type); - - if (type != EntityType.PLAYER) { - npc.getData().setGlowing(false); + // Removing NPC from the player-list if new type is not EntityType.PLAYER. + if (type != EntityType.PLAYER) npc.getData().setShowInTab(false); - if (npc.getData().getEquipment() != null) { - npc.getData().getEquipment().clear(); - } - } - + // Clearing equipment if new type is not a living entity or equipment list is empty. + if (!type.isAlive() && npc.getData().getEquipment() != null) + npc.getData().getEquipment().clear(); npc.removeForAll(); npc.create(); npc.spawnForAll(); - MessageHelper.success(receiver, lang.get("npc-command-type-updated")); + translator.translate("npc_type_success").replace("npc", npc.getData().getName()).replace("type", type.name().toLowerCase()).send(sender); } else { - MessageHelper.error(receiver, lang.get("npc-command-modification-cancelled")); + translator.translate("command_npc_modification_cancelled").send(sender); } - - return true; } } diff --git a/src/main/java/de/oliver/fancynpcs/listeners/PlayerNpcsListener.java b/src/main/java/de/oliver/fancynpcs/listeners/PlayerNpcsListener.java index a0c174ef..c5ee1fef 100644 --- a/src/main/java/de/oliver/fancynpcs/listeners/PlayerNpcsListener.java +++ b/src/main/java/de/oliver/fancynpcs/listeners/PlayerNpcsListener.java @@ -3,7 +3,7 @@ import com.plotsquared.core.PlotSquared; import com.plotsquared.core.player.PlotPlayer; import com.plotsquared.core.plot.Plot; -import de.oliver.fancylib.MessageHelper; +import de.oliver.fancylib.translations.Translator; import de.oliver.fancynpcs.FancyNpcs; import de.oliver.fancynpcs.api.Npc; import de.oliver.fancynpcs.api.events.NpcCreateEvent; @@ -18,6 +18,8 @@ public class PlayerNpcsListener implements Listener { + private final Translator translator = FancyNpcs.getInstance().getTranslator(); + private static final boolean isUsingPlotSquared = FancyNpcs.getInstance().isUsingPlotSquared(); @EventHandler @@ -30,7 +32,7 @@ public void onNpcCreate(NpcCreateEvent event) { PlotPlayer plotPlayer = PlotSquared.platform().playerManager().getPlayer(player.getUniqueId()); Plot currentPlot = plotPlayer.getCurrentPlot(); if ((currentPlot == null || !currentPlot.isOwner(player.getUniqueId())) && !player.hasPermission("fancynpcs.admin")) { - MessageHelper.error(player, "You are only allowed to create npcs on your plot"); + translator.translate("player_npcs_create_failure_not_owned_plot").send(player); event.setCancelled(true); return; } @@ -48,7 +50,7 @@ public void onNpcCreate(NpcCreateEvent event) { npcAmount++; } if (npcAmount >= maxNpcs && !player.hasPermission("fancynpcs.admin")) { - MessageHelper.error(player, "You have reached the maximum amount of npcs"); + translator.translate("player_npcs_create_failure_limit_reached").send(player); event.setCancelled(true); return; } @@ -61,7 +63,7 @@ public void onNpcRemove(NpcRemoveEvent event) { } if (!event.getNpc().getData().getCreator().equals(player.getUniqueId()) && !player.hasPermission("fancynpcs.admin")) { - MessageHelper.error(player, "You can only modify your npcs"); + translator.translate("player_npcs_cannot_modify_npc").send(player); event.setCancelled(true); return; } @@ -74,7 +76,7 @@ public void onNpcModify(NpcModifyEvent event) { } if (!event.getNpc().getData().getCreator().equals(player.getUniqueId()) && !player.hasPermission("fancynpcs.admin")) { - MessageHelper.error(player, "You can only modify your npcs"); + translator.translate("player_npcs_cannot_modify_npc").send(player); event.setCancelled(true); return; } @@ -83,7 +85,7 @@ public void onNpcModify(NpcModifyEvent event) { Plot currentPlot = plotPlayer.getCurrentPlot(); if ((currentPlot == null || !currentPlot.isOwner(player.getUniqueId())) && !player.hasPermission("fancynpcs.admin")) { - MessageHelper.error(player, "You are only allowed to teleport npcs on your plot"); + translator.translate("player_npcs_cannot_move_npc").send(player); event.setCancelled(true); } } diff --git a/src/main/java/de/oliver/fancynpcs/utils/GlowingColor.java b/src/main/java/de/oliver/fancynpcs/utils/GlowingColor.java new file mode 100644 index 00000000..d5e1e564 --- /dev/null +++ b/src/main/java/de/oliver/fancynpcs/utils/GlowingColor.java @@ -0,0 +1,52 @@ +package de.oliver.fancynpcs.utils; + +import net.kyori.adventure.text.format.NamedTextColor; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +// Used 'info' and 'glowing' sub-commands. +public enum GlowingColor { + DISABLED(null, ""), + BLACK(NamedTextColor.BLACK, "color_black"), + DARK_BLUE(NamedTextColor.DARK_BLUE, "color_dark_blue"), + DARK_GREEN(NamedTextColor.DARK_GREEN, "color_dark_green"), + DARK_AQUA(NamedTextColor.DARK_AQUA, "color_dark_aqua"), + DARK_RED(NamedTextColor.DARK_RED, "color_dark_red"), + DARK_PURPLE(NamedTextColor.DARK_PURPLE, "color_dark_purple"), + GOLD(NamedTextColor.GOLD, "color_gold"), + GRAY(NamedTextColor.GRAY, "color_gray"), + DARK_GRAY(NamedTextColor.DARK_GRAY, "color_dark_gray"), + BLUE(NamedTextColor.BLUE, "color_blue"), + GREEN(NamedTextColor.GREEN, "color_green"), + AQUA(NamedTextColor.AQUA, "color_aqua"), + RED(NamedTextColor.RED, "color_red"), + LIGHT_PURPLE(NamedTextColor.LIGHT_PURPLE, "color_light_purple"), + YELLOW(NamedTextColor.YELLOW, "color_yellow"), + WHITE(NamedTextColor.WHITE, "color_white"); + + private final @Nullable NamedTextColor color; + private final @NotNull String translationKey; + + GlowingColor(final @Nullable NamedTextColor color, final @NotNull String translationKey) { + this.color = color; + this.translationKey = translationKey; + } + + public @Nullable NamedTextColor getColor() { + return color; + } + + public @NotNull String getTranslationKey() { + return translationKey; + } + + public static @NotNull GlowingColor fromAdventure(final @NotNull NamedTextColor color) { + for (final GlowingColor glowingColor : GlowingColor.values()) + if (glowingColor.color != null && glowingColor.color.value() == color.value()) + return glowingColor; + // Throwing exception if specified color is not mapped. + throw new IllegalArgumentException("UNSUPPORTED COLOR"); + } + +} diff --git a/src/main/resources/lang.yml b/src/main/resources/lang.yml deleted file mode 100644 index a92ec4c1..00000000 --- a/src/main/resources/lang.yml +++ /dev/null @@ -1,91 +0,0 @@ -messages: - reloaded-config: Reloaded the config - saved-npcs: Saved all NPCs - fancynpcs-syntax: /FancyNpcs - illegal-command: This command is not allowed to use in playerCommand, serverCommand and message - only-players: Only players can execute this command - no-permission-subcommand: You don't have permission for this subcommand - wrong-usage: 'Wrong usage: /npc help' - could-not-parse-number: Could not parse number - world-not-found: Could not find world - npc-not-found: Could not find NPC - npc-must-be-player: Npc's type must be Player to do this - on-interaction-cooldown: You need to wait {time} seconds until you can interact with this npc again - npc-command-help-header: 'FancyNpcs Plugin help:' - npc-command-help-create: ' - /npc create (name) - Creates a new npc at your location' - npc-command-help-remove: ' - /npc remove (name) - Removes an npc' - npc-command-help-copy: ' - /npc copy (name) (new name) - Copies an npc' - npc-command-help-list: ' - /npc list - Summary of all npcs' - npc-command-help-skin: ' - /npc skin (name) [(skin)] - Sets the skin for an npc' - npc-command-help-type: ' - /npc type (name) (type) - Sets the entity type for an npc' - npc-command-help-moveHere: ' - /npc movehere (name) - Teleports an npc to your location' - npc-command-help-teleport: ' - /npc teleport (name) (x) (y) (z) [world] - Teleports the npc to the provided location' - npc-command-help-displayName: ' - /npc displayName (name) (displayName ...) - Sets the displayname for an npc' - npc-command-help-equipment: ' - /npc equipment (name) (slot) - Equips the npc with the item you are holding' - npc-command-help-message: ' - /npc message (name) (none|message) - Set NPC message' - npc-command-help-playerCommand: ' - /npc playerCommand (name) (none|command ...) - Executes the command on a player when interacting' - npc-command-help-serverCommand: ' - /npc serverCommand (name) (none|command ...) - The command will be executed by the console when someone interacts with the npc' - npc-command-help-showInTab: ' - /npc showInTab (name) (true|false) - Whether the NPC will be shown in tab-list or not' - npc-command-help-glowing: ' - /npc glowing (name) (true|false) - Whether the NPC will glow or not' - npc-command-help-glowingColor: ' - /npc glowingColor (name) (color) - The color of the glowing effect' - npc-command-help-collidable: ' - /npc collidable (name) (true|false) - Whether the NPC will be collidable or not' - npc-command-help-turnToPlayer: ' - /npc turnToPlayer (name) (true|false) - Whether the NPC will turn to you or not' - npc-command-help-attribute: ' - /npc attribute (name) (attribute) (value) - Set certain npc attributes' - npc-command-help-interactionCooldown: ' - /npc interactionCooldown (name) (seconds) - Set the interaction cooldown (0 = disabled)' - npc_command-help-mirrorSkin: ' - /npc mirrorSkin (name) (true|false) - Whether the NPC will mirror the skin of the viewing player' - npc-command-list-header: All NPCs: - npc-command-list-no-npcs: There are no NPCs. Use '/npc create' to create one - npc-command-list-tp-hover: Click to teleport'> - {name} ({x}/{y}/{z}) - npc-command-create-name-already-exists: An npc with that name already exists - npc-command-create-created: Created new NPC - npc-command-create-cancelled: Creation has been cancelled - npc-command-collidable-true: NPC will now be collidable - npc-command-collidable-false: NPC will no longer be collidable - npc-command-copy-success: Copied NPC - npc-command-copy-cancelled: Copying has been cancelled - npc-command-remove-removed: Removed NPC - npc-command-remove-cancelled: Removing has been cancelled - npc-command-moveHere-moved: Moved NPC to your location - npc-command-message-updated: Updated message - npc-command-message-removed: Removed message - npc-command-message-invalid: Invalid message - npc-command-message-invalid-index: Invalid index - npc-command-message-cancelled: Updating message has been cancelled - npc-command-message-sendMessagesRandomly-true: NPC will now send messages randomly - npc-command-message-sendMessagesRandomly-false: NPC will no longer send messages randomly - npc-command-skin-invalid: Invalid username or url - npc-command-skin-failed_header: 'Could not load skin. Possible causes:' - npc-command-skin-failed_url: ' - Invalid URL (check the url)' - npc-command-skin-failed_limited: ' - Rate limit reached (try again later)' - npc-command-skin-updated: Updated skin - npc-command-displayName-updated: Updated display name - npc-command-equipment-invalid-slot: Invalid equipment slot - npc-command-equipment-updated: Updated equipment - npc-command-serverCommand-updated: Updated server command to be executed - npc-command-playercommand-updated: Updated playerCommand - npc-command-playercommand-removed: Removed playerCommand - npc-command-playercommand-invalid: Invalid playerCommand - npc-command-playercommand-invalid-index: Invalid index - npc-command-playercommand-cancelled: Updating playerCommands has been cancelled - npc-command-showInTab-invalid-argument: 'Invalid argument (expected: ''true'' or ''false'')' - npc-command-showInTab-same: Nothing has changed - npc-command-showInTab-true: NPC will now be shown in tab - npc-command-showInTab-false: NPC will no longer be shown in tab - npc-command-glowing-true: NPC will now glow - npc-command-glowing-false: NPC will no longer glow - npc-command-glowingColor-invalid: Invalid color - npc-command-glowingColor-updated: Updated glowing color - npc-command-turnToPlayer-true: NPC will now turn to the players - npc-command-turnToPlayer-false: NPC will no longer turn to the players - npc-command-type-invalid: Invalid type - npc-command-type-updated: Updated entity type - npc-command-attribute-attribute-not-found: Could not find attribute - npc-command-attribute-wrong-entity-type: This attribute can not be applied to this entity type - npc-command-attribute-invalid-value: Invalid attribute value - npc-command-attribute-success: Successfully set the attribute - npc-command-interactioncooldown-updated: Successfully updated the interaction cooldown - npc-command-teleport-success: Successfully teleported npc - npc-command-mirrorSkin-true: NPC will now mirror the skin of the viewer - npc-command-mirrorSkin-false: NPC will no longer mirror the skin of the viewer - npc-command-mirrorSkin-cancelled: Modification has been cancelled - npc-command-fix-success: Successfully tried to fixed the npc diff --git a/src/main/resources/languages/default.yml b/src/main/resources/languages/default.yml new file mode 100644 index 00000000..91f34c42 --- /dev/null +++ b/src/main/resources/languages/default.yml @@ -0,0 +1,334 @@ +# ======================================================================== +# THIS FILE IS A TEMPLATE, ANY MODIFICATIONS MADE HERE MAY NOT BE APPLIED. +# ======================================================================== +# HOW TO CREATE CUSTOM LANGUAGE: +# 1. Copy this file and name it eg. "en.yml" +# 2. Change "language_name" property to eg. "english" and update "language" property inside config.yml to match this value. +# 3. Modify contents to your liking. +# 4. Reload the plugin using "/fancynpcs reload" command. +# ======================================================================== + +# Language name. This value can be used to specify "language" property in the config.yml file. +language_name: default + +# Messages support MiniMessage formatting: https://docs.advntr.dev/minimessage/format.html +messages: + # Common (States) + true: "{successColor}True" + enabled: "{successColor}Enabled" + false: "{errorColor}False" + disabled: "{errorColor}Disabled" + + # Common (Equipment Slots) + main_hand: "Main Hand" + off_hand: "Off Hand" + head: "Head" + chest: "Chest" + legs: "Legs" + feet: "Feet" + + # Common (Colors) + color_black: "Black" + color_dark_blue: "Dark Blue" + color_dark_green: "Dark Green" + color_dark_aqua: "Dark Aqua" + color_dark_red: "Dark Red" + color_dark_purple: "Dark Purple" + color_gold: "Gold" + color_gray: "Gray" + color_dark_gray: "Dark Gray" + color_blue: "Blue" + color_green: "Green" + color_aqua: "Aqua" + color_red: "Red" + color_light_purple: "Light Purple" + color_yellow: "Yellow" + color_white: "White" + + # Common (Other) + interaction_on_cooldown: "› {errorColor}You're currently on cooldown. {warningColor}{time}{errorColor} remaining." + player_npcs_cannot_modify_npc: "› {errorColor}You can only modify NPCs you own." + player_npcs_cannot_move_npc: "› {errorColor}You are only allowed to teleport NPCs to your plot." + player_npcs_create_failure_limit_reached: "› {errorColor}You have reached maximum number of NPCs." + player_npcs_create_failure_not_owned_plot: "› {errorColor}You can only create NPCs on your plot." + + # Commands (Common Replies) + command_missing_permissions: "› {errorColor}Insufficient permissions. You cannot use this command." + command_wrong_usage: "› {errorColor}This sub-command does not exist. Use {warningColor}/npc help{errorColor} to view available commands." + command_incomplete_usage: "› {errorColor}Incomplete command. Use {warningColor}/npc help{errorColor} to view correct syntax." + command_player_only: "› {errorColor}This command can only be executed by in-game players." + command_invalid_boolean: "› {errorColor}Argument {warningColor}{input}{errorColor} must be either {warningColor}true{errorColor} or {warningColor}false{errorColor}." + command_invalid_number: "› {errorColor}Argument {warningColor}{input}{errorColor} is not a valid number." + command_invalid_location: "› {errorColor}Argument {warningColor}{input}{errorColor} is not a valid location." + command_invalid_world: "› {errorColor}World named {warningColor}{input}{errorColor} does not exist or is not loaded." + command_invalid_glowing_color: "› {errorColor}Argument named {warningColor}{input}{errorColor} is not a valid glowing color." + command_invalid_list_sort_type: "› {errorColor}Argument named {warningColor}{input}{errorColor} is not a valid sort type." + command_invalid_nearby_sort_type: "› {errorColor}Argument named {warningColor}{input}{errorColor} is not a valid sort type." + command_invalid_entity_type: "› {errorColor}Argument named {warningColor}{input}{errorColor} is not a valid entity type." + command_invalid_npc: "› {errorColor}NPC {warningColor}{input}{errorColor} does not exist." + command_invalid_material: "› {errorColor}Argument {warningColor}{input}{errorColor} is not a valid material." + command_invalid_attribute: "› {errorColor}Specified value {warningColor}{input}{errorColor} is not a valid attribute." + command_invalid_attribute_value: "› {errorColor}Specified value {warningColor}{input}{errorColor} is not valid for this attribute." + command_invalid_equipment_slot: "› {errorColor}Argument {warningColor}{input}{errorColor} is not a valid slot." + command_invalid_interval: "› {errorColor}Argument {warningColor}{input}{errorColor} is not a valid duration of time." + command_invalid_enum_generic: "› {errorColor}Argument {warningColor}{input}{errorColor} is not a valid {enum}." + command_unsupported_npc_type: "› {errorColor}This NPC type does not support this feature." + command_input_contains_blocked_command: "› {errorColor}This command is not allowed for use in interactions." + command_npc_modification_cancelled: "› {errorColor}NPC modification has been cancelled by the API." + + # Generic Syntax Message (syntax is provided by Cloud and may not be human-friendly) + command_invalid_syntax_generic: "Syntax: {warningColor}/{syntax}" + + # Command Syntaxes + command_syntax: + fancynpcs: "Syntax: {primaryColor}/fancynpcs {secondaryColor}(version | reload | save | feature_flags)" + npc: "› {errorColor}Unknown command. Use {warningColor}/npc help{errorColor} to view available commands." + npc_attribute: "Syntax: {primaryColor}/npc attribute {secondaryColor}(npc) {primaryColor}(set | list)" + npc_attribute_set: "Syntax: {primaryColor}/npc attribute {secondaryColor}(npc) {primaryColor}set {secondaryColor}(attribute) (value)" + npc_collidable: "Syntax: {primaryColor}/npc collidable {secondaryColor}(npc) (state)" + npc_copy: "Syntax: {primaryColor}/npc copy {secondaryColor}(npc) (new_name)" + npc_create: "Syntax: {primaryColor}/npc create {secondaryColor}(npc) [--type] [--position] [--world]" + npc_displayname: "Syntax: {primaryColor}/npc displayname {secondaryColor}(npc) (name)" + npc_equipment: "Syntax: {primaryColor}/npc equipment {secondaryColor}(npc) {primaryColor}(set | clear | list)" + npc_equipment_set: "Syntax: {primaryColor}/npc equipment {secondaryColor}(npc) {primaryColor}set {secondaryColor}(slot) (@hand | @none | item)" + npc_glowing: "Syntax: {primaryColor}/npc glowing {secondaryColor}(npc) (disabled | color)" + npc_info: "Syntax: {primaryColor}/npc info {secondaryColor}(npc)" + npc_interaction_cooldown: "Syntax: {primaryColor}/npc interaction_cooldown {secondaryColor}(npc) (disabled | time)" + npc_list: "Syntax: {primaryColor}/npc list {secondaryColor}[filters...]" + npc_message: "Syntax: {primaryColor}/npc message {secondaryColor}(npc) {primaryColor}(add | set | remove | clear | list | send_randomly)" + npc_message_add: "Syntax: {primaryColor}/npc message {secondaryColor}(npc) {primaryColor}add {secondaryColor}(@none | message)" + npc_message_remove: "Syntax: {primaryColor}/npc message {secondaryColor}(npc) {primaryColor}remove {secondaryColor}(number)" + npc_message_set: "Syntax: {primaryColor}/npc message {secondaryColor}(npc) {primaryColor}set {secondaryColor}(number) (@none | message)" + npc_message_send_randomly: "Syntax: {primaryColor}/npc message {secondaryColor}(npc) {primaryColor}send_randomly {secondaryColor}[state]" + npc_move_here: "Syntax: {primaryColor}/npc move_here {secondaryColor}(npc)" + npc_move_to: "Syntax: {primaryColor}/npc move_to {secondaryColor}(npc) (x) (y) (z) [world]" + npc_nearby: "Syntax: {primaryColor}/npc nearby {secondaryColor}[filters...]" + npc_player_command: "Syntax: {primaryColor}/npc player_command {secondaryColor}(npc) {primaryColor}(add | set | remove | clear | list)" + npc_player_command_add: "Syntax: {primaryColor}/npc player_command {secondaryColor}(npc) {primaryColor}add {secondaryColor}(command)" + npc_player_command_remove: "Syntax: {primaryColor}/npc player_command {secondaryColor}(npc) {primaryColor}remove {secondaryColor}(number)" + npc_player_command_set: "Syntax: {primaryColor}/npc player_command {secondaryColor}(npc) {primaryColor}set {secondaryColor}(number) (command)" + npc_server_command: "Syntax: {primaryColor}/npc server_command {secondaryColor}(npc) {primaryColor}(add | set | remove | clear | list)" + npc_server_command_add: "Syntax: {primaryColor}/npc server_command {secondaryColor}(npc) {primaryColor}add {secondaryColor}(command)" + npc_server_command_remove: "Syntax: {primaryColor}/npc server_command {secondaryColor}(npc) {primaryColor}remove {secondaryColor}(number)" + npc_server_command_set: "Syntax: {primaryColor}/npc server_command {secondaryColor}(npc) {primaryColor}set {secondaryColor}(number) (command)" + npc_remove: "Syntax: {primaryColor}/npc remove {secondaryColor}(npc)" + npc_show_in_tab: "Syntax: {primaryColor}/npc show_in_tab {secondaryColor}(npc) (state)" + npc_skin: "Syntax: {primaryColor}/npc skin {secondaryColor}(npc) (@none | @mirror | name | url)" + npc_teleport: "Syntax: {primaryColor}/npc teleport {secondaryColor}(npc)" + npc_turn_to_player: "Syntax: {primaryColor}/npc turn_to_player {secondaryColor}(npc) (state)" + npc_type: "Syntax: {primaryColor}/npc type {secondaryColor}(npc) (type)" + + # Commands (fancynpcs) + fancynpcs_reload_success: "› {successColor}Plugin has been reloaded." + fancynpcs_save_success: "› {successColor}NPCs have been saved." + fancynpcs_feature_flags_header: "------------------- Feature Flags ------------------" + fancynpcs_feature_flags_entry: " <#848484>{number}. {warningColor}{name} <#848484>({id}): {state}" + fancynpcs_feature_flags_footer: "------------- Showing total of {warningColor}{total_formatted} entries -------------" + + # Commands (npc help) + npc_help_page_header: "------------- {primaryColor}FancyNpcs Commands ({primaryColor}{page}/{primaryColor}{max_page}) --------------" + npc_help_page_footer: "----------- Click {primaryColor}here to open documentation -----------" + npc_help_contents: + - "Sets an attribute of the NPC.'>{primaryColor}/npc attribute {secondaryColor}(npc) {primaryColor}set {secondaryColor}(attribute) (value)" + - "Lists all modified attributes of the NPC.'>{primaryColor}/npc attribute {secondaryColor}(npc) {primaryColor}list" + - "Changes whether the NPC can collide with other entities.'>{primaryColor}/npc collidable {secondaryColor}(npc) [state]" + - "Copies (duplicates) specified NPC.'>{primaryColor}/npc copy {secondaryColor}(npc) (new_name)" + - "Creates a new NPC. Can be customized with flags.'>{primaryColor}/npc create {secondaryColor}(npc) [--type] [--location] [--world]" + - "Changes displayname of the NPC. Supports MiniMessage, PlaceholderAPI and MiniPlaceholders.'>{primaryColor}/npc displayname {secondaryColor}(npc) (name)" + - "Sets equipment slot of the NPC to item currently held in main hand, none or a specific item type.'>{primaryColor}/npc equipment {secondaryColor}(npc) {primaryColor}set {secondaryColor}(slot) (@hand | @none | item)" + - "Clears all equipment slots of the NPC.'>{primaryColor}/npc equipment {secondaryColor}(npc) {primaryColor}clear" + - "Lists all equipment of the NPC.'>{primaryColor}/npc equipment {secondaryColor}(npc) {primaryColor}list" + - "Changes glowing state and color of the NPC.'>{primaryColor}/npc glowing {secondaryColor}(npc) (disabled | color)" + - "Shows information about specified NPC.'>{primaryColor}/npc info {secondaryColor}(npc)" + - "Changes duration between interactions (cooldown) of the NPC.'>{primaryColor}/npc interaction_cooldown {secondaryColor}(npc) (disabled | time)" + - "Lists all NPCs in all worlds. Can be filtered and sorted.'>{primaryColor}/npc list {secondaryColor}[--type] [--sort]" + - "Adds a new message to the list. Supports MiniMessage, PlaceholderAPI and MiniPlaceholders.Messages are shown to the player upon interaction with the NPC.'>{primaryColor}/npc message {secondaryColor}(npc) {primaryColor}add {secondaryColor}(@none | message)" + - "Removes message at a specified index.Messages are shown to the player upon interaction with the NPC.'>{primaryColor}/npc message {secondaryColor}(npc) {primaryColor}remove {secondaryColor}(number)" + - "Changes message at a specified index. Supports MiniMessage, PlaceholderAPI and MiniPlaceholders.Messages are shown to the player upon interaction with the NPC.'>{primaryColor}/npc message {secondaryColor}(npc) {primaryColor}set {secondaryColor}(number) (@none | message)" + - "Clears all attached messages.Messages are shown to the player upon interaction with the NPC.'>{primaryColor}/npc message {secondaryColor}(npc) {primaryColor}clear" + - "Lists all attached messages.Messages are shown to the player upon interaction with the NPC.'>{primaryColor}/npc message {secondaryColor}(npc) {primaryColor}list" + - "Changes whether messages in the list should be sent randomly.Messages are shown to the player upon interaction with the NPC.'>{primaryColor}/npc message {secondaryColor}(npc) {primaryColor}send_randomly {secondaryColor}[state]" + - "Teleports specified NPC to your location.'>{primaryColor}/npc move_here {secondaryColor}(npc)" + - "Teleports NPC to specified location.'>{primaryColor}/npc move_to {secondaryColor}(npc) (x) (y) (z) [world]" + - "Lists all NPCs in your world. Can be filtered and sorted.'>{primaryColor}/npc nearby {secondaryColor}[--radius] [--type] [--sort]" + - "Adds a new command to the list. Supports PlaceholderAPI and MiniPlaceholders.Commands are executed by the player upon interaction with the NPC.'>{primaryColor}/npc player_command {secondaryColor}(npc) {primaryColor}add {secondaryColor}(command)" + - "Removes command at specified index.Commands are executed by the player upon interaction with the NPC.'>{primaryColor}/npc player_command {secondaryColor}(npc) {primaryColor}remove {secondaryColor}(number)" + - "Changes command at specified index. Supports PlaceholderAPI and MiniPlaceholders.Commands are executed by the player upon interaction with the NPC.'>{primaryColor}/npc player_command {secondaryColor}(npc) {primaryColor}set {secondaryColor}(number) (command)" + - "Clears all attached player commands.Commands are executed by the player upon interaction with the NPC.'>{primaryColor}/npc player_command {secondaryColor}(npc) {primaryColor}clear" + - "Lists all attached player commands.Commands are executed by the player upon interaction with the NPC.'>{primaryColor}/npc player_command {secondaryColor}(npc) {primaryColor}list" + - "Removes (deletes) specified NPC.'>{primaryColor}/npc remove {secondaryColor}(npc)" + - "Adds a new command to the list.Commands are executed by the console upon interaction with the NPC.'>{primaryColor}/npc server_command {secondaryColor}(npc) {primaryColor}add {secondaryColor}(command)" + - "Removes command at specified index.Commands are executed by the console upon interaction with the NPC.'>{primaryColor}/npc server_command {secondaryColor}(npc) {primaryColor}remove {secondaryColor}(number)" + - "Changes command at specified index.Commands are executed by the console upon interaction with the NPC.'>{primaryColor}/npc server_command {secondaryColor}(npc) {primaryColor}set {secondaryColor}(number) (command)" + - "Clears all attached server commands.Commands are executed by the console upon interaction with the NPC.'>{primaryColor}/npc server_command {secondaryColor}(npc) {primaryColor}clear" + - "Lists all attached server commands.Commands are executed by the console upon interaction with the NPC.'>{primaryColor}/npc server_command {secondaryColor}(npc) {primaryColor}list" + - "Changes whether the NPC is shown in the player-list. This works only on NPCs of PLAYER type.{errorColor}Re-connecting to the server might be required for changes to take effect.'>{primaryColor}/npc show_in_tab {secondaryColor}(npc) (state)" + - "Changes skin of the NPC.{warningColor}@none - removes the skin{warningColor}@mirror - mirrors player skin{warningColor}(name) - name of any player{warningColor}(url) - url of the skin texture'>{primaryColor}/npc skin {secondaryColor}(npc) (@none | @mirror | name | url)" + - "Teleports you to the specified NPC.'>{primaryColor}/npc teleport {secondaryColor}(npc)" + - "Changes whether the NPC should turn to the player when in range.'>{primaryColor}/npc turn_to_player {secondaryColor}(npc) (state)" + - "Changes the type of the NPC.'>{primaryColor}/npc type {secondaryColor}(npc) (type)" + + # Commands (npc attribute) + npc_attribute_set: "Attribute {warningColor}{attribute} has been set to {warningColor}{value}." + npc_attribute_set_invalid_for_this_entity_type: "› {errorColor}Attribute {warningColor}{input}{errorColor} is not valid attribute for this entity type." + npc_attribute_list_header: "-------------------- Attributes --------------------" + npc_attribute_list_entry: " › <#848484>{attribute}: {warningColor}{value}" + npc_attribute_list_footer: "---------------------------------------------------" + npc_attribute_list_failure_empty: "› {errorColor}There is no attributes set. Use {warningColor}/npc attribute (npc) set (attribute) (value){errorColor} to set an attribute." + + # Commands (npc collidable) + npc_collidable_set_true: "NPC {warningColor}{npc} is now collidable." + npc_collidable_set_false: "NPC {warningColor}{npc} is no longer collidable." + + # Commands (npc copy) + npc_copy_success: "NPC {warningColor}{npc} has been copied to {warningColor}{new_npc}." + + # Commands (npc create) + npc_create_success: "NPC {warningColor}{npc} has been created." + npc_create_failure_invalid_name: "› {errorColor}Name contains illegal characters. Only [{warningColor}A-Z{errorColor}, {warningColor}a-z{errorColor}, {warningColor}0-9{errorColor}, {warningColor}_{errorColor}, {warningColor}-{errorColor}, {warningColor}/{errorColor}] characters are allowed." + npc_create_failure_already_exists: "› {errorColor}NPC {warningColor}{npc}{errorColor} already exists." + npc_create_failure_must_specify_world: "› {errorColor}You must specify {warningColor}--world{errorColor} flag when running this command from the console." + + # Commands (npc displayname) + npc_displayname_set_name: "NPC {warningColor}{npc} is now using {name} as their display name." + npc_displayname_set_empty: "NPC {warningColor}{npc} is no longer showing display name." + + # Commands (npc equipment) + npc_equipment_set_item: "Equipment slot {warningColor}{slot} has been set to ." + npc_equipment_set_empty: "Equipment slot {warningColor}{slot} has been removed." + npc_equipment_set_failure_invalid_slot: "› {errorColor}Argument {warningColor}{input}{errorColor} is not a valid equipment slot." + npc_equipment_clear_success: "Equipment has been cleared." + npc_equipment_list_header: "-------------------- Equipment --------------------" + npc_equipment_list_entry: " <#848484>{slot}: " + npc_equipment_list_footer: "---------------------------------------------------" + npc_equipment_list_failure_empty: "› {errorColor}There is no equipment slots set. Use {warningColor}/npc equipment (npc) set (slot) (@hand | @none | material){errorColor} to set an equipment slot." + + # Commands (npc fix) + npc_fix_success: "Attempted to fix NPC {warningColor}{npc}... Still having issues? Please let us know." + + # Commands (npc glowing) + npc_glowing_set_true: "NPC {warningColor}{npc} is now glowing." + npc_glowing_set_false: "NPC {warningColor}{npc} is no longer glowing." + npc_glowing_set_color_success: "NPC {warningColor}{npc} is now glowing in {color}." + npc_glowing_set_color_failure_invalid_color: "› {errorColor}Specified value {warningColor}{input}{errorColor} is not a valid color." + + # Commands (npc info) + npc_info_general: + - "" + - "Unique, permanent identifier of the NPC.'><#848484>Identifier: Click to copy identifier to clipboard.'>{warningColor}{id_short}" + - "Identifier of player who created this NPC.'>Creator: Click to copy creator to clipboard.'>{warningColor}{creator_short}" + - "Name of the NPC, used in commands and displayed above their head if display name is not set.'><#848484>Name: {warningColor}{name}" + - "Display name of the NPC, displayed above their head.'>Display Name: {displayname}" + - "Entity type of the NPC.'><#848484>Type: {warningColor}{type}" + - "Current location of the NPC.'>Location: Click to copy location to clipboard.'>{warningColor}{location_x}, {warningColor}{location_y}, {warningColor}{location_z} in {warningColor}{world}" + - "Glowing state of the NPC. Can be a {warningColor}color or {errorColor}disabled.'><#848484>Glow: {glow}" + - "Whether the NPC should turn to player. Can be {successColor}true or {errorColor}false.'>Turns to Player: {is_turn_to_player}" + - "Whether the NPC should be shown in player-list. Can be {successColor}true or {errorColor}false.'><#848484>Shown in TAB: {is_show_in_tab}" + - "Collidable state of the NPC. Can be {successColor}true or {errorColor}false.'>Collidable: {is_collidable}" + - "Skin mirroring state of the NPC.. Can be {successColor}true or {errorColor}false.'><#848484>Skin Mirroring: {warningColor}{is_skin_mirror}" + - "Cooldown between interactions.'>Interaction Cooldown: {warningColor}{interaction_cooldown}" + - "" + - "Click here to browse equipment.'><#848484>Equipment: {warningColor}[Click Here]" + - "Click here to browse attributes.'>Attributes: {warningColor}[Click Here]" + - "Click here to browse list of messages.'><#848484>Messages: {warningColor}[Click Here] ({messages_total} total)" + - "Click here to browse list of player commands.'>Player Commands: {warningColor}[Click Here] ({player_commands_total} total)" + - "Click here to browse list of server commands.'><#848484>Server Commands: {warningColor}[Click Here] ({server_commands_total} total)" + - "" + - "› {primaryColor}Can't find what you're looking for?" + - "Open the chat window to see all information." + - "" + + # Commands (npc interaction_cooldown) + npc_interaction_cooldown_set: "Interaction cooldown has been set to {warningColor}{time}." + npc_interaction_cooldown_disabled: "Interaction cooldown has been disabled." + + # Commands (npc list) + npc_list_header: "------------------ List Query Result ------------------" + npc_list_entry: " <#848484>{number}. Click to see more details.'>{warningColor}{npc} Click to teleport.'><#848484>({location_x}, {location_y}, {location_z} in {world})" + npc_list_footer: "---------- Showing {warningColor}{count_formatted} out of total {warningColor}{total_formatted} entries ----------" + npc_list_failure_sort_requires_player: "› {errorColor}This sort type cannot be used from the console." + npc_list_failure_requires_world_flag: "› {errorColor}You must specify {warningColor}--world{errorColor} flag when running this command from the console." + + # Commands (npc message) + npc_message_add_success: "Message has been added. There is {warningColor}{total} messages in total." + npc_message_set_success: "Message {warningColor}{number} has been updated. There is {warningColor}{total} messages in total." + npc_message_set_failure_not_in_range: "› {errorColor}Number {warningColor}{input}{errorColor} must be in range between {warningColor}1{errorColor} and {warningColor}{max}{errorColor}." + npc_message_set_failure_list_is_empty: "› {errorColor}There are no messages in the list. Use {warningColor}/npc message (npc) add (none | message){errorColor} to add your first message." + npc_message_remove_success: "Message {warningColor}{number} has been removed. There is {warningColor}{total} messages in total." + npc_message_remove_failure_not_in_range: "› {errorColor}Number {warningColor}{input}{errorColor} must be in range between {warningColor}1{errorColor} and {warningColor}{max}{errorColor}." + npc_message_remove_failure_list_is_empty: "› {errorColor}There are no messages in the list. Use {warningColor}/npc message (npc) add (none | message){errorColor} to add your first message." + npc_message_clear_success: "Messages have been cleared. There was {warningColor}{total} messages in total." + npc_message_send_randomly_set_true: "NPC {warningColor}{npc} is now sending messages randomly." + npc_message_send_randomly_set_false: "NPC {warningColor}{npc} is no longer sending messages randomly." + npc_message_list_header: "--------------------- Messages --------------------" + npc_message_list_entry: " <#848484>{number}. {message}" + npc_message_list_footer: "------------- Showing total of {warningColor}{total_formatted} entries -------------" + npc_message_list_failure_empty: "› {errorColor}There are no messages in the list. Use {warningColor}/npc message (npc) add (none | message){errorColor} to add your first message." + + # Commands (npc move_here) + npc_move_here_success: "NPC {warningColor}{npc} has been moved to your location." + + # Commands (npc move_to) + npc_move_to_success: "NPC {warningColor}{npc} has been moved to {warningColor}{x}, {warningColor}{y}, {warningColor}{z} in {warningColor}{world}." + npc_move_to_failure_must_specify_world: "› {errorColor}You must specify world when running this command from the console." + + # Commands (npc nearby) + npc_nearby_header: "---------------- Nearby Query Result -----------------" + npc_nearby_entry: " <#848484>{number}. Click to see more details.'>{warningColor}{npc} Click to teleport.'><#848484>({distance} blocks away)" + npc_nearby_footer: "---------- Showing {warningColor}{count_formatted} out of total {warningColor}{total_formatted} entries ----------" + + # Commands (npc player_command) + npc_player_command_add_success: "Command has been added. There is {warningColor}{total} commands in total." + npc_player_command_set_success: "Command {warningColor}{number} has been updated. There is {warningColor}{total} commands in total." + npc_player_command_set_failure_not_in_range: "› {errorColor}Number {warningColor}{input}{errorColor} must be in range between {warningColor}1{errorColor} and {warningColor}{max}{errorColor}." + npc_player_command_set_failure_list_is_empty: "› {errorColor}There are no commands in the list. Use {warningColor}/npc player_command (npc) add (command){errorColor} to add your first command." + npc_player_command_remove_success: "Command {warningColor}{number} has been removed. There is {warningColor}{total} commands in total." + npc_player_command_remove_failure_not_in_range: "› {errorColor}Number {warningColor}{input}{errorColor} must be in range between {warningColor}1{errorColor} and {warningColor}{max}{errorColor}." + npc_player_command_remove_failure_list_is_empty: "› {errorColor}There are no commands in the list. Use {warningColor}/npc player_command (npc) add (command){errorColor} to add your first command." + npc_player_command_clear_success: "Commands have been cleared. There was {warningColor}{total} commands in total." + npc_player_command_list_header: "------------------ Player Commands -----------------" + npc_player_command_list_entry: " <#848484>{number}. {command}" + npc_player_command_list_footer: "------------- Showing total of {warningColor}{total_formatted} entries -------------" + npc_player_command_list_failure_empty: "› {errorColor}There are no commands in the list. Use {warningColor}/npc player_command (npc) add (command){errorColor} to add your first command." + + # Commands (npc remove) + npc_remove_success: "NPC {warningColor}{npc} has been removed." + + # Commands (npc server_command) + npc_server_command_add_success: "Command has been added. There is {warningColor}{total} commands in total." + npc_server_command_set_success: "Command {warningColor}{number} has been updated. There is {warningColor}{total} commands in total." + npc_server_command_set_failure_not_in_range: "› {errorColor}Number {warningColor}{input}{errorColor} must be in range between {warningColor}1{errorColor} and {warningColor}{max}{errorColor}." + npc_server_command_set_failure_list_is_empty: "› {errorColor}There are no commands in the list. Use {warningColor}/npc server_command (npc) add (command){errorColor} to add your first command." + npc_server_command_remove_success: "Command {warningColor}{number} has been removed. There is {warningColor}{total} commands in total." + npc_server_command_remove_failure_not_in_range: "› {errorColor}Number {warningColor}{input}{errorColor} must be in range between {warningColor}1{errorColor} and {warningColor}{max}{errorColor}." + npc_server_command_remove_failure_list_is_empty: "› {errorColor}There are no commands in the list. Use {warningColor}/npc server_command (npc) add (command){errorColor} to add your first command." + npc_server_command_clear_success: "Commands have been cleared. There was {warningColor}{total} commands in total." + npc_server_command_list_header: "------------------ Server Commands -----------------" + npc_server_command_list_entry: " <#848484>{number}. {command}" + npc_server_command_list_footer: "------------- Showing total of {warningColor}{total_formatted} entries -------------" + npc_server_command_list_failure_empty: "› {errorColor}There are no commands in the list. Use {warningColor}/npc server_command (npc) add (command){errorColor} to add your first command." + + # Commands (npc show_in_tab) + npc_show_in_tab_set_true: "NPC {warningColor}{npc} is now shown in player-list." + npc_show_in_tab_set_false: "NPC {warningColor}{npc} is no longer shown in player-list." + + # Commands (npc skin) + npc_skin_set_name: "NPC {warningColor}{npc} is now using {warningColor}{name}'s skin." + npc_skin_set_url: "NPC {warningColor}{npc} is now using skin from specified URL." + npc_skin_set_mirror: "NPC {warningColor}{npc} is now mirroring player skin." + npc_skin_set_none: "NPC {warningColor}{npc} is no longer using any skin." + npc_skin_failure_invalid_url: "› {errorColor}Argument {warningColor}{input}{errorColor} is invalid or unsupported URL." + npc_skin_failure_invalid_name_or_url: "› {errorColor}Argument {warningColor}{input}{errorColor} is not a valid player name or URL." + npc_skin_failure_invalid_name_or_rate_limit: "› {errorColor}Could not find {warningColor}{input}'s{errorColor} skin. Either the account does not exist or you're being rate limited." + + # Commands (npc teleport) + npc_teleport_success: "You have been teleported to NPC {warningColor}{npc}." + npc_teleport_failure_exception: "› {errorColor}An error occurred while trying to teleport to NPC. Check console for errors." + npc_teleport_failure_world_not_loaded: "› {errorColor}An error occurred while trying to teleport to NPC. Destination world is not loaded." + + # Commands (npc turn_to_player) + npc_turn_to_player_set_true: "NPC {warningColor}{npc} is now turning to player." + npc_turn_to_player_set_false: "NPC {warningColor}{npc} is no longer turning to player." + + # Commands (npc type) + npc_type_success: "NPC {warningColor}{npc} type has been changed to {warningColor}{type}."