diff --git a/build.gradle.kts b/build.gradle.kts
index 5c45515a..50c6e120 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -25,7 +25,7 @@ plugins {
`maven-publish`
id("xyz.jpenilla.run-paper") version "3.0.2"
id("io.github.goooler.shadow") version "8.1.8"
- id("io.papermc.paperweight.userdev") version "2.0.0-beta.19" apply false
+ id("io.papermc.paperweight.userdev") version "2.0.0-SNAPSHOT" apply false
}
repositories {
@@ -38,20 +38,19 @@ repositories {
dependencies {
implementation(project(":core"))
- implementation(project(":v1_21_R6"))
- implementation(project(":v1_21_R5"))
- implementation(project(":v1_21_R4"))
- implementation(project(":v1_21_R3"))
- implementation(project(":v1_21_R2"))
- implementation(project(":v1_21_R1"))
- implementation(project(":v1_21_R0"))
- implementation(project(":v1_20_R4"))
- implementation(project(":v1_20_R3", "reobf"))
- implementation(project(":v1_20_R2", "reobf"))
- implementation(project(":v1_20_R1", "reobf"))
+ implementation(project(":v26_2_R1", configuration = "default"))
+ implementation(project(":v26_1_R1", configuration = "default"))
+ implementation(project(":v1_21_R6", configuration = "default"))
+ implementation(project(":v1_21_R5", configuration = "default"))
+ implementation(project(":v1_21_R4", configuration = "default"))
+ implementation(project(":v1_21_R3", configuration = "default"))
+ implementation(project(":v1_21_R2", configuration = "default"))
+ implementation(project(":v1_21_R1", configuration = "default"))
+ implementation(project(":v1_21_R0", configuration = "default"))
+ implementation(project(":v1_20_R4", configuration = "default"))
}
-var pluginVersion = "1.7.9"
+var pluginVersion = "1.7.10-pre2"
allprojects {
group = "dev.foxikle"
@@ -59,7 +58,7 @@ allprojects {
description = "CustomNPCs"
}
-java.sourceCompatibility = JavaVersion.VERSION_21
+java.sourceCompatibility = JavaVersion.VERSION_25
val javadocJar = tasks.register("javadocJar") {
archiveClassifier.set("javadoc")
@@ -95,6 +94,7 @@ publishing {
}
tasks {
+
assemble {
dependsOn(shadowJar)
}
@@ -103,6 +103,8 @@ tasks {
dependsOn(shadowJar)
}
+
+
compileJava {
options.encoding = Charsets.UTF_8.name()
options.release = 21
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index f92be989..f90c741f 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -37,11 +37,17 @@ dependencies {
compileOnly("com.github.mqzn:Lotus:1.6.0")
compileOnly("org.bstats:bstats-bukkit:3.1.0")
compileOnly("me.clip:placeholderapi:2.12.1")
- compileOnly("io.papermc.paper:paper-api:1.20.2-R0.1-SNAPSHOT")
+ compileOnly("io.papermc.paper:paper-api:1.20.6-R0.1-SNAPSHOT")
+ compileOnly("net.kyori:adventure-api:5.1.1")
compileOnly("org.mineskin:java-client:3.2.5")
compileOnly("org.mineskin:java-client-jsoup:3.2.5")
- compileOnly("dev.velix:imperat-bukkit:1.9.7")
- compileOnly("dev.velix:imperat-core:1.9.7")
+
+ testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
+ testRuntimeOnly("org.junit.platform:junit-platform-launcher")
+}
+
+tasks.test {
+ useJUnitPlatform()
}
val generateClassloader = tasks.register("generateClassloader") {
@@ -122,7 +128,7 @@ tasks {
}
compileJava {
- options.release = 17
+ options.release = 21
}
jar {
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/Action.java b/core/src/main/java/dev/foxikle/customnpcs/actions/Action.java
index 986b200e..9e8c968c 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/Action.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/Action.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/ActionType.java b/core/src/main/java/dev/foxikle/customnpcs/actions/ActionType.java
index 293f350d..749547b2 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/ActionType.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/ActionType.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java b/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java
index cd739d7d..6ba6c9de 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/LegacyAction.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ActionAdapter.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ActionAdapter.java
index d7cdcaa6..296c0d20 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ActionAdapter.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ActionAdapter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Condition.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Condition.java
index 11d83af1..0e7b399e 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Condition.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Condition.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Conditional.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Conditional.java
index 461d3d00..478bcede 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Conditional.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/Conditional.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ConditionalTypeAdapter.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ConditionalTypeAdapter.java
index 89f3d647..18d61d2b 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ConditionalTypeAdapter.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/ConditionalTypeAdapter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalCondition.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalCondition.java
index c5dd5575..f5f34520 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalCondition.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalCondition.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalConditional.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalConditional.java
index 69fb58c4..57e906a2 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalConditional.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/LogicalConditional.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericCondition.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericCondition.java
index d982a227..30e3b6b2 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericCondition.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericCondition.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericConditional.java b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericConditional.java
index 2cda6ceb..91453f40 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericConditional.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/conditions/NumericConditional.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/ActionBar.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/ActionBar.java
index 6ef3511b..79e02242 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/ActionBar.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/ActionBar.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/DisplayTitle.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/DisplayTitle.java
index 324c088d..71cd8ea3 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/DisplayTitle.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/DisplayTitle.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/FollowPresetPathAction.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/FollowPresetPathAction.java
new file mode 100644
index 00000000..797e7129
--- /dev/null
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/FollowPresetPathAction.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (c) 2026. Foxikle
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package dev.foxikle.customnpcs.actions.defaultImpl;
+
+import com.google.gson.reflect.TypeToken;
+import dev.foxikle.customnpcs.actions.Action;
+import dev.foxikle.customnpcs.actions.conditions.Condition;
+import dev.foxikle.customnpcs.internal.CustomNPCs;
+import dev.foxikle.customnpcs.internal.interfaces.InternalNpc;
+import dev.foxikle.customnpcs.internal.menu.MenuUtils;
+import dev.foxikle.customnpcs.internal.runnables.RecordingRunnable;
+import dev.foxikle.customnpcs.internal.utils.Msg;
+import dev.foxikle.customnpcs.internal.utils.WaitingType;
+import io.github.mqzen.menus.base.Content;
+import io.github.mqzen.menus.base.Menu;
+import io.github.mqzen.menus.misc.Capacity;
+import io.github.mqzen.menus.misc.DataRegistry;
+import io.github.mqzen.menus.misc.button.Button;
+import io.github.mqzen.menus.misc.button.actions.ButtonClickAction;
+import io.github.mqzen.menus.misc.itembuilder.ItemBuilder;
+import io.github.mqzen.menus.titles.MenuTitle;
+import io.github.mqzen.menus.titles.MenuTitles;
+import lombok.Getter;
+import lombok.Setter;
+import org.bukkit.*;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.scheduler.BukkitTask;
+import org.bukkit.util.Vector;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class FollowPresetPathAction extends Action {
+
+ private static final Map> recordingPaths = new ConcurrentHashMap<>();
+ private static final Map viewPaths = new ConcurrentHashMap<>();
+ private static final Map lastRecordTime = new ConcurrentHashMap<>();
+ private static final Map activePlaybacks = new ConcurrentHashMap<>();
+ @Getter
+ @Setter
+ private List path;
+
+ @Getter
+ @Setter
+ private boolean loop;
+
+ public FollowPresetPathAction(List path, boolean loop, int delay, Condition.SelectionMode mode,
+ List conditions, int cooldown) {
+ super(delay, mode, conditions, cooldown);
+ this.path = path;
+ this.loop = loop;
+ }
+
+ public static Button creationButton(Player player) {
+ return Button.clickable(ItemBuilder.modern(Material.RAIL)
+ .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.favicon"))
+ .setLore(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.description"))
+ .build(),
+ ButtonClickAction.plain((menuView, event) -> {
+ Player p = (Player) event.getWhoClicked();
+ event.setCancelled(true);
+ p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1);
+ FollowPresetPathAction action = new FollowPresetPathAction(new ArrayList<>(), false, 0,
+ Condition.SelectionMode.ONE, new ArrayList<>(), 0);
+ CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), action);
+ menuView.getAPI().openMenu(p, action.getMenu());
+ }));
+ }
+
+ public static FollowPresetPathAction deserialize(String serialized, Class extends Action> clazz) {
+ if (clazz != FollowPresetPathAction.class)
+ throw new IllegalArgumentException("This deserialize method only supports the FollowPresetPathAction");
+ ParseResult result = Action.parseBase(serialized);
+ String pathData = Action.parseString(serialized, "path");
+ List path = CustomNPCs.getGson().fromJson(pathData, new TypeToken>() {
+ }.getType());
+ boolean loop = Action.parseBoolean(pathData, "loop");
+ return new FollowPresetPathAction(path, loop, result.delay(), result.mode(), result.conditions(),
+ result.cooldown());
+ }
+
+ public static void startRecording(Player player) {
+ UUID uuid = player.getUniqueId();
+ recordingPaths.put(uuid, new ArrayList<>());
+ lastRecordTime.put(uuid, 0L);
+ viewPaths.put(uuid, Bukkit.getScheduler().runTaskTimer(CustomNPCs.getInstance(), () -> {
+ for (RecordedPathNode n : recordingPaths.get(uuid)) {
+ player.spawnParticle(Particle.END_ROD, n.toLocation(player.getWorld()), 1, 0, 0, 0, 0);
+ }
+ }, 5, 5));
+ CustomNPCs.getInstance().wait(player, WaitingType.RECORDING);
+ new RecordingRunnable(player, CustomNPCs.getInstance()).runTaskTimer(CustomNPCs.getInstance(), 0, 10);
+ }
+
+ public static List stopRecording(Player player) {
+ UUID uuid = player.getUniqueId();
+ lastRecordTime.remove(uuid);
+ viewPaths.remove(uuid).cancel();
+ return recordingPaths.remove(uuid);
+ }
+
+ // Better recordMovement
+ public static void recordMovement(Player player, Location loc) {
+ UUID uuid = player.getUniqueId();
+ List currentPath = recordingPaths.get(uuid);
+ if (currentPath == null) return;
+
+ long now = System.currentTimeMillis();
+ if (currentPath.isEmpty()) {
+ lastRecordTime.put(uuid, now);
+ currentPath.add(new RecordedPathNode(0, loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch()));
+ return;
+ }
+
+ long startTime = lastRecordTime.get(uuid);
+ long elapsed = now - startTime;
+
+ // Sample every 50ms
+ RecordedPathNode lastNode = currentPath.get(currentPath.size() - 1);
+ if (elapsed - lastNode.timestamp() < 50) return;
+
+ // Don't record duplicates
+ if (lastNode.x() == loc.getX() && lastNode.y() == loc.getY() && lastNode.z() == loc.getZ() &&
+ lastNode.yaw() == loc.getYaw() && lastNode.pitch() == loc.getPitch()) {
+ return;
+ }
+
+ currentPath.add(new RecordedPathNode(elapsed, loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(),
+ loc.getPitch()));
+ }
+
+ @Override
+ public void perform(InternalNpc npc, Menu menu, Player player) {
+ if (path == null || path.isEmpty()) return;
+ if (activePlaybacks.containsKey(npc)) {
+ if (loop) return;
+ activePlaybacks.get(npc).cancel();
+ }
+ final Location returnTo = npc.getCurrentLocation();
+
+ npc.teleport(path.getFirst().toLocation(npc.getWorld()));
+
+ final int[] currentIndex = {0};
+ BukkitTask task = Bukkit.getScheduler().runTaskTimer(CustomNPCs.getInstance(), () -> {
+ if (currentIndex[0] >= path.size()) {
+ if (loop) {
+ currentIndex[0] = 0;
+ npc.teleport(path.getFirst().toLocation(npc.getWorld()));
+ } else {
+ BukkitTask t = activePlaybacks.remove(npc);
+ if (t != null) t.cancel();
+ Bukkit.getScheduler().runTaskLater(CustomNPCs.getInstance(), () -> npc.teleport(returnTo), 20L);
+ }
+ return;
+ }
+ Location prev;
+ if (currentIndex[0] > 0) {
+ prev = path.get(currentIndex[0] - 1).toLocation(npc.getWorld());
+ } else prev = npc.getCurrentLocation();
+ RecordedPathNode node = path.get(currentIndex[0]);
+ Location loc = node.toLocation(npc.getWorld());
+ npc.setYRotation(loc.getYaw());
+ npc.setXRotation(loc.getPitch());
+ Vector vec = loc.subtract(prev).toVector();
+ npc.moveTo(vec);
+ currentIndex[0]++;
+ }, 0, 1);
+
+
+ activePlaybacks.put(npc, task);
+ }
+
+ @Override
+ public String serialize() {
+ Map params = new HashMap<>();
+ params.put("path", CustomNPCs.getGson().toJson(path));
+ params.put("loop", loop);
+ return generateSerializedString("FollowPresetPath", params).replace("},]", "}]");
+ }
+
+ @Override
+ public ItemStack getFavicon(Player player) {
+ return ItemBuilder.modern(Material.RAIL)
+ .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.favicon"))
+ .setLore(
+ Msg.translate(player.locale(), "customnpcs.favicons.delay", getDelay()),
+ Msg.format(""),
+ Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.nodes", path.size()),
+ Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.looped", loop),
+ Msg.format(""),
+ Msg.translate(player.locale(), "customnpcs.favicons.edit"),
+ Msg.translate(player.locale(), "customnpcs.favicons.remove")
+ )
+ .build();
+ }
+
+ @Override
+ public Menu getMenu() {
+ return new FollowPathCustomizer(this);
+ }
+
+ @Override
+ public Action clone() {
+ return new FollowPresetPathAction(new ArrayList<>(path), loop, getDelay(), getMode(),
+ new ArrayList<>(getConditions()), getCooldown());
+ }
+
+ private class FollowPathCustomizer implements Menu {
+ private final FollowPresetPathAction action;
+
+ public FollowPathCustomizer(FollowPresetPathAction action) {
+ this.action = action;
+ }
+
+ @Override
+ public String getName() {
+ return "follow_path_customizer";
+ }
+
+ @Override
+ public @NotNull MenuTitle getTitle(DataRegistry dataRegistry, Player player) {
+ return MenuTitles.createModern(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.title"));
+ }
+
+ @Override
+ public @NotNull Capacity getCapacity(DataRegistry dataRegistry, Player player) {
+ return Capacity.ofRows(5);
+ }
+
+ @Override
+ public @NotNull Content getContent(DataRegistry dataRegistry, Player player, Capacity capacity) {
+ return MenuUtils.actionBase(action, player)
+ .setButton(20, button(player))
+ .setButton(24, candle(player))
+ .build();
+ }
+
+ private Button candle(Player player) {
+ return Button.clickable(ItemBuilder.modern(action.loop ? Material.GREEN_CANDLE : Material.RED_CANDLE)
+ .setDisplay(Msg.translate(player.locale(), action.loop ? "customnpcs.menus.action.follow_path" +
+ ".loop.true" : "customnpcs.menus.action" +
+ ".follow_path.loop.false"))
+ .setLore(Msg.lore(player.locale(), action.loop ? "customnpcs.menus.action.follow_path.loop.true" +
+ ".description" : "customnpcs.menus.action" +
+ ".follow_path.loop.false" +
+ ".description"))
+ .build(), ButtonClickAction.plain((m, e) -> {
+ e.setCancelled(true);
+ player.playSound(player, Sound.UI_BUTTON_CLICK, 1, 1);
+ action.loop = !action.loop;
+ m.updateButton(24, button -> button.setItem(candle(player).getItem()));
+ //todo: autostart
+ }
+ ));
+ }
+
+ private Button button(Player player) {
+ if (action.path == null || action.path.isEmpty()) {
+ return Button.clickable(ItemBuilder.modern(Material.PLAYER_HEAD)
+ .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.record"))
+ .setLore(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.record.lore"))
+ .build(), ButtonClickAction.plain((menuView, event) -> {
+ player.closeInventory();
+ startRecording(player);
+ }));
+ }
+
+ return Button.clickable(ItemBuilder.modern(Material.PLAYER_HEAD)
+ .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.rerecord"))
+ .setLore(Msg.translate(player.locale(), "customnpcs.menus.action.follow_path.rerecord.lore",
+ path.size()))
+ .build(), ButtonClickAction.plain((menuView, event) -> {
+ player.closeInventory();
+ startRecording(player);
+ }));
+ }
+ }
+}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveEffect.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveEffect.java
index 20eb3205..ad9bdf7f 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveEffect.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveEffect.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveXP.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveXP.java
index db1d8130..d799f541 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveXP.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/GiveXP.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/PlaySound.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/PlaySound.java
index 4c3b9dd8..ff947331 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/PlaySound.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/PlaySound.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/v1_20_R2/src/main/java/dev/foxikle/customnpcs/versions/FakeConnection_v1_20_R2.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RecordedPathNode.java
similarity index 64%
rename from v1_20_R2/src/main/java/dev/foxikle/customnpcs/versions/FakeConnection_v1_20_R2.java
rename to core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RecordedPathNode.java
index 23c991e7..d8af60b0 100644
--- a/v1_20_R2/src/main/java/dev/foxikle/customnpcs/versions/FakeConnection_v1_20_R2.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RecordedPathNode.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -20,27 +20,23 @@
* SOFTWARE.
*/
-package dev.foxikle.customnpcs.versions;
+package dev.foxikle.customnpcs.actions.defaultImpl;
-import net.minecraft.network.Connection;
-import net.minecraft.network.PacketListener;
-import net.minecraft.network.protocol.PacketFlow;
+import org.bukkit.Location;
+import org.bukkit.World;
-/**
- * A fake connection for the NPCs
- */
-public class FakeConnection_v1_20_R2 extends Connection {
- /**
- * Creates a fake Connection for NPC
- *
- * @param enumprotocoldirection The protocol direction
- */
- public FakeConnection_v1_20_R2(PacketFlow enumprotocoldirection) {
- super(enumprotocoldirection);
+import java.io.Serializable;
+
+public record RecordedPathNode(long timestamp, double x, double y, double z, float yaw,
+ float pitch) implements Serializable {
+
+ public Location toLocation(World world) {
+ return new Location(world, x, y, z, yaw, pitch);
}
@Override
- public void setListener(PacketListener packetListener) {
-
+ public String toString() {
+ return String.format("Node{t=%d, x=%.2f, y=%.2f, z=%.2f, yaw=%.1f, pitch=%.1f}", timestamp, x, y, z, yaw,
+ pitch);
}
}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveEffect.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveEffect.java
index 295d21a5..16e6ad6e 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveEffect.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveEffect.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveXP.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveXP.java
index df4074f9..0a1edd11 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveXP.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RemoveXP.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RunCommand.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RunCommand.java
index 05317a49..978039f1 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RunCommand.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/RunCommand.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -62,25 +62,9 @@
@Setter
public class RunCommand extends Action {
- public static Button creationButton(Player player) {
- return Button.clickable(ItemBuilder.modern(ANVIL)
- .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.command"))
- .setLore(Msg.lore(player.locale(), "customnpcs.favicons.command.description"))
- .build(),
- ButtonClickAction.plain((menuView, event) -> {
- Player p = (Player) event.getWhoClicked();
- event.setCancelled(true);
- p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1);
- RunCommand actionImpl = new RunCommand("say hi", false, 0, Condition.SelectionMode.ONE, new ArrayList<>(), 0);
- CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl);
- menuView.getAPI().openMenu(p, actionImpl.getMenu());
- }));
- }
-
private String command;
private boolean asConsole;
-
/**
* Creates a new RunCommand with the specified command
*
@@ -90,12 +74,14 @@ public static Button creationButton(Player player) {
* @param mode The mode
* @param conditionals The conditionals
*/
- public RunCommand(String rawCommand, boolean asConsole, int delay, Condition.SelectionMode mode, List conditionals, int cooldown) {
+ public RunCommand(String rawCommand, boolean asConsole, int delay, Condition.SelectionMode mode,
+ List conditionals, int cooldown) {
super(delay, mode, conditionals, cooldown);
this.command = rawCommand;
this.asConsole = asConsole;
}
+
/**
* Creates a new RunCommand with the specified command
*
@@ -108,12 +94,29 @@ public RunCommand(String rawCommand, boolean asConsole, int delay, Condition.Sel
*/
@Deprecated
@ApiStatus.ScheduledForRemoval(inVersion = "1.9")
- public RunCommand(String rawCommand, boolean asConsole, int delay, Condition.SelectionMode mode, List conditionals) {
+ public RunCommand(String rawCommand, boolean asConsole, int delay, Condition.SelectionMode mode,
+ List conditionals) {
super(delay, mode, conditionals, 0);
this.command = rawCommand;
this.asConsole = asConsole;
}
+ public static Button creationButton(Player player) {
+ return Button.clickable(ItemBuilder.modern(ANVIL)
+ .setDisplay(Msg.translate(player.locale(), "customnpcs.favicons.command"))
+ .setLore(Msg.lore(player.locale(), "customnpcs.favicons.command.description"))
+ .build(),
+ ButtonClickAction.plain((menuView, event) -> {
+ Player p = (Player) event.getWhoClicked();
+ event.setCancelled(true);
+ p.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1);
+ RunCommand actionImpl = new RunCommand("say hi", false, 0, Condition.SelectionMode.ONE,
+ new ArrayList<>(), 0);
+ CustomNPCs.getInstance().editingActions.put(p.getUniqueId(), actionImpl);
+ menuView.getAPI().openMenu(p, actionImpl.getMenu());
+ }));
+ }
+
public static T deserialize(String serialized, Class clazz) {
if (!clazz.equals(RunCommand.class)) {
throw new IllegalArgumentException("Cannot deserialize " + clazz.getName() + " to " + RunCommand.class.getName());
@@ -169,7 +172,8 @@ public String serialize() {
@Override
public Action clone() {
- return new RunCommand(command, asConsole, getDelay(), getMode(), new ArrayList<>(getConditions()), getCooldown());
+ return new RunCommand(command, asConsole, getDelay(), getMode(), new ArrayList<>(getConditions()),
+ getCooldown());
}
public class RunCommandCustomizer implements Menu {
@@ -198,7 +202,9 @@ public String getName() {
@Override
public @NotNull Content getContent(DataRegistry dataRegistry, Player player, Capacity capacity) {
return MenuUtils.actionBase(action, player)
- .setButton(player.hasPermission("customnpcs.run_command.enable_console") ? 21 : 22, setCommand(player))
+ .setButton(4, papiTip(player))
+ .setButton(player.hasPermission("customnpcs.run_command.enable_console") ? 21 : 22,
+ setCommand(player))
.setButton(23, toggle(player))
.build();
}
@@ -207,12 +213,15 @@ private Button toggle(Player player) {
if (!player.hasPermission("customnpcs.run_command.enable_console")) return MenuItems.MENU_GLASS;
List lore = new ArrayList<>();
if (isAsConsole()) {
- lore.addAll(Utils.list(Msg.lore(player.locale(), "customnpcs.menus.action.command.as_console.warning")));
+ lore.addAll(Utils.list(Msg.lore(player.locale(),
+ "customnpcs.menus.action.command.as_console.warning")));
}
lore.add(Msg.translate(player.locale(), "customnpcs.items.click_to_change"));
return Button.clickable(ItemBuilder.modern(isAsConsole() ? RED_CANDLE : GREEN_CANDLE)
.setLore(lore.toArray(new Component[]{}))
- .setDisplay(isAsConsole() ? Msg.translate(player.locale(), "customnpcs.menus.action.command.as_console.true") : Msg.translate(player.locale(), "customnpcs.menus.action.command.as_console.false"))
+ .setDisplay(isAsConsole() ? Msg.translate(player.locale(), "customnpcs.menus.action" +
+ ".command.as_console.true") : Msg.translate(player.locale(), "customnpcs.menus" +
+ ".action.command.as_console.false"))
.build(),
ButtonClickAction.plain((menuView, event) -> {
event.setCancelled(true);
@@ -243,5 +252,31 @@ private Button setCommand(Player player) {
player.playSound(event.getWhoClicked(), Sound.UI_BUTTON_CLICK, 1, 1);
}));
}
+
+ private Button papiTip(Player player) {
+ Component[] lore;
+ if (!CustomNPCs.getInstance().papi) {
+ lore = Msg.lore(player.locale(), "customnpcs.menus.action.command.papi_tip.no_papi");
+ } else if (!CustomNPCs.getInstance().papiPlayerExpansion) {
+ lore = Msg.lore(player.locale(), "customnpcs.menus.action.command.papi_tip.no_expansion");
+ } else {
+ lore = Msg.lore(player.locale(), "customnpcs.menus.action.command.papi_tip.all_good");
+ }
+ return Button.clickable(ItemBuilder.modern(REDSTONE_TORCH)
+ .setDisplay(Msg.translate(player.locale(), "customnpcs.menus.action.command.papi_tip.title"))
+ .setLore(lore)
+ .build(), ButtonClickAction.plain((menuView, inventoryClickEvent) -> {
+ inventoryClickEvent.setCancelled(true);
+ if (!CustomNPCs.getInstance().papi) {
+ player.sendMessage(Msg.translate(player.locale(),
+ "customnpcs.menus.action.command.papi_tip.download.plugin"));
+ return;
+ }
+ if (!CustomNPCs.getInstance().papiPlayerExpansion) {
+ player.sendMessage(Msg.translate(player.locale(),
+ "customnpcs.menus.action.command.papi_tip.download.expansion"));
+ }
+ }));
+ }
}
}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendMessage.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendMessage.java
index 18bb2d4a..01e084a0 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendMessage.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendMessage.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendServer.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendServer.java
index 6815c51d..4e9f41a0 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendServer.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/SendServer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/Teleport.java b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/Teleport.java
index 1bc87b0c..a9b4be9c 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/Teleport.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/actions/defaultImpl/Teleport.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/api/NPC.java b/core/src/main/java/dev/foxikle/customnpcs/api/NPC.java
index 470bb5d7..9df894cf 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/api/NPC.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/api/NPC.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/api/NPCApi.java b/core/src/main/java/dev/foxikle/customnpcs/api/NPCApi.java
index 96b68347..8f104be1 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/api/NPCApi.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/api/NPCApi.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/api/Pose.java b/core/src/main/java/dev/foxikle/customnpcs/api/Pose.java
index 972a0a8f..b0ae283d 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/api/Pose.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/api/Pose.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025. Foxikle
+ * Copyright (c) 2025-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcDeleteEvent.java b/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcDeleteEvent.java
index f5e48641..fe370e5b 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcDeleteEvent.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcDeleteEvent.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcEvent.java b/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcEvent.java
index 76059e3d..2cfdf717 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcEvent.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcEvent.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInjectEvent.java b/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInjectEvent.java
index 0384a49a..6589a57d 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInjectEvent.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInjectEvent.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInteractEvent.java b/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInteractEvent.java
index a33196db..832e3db3 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInteractEvent.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/api/events/NpcInteractEvent.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/data/Equipment.java b/core/src/main/java/dev/foxikle/customnpcs/data/Equipment.java
index 8a2eea30..e43cb6ab 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/data/Equipment.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/data/Equipment.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/data/Settings.java b/core/src/main/java/dev/foxikle/customnpcs/data/Settings.java
index e93e6cc5..bd55ad3c 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/data/Settings.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/data/Settings.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/CustomNPCs.java b/core/src/main/java/dev/foxikle/customnpcs/internal/CustomNPCs.java
index bc39681f..d7a69b50 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/CustomNPCs.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/CustomNPCs.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -26,6 +26,7 @@
import com.google.common.cache.CacheBuilder;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.mojang.brigadier.CommandDispatcher;
import dev.foxikle.customnpcs.actions.Action;
import dev.foxikle.customnpcs.actions.LegacyAction;
import dev.foxikle.customnpcs.actions.conditions.ActionAdapter;
@@ -34,10 +35,7 @@
import dev.foxikle.customnpcs.actions.defaultImpl.*;
import dev.foxikle.customnpcs.data.Equipment;
import dev.foxikle.customnpcs.data.Settings;
-import dev.foxikle.customnpcs.internal.commands.NpcCommand;
-import dev.foxikle.customnpcs.internal.commands.suggestion.NpcSuggester;
-import dev.foxikle.customnpcs.internal.commands.suggestion.SoundSuggester;
-import dev.foxikle.customnpcs.internal.commands.suggestion.WorldSuggester;
+import dev.foxikle.customnpcs.internal.commands.NpcCommandRegistrar;
import dev.foxikle.customnpcs.internal.interfaces.InternalNpc;
import dev.foxikle.customnpcs.internal.listeners.Listeners;
import dev.foxikle.customnpcs.internal.menu.*;
@@ -45,11 +43,11 @@
import dev.foxikle.customnpcs.internal.utils.ActionRegistry;
import dev.foxikle.customnpcs.internal.utils.AutoUpdater;
import dev.foxikle.customnpcs.internal.utils.WaitingType;
-import dev.velix.imperat.BukkitImperat;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.Imperat;
import io.github.mqzen.menus.Lotus;
import io.github.mqzen.menus.base.pagination.Pagination;
+import io.papermc.paper.command.brigadier.Commands;
+import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager;
+import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@@ -63,6 +61,7 @@
import org.bukkit.entity.Player;
import org.bukkit.entity.TextDisplay;
import org.bukkit.event.HandlerList;
+import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.messaging.PluginMessageListener;
import org.jetbrains.annotations.NotNull;
@@ -108,7 +107,7 @@ public final class CustomNPCs extends JavaPlugin implements PluginMessageListene
TimeUnit.MINUTES).expireAfterAccess(1, TimeUnit.MINUTES).build();
private final String[] COMPATIBLE_VERSIONS = {"1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6",
"1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5", "1.21.6", "1.21.7", "1.21.8", "1.21.9",
- "1.21.10", "1.21.11"};
+ "1.21.10", "1.21.11", "26.1", "26.1.1", "26.1.2", "26.2"};
private final String NPC_CLASS = "dev.foxikle.customnpcs.versions.NPC_%s";
/**
* The map of what the plugin is waiting for the players to enter.
@@ -142,6 +141,10 @@ public final class CustomNPCs extends JavaPlugin implements PluginMessageListene
* If the plugin should try to format messages with PlaceholderAPI
*/
public boolean papi = false;
+ /**
+ * If this server has the Player expansion installed
+ */
+ public boolean papiPlayerExpansion = false;
/**
* If there is a new update available
*/
@@ -165,7 +168,7 @@ public final class CustomNPCs extends JavaPlugin implements PluginMessageListene
@Getter
private Lotus lotus;
@Getter
- private Imperat imperat;
+ private CommandDispatcher commandDispatcher;
@Getter
@Setter
@@ -180,11 +183,24 @@ public final class CustomNPCs extends JavaPlugin implements PluginMessageListene
*
*/
@Override
+ @SuppressWarnings("UnstableApiUsage")
public void onEnable() {
// paper... why??
- System.setProperty("org.bukkit.plugin.java.LibraryLoader.centralURL", "https://repo1.maven.org/maven2");
instance = this;
+ if (!Objects.equals(System.getProperty("CustomNPCs-Started"), "true")) {
+ LifecycleEventManager manager = this.getLifecycleManager();
+ manager.registerEventHandler(LifecycleEvents.COMMANDS, event -> {
+ final Commands commands = event.registrar();
+ commands.register(
+ NpcCommandRegistrar.buildNode(),
+ "The command for anything NPC related.",
+ List.of()
+ );
+ });
+ System.setProperty("CustomNPCs-Started", "true");
+ }
+
if (!checkForValidVersion()) {
printInvalidVersion();
Bukkit.getPluginManager().disablePlugin(this);
@@ -199,7 +215,7 @@ public void onEnable() {
} catch (ClassNotFoundException e) {
getLogger().log(Level.SEVERE, "Failed to load NPC class for server version " + s + "!", e);
} catch (Exception e) {
- getLogger().log(Level.SEVERE, "SERVERE ERROR: ", e);
+ getLogger().log(Level.SEVERE, "SERVER ERROR: ", e);
}
INTERPOLATION_DURATION = getConfig().getInt("DefaultInterpolationDuration");
@@ -232,6 +248,8 @@ public void onEnable() {
ACTION_REGISTRY.register("SendMessage", SendMessage.class, SendMessage::creationButton);
ACTION_REGISTRY.register("SendServer", SendServer.class, SendServer::creationButton, true, false, true);
ACTION_REGISTRY.register("Teleport", Teleport.class, Teleport::creationButton);
+ ACTION_REGISTRY.register("FollowPresetPath", FollowPresetPathAction.class,
+ FollowPresetPathAction::creationButton);
try {
this.getLogger().info("Loading NPCs!");
@@ -312,16 +330,6 @@ public void onEnable() {
);
}));
- // setup papi
- if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
- this.getLogger().info("Successfully hooked into PlaceholderAPI.");
- papi = true;
- } else {
- papi = false;
- this.getLogger().warning("Could not find PlaceholderAPI! PlaceholderAPI isn't required, but CustomNPCs " +
- "does support it.");
- }
-
if (!System.getProperties().containsKey("customnpcs-reload-check")) {
getLogger().info("Loading listeners...");
listeners = new Listeners(this);
@@ -350,23 +358,6 @@ public void onEnable() {
lotus.registerMenu(new HologramMenu());
lotus.registerMenu(new DeleteLineMenu());
lotus.registerMenu(new PoseEditorMenu());
-
- // prevent reload goofery
- if (!System.getProperties().containsKey("CUSTOMNPCS_LOADED")) {
- getLogger().info("Loading commands!");
-
- imperat = BukkitImperat.builder(this).applyBrigadier(false)//todo: true
- .namedSuggestionResolver("sound", new SoundSuggester())
- .namedSuggestionResolver("current_npc", new NpcSuggester())
- .namedSuggestionResolver("broken_npc", new NpcSuggester())
- .namedSuggestionResolver("worlds", new WorldSuggester())
- .build();
-
- // only one command, the rest are sub commands
- imperat.registerCommand(new NpcCommand());
- }
-
- System.setProperty("CUSTOMNPCS_LOADED", "true");
}
/**
@@ -406,9 +397,9 @@ public void onDisable() {
// don't unregister these on reload
if (!reloading) {
- if (imperat != null) {
- imperat.shutdownPlatform();
- imperat.unregisterAllCommands();
+ if (commandDispatcher != null) {
+ // Clear the command dispatcher by getting a new instance
+ commandDispatcher = null;
}
}
}
@@ -503,9 +494,6 @@ public InternalNpc createNPC(World world, Location location, Equipment equipment
public String translateVersion() {
return switch (serverVersion) {
- case "1.20", "1.20.1" -> "v1_20_R1";
- case "1.20.2" -> "v1_20_R2";
- case "1.20.3", "1.20.4" -> "v1_20_R3";
case "1.20.5", "1.20.6" -> "v1_20_R4";
case "1.21", "1.21.1" -> "v1_21_R0";
case "1.21.2", "1.21.3" -> "v1_21_R1";
@@ -514,6 +502,8 @@ public String translateVersion() {
case "1.21.6", "1.21.7", "1.21.8" -> "v1_21_R4";
case "1.21.9", "1.21.10" -> "v1_21_R5";
case "1.21.11" -> "v1_21_R6";
+ case "26.1", "26.1.1", "26.1.2" -> "v26_1_R1";
+ case "26.2" -> "v26_2_R1";
default -> "";
};
}
@@ -535,7 +525,7 @@ private void printInvalidVersion() {
logger.severe("+------------------------------------------------------------------------------+");
logger.severe("| INVALID SERVER VERSION DETECTED |");
logger.severe("| ** PLEASE USE ONE OF THE FOLLOWING SERVER VERSIONS ** |");
- logger.severe("| [1.20.x, 1.21.x] |");
+ logger.severe("| [1.20.x, 1.21.x, 26.x] |");
logger.severe("| DETECTED: '" + serverVersion + "' " +
"|");
logger.severe("| Please contact @foxikle on Discord for more information. |");
@@ -556,4 +546,17 @@ public Pagination getSkinCatalog(Player player) {
return getMenuUtils().getSkinCatalogue(player.locale());
}
+ public static class BukkitImperat {
+ public static void handleCommandException(com.mojang.brigadier.exceptions.CommandSyntaxException e,
+ org.bukkit.command.CommandSender sender) {
+ if (e.getCursor() >= 0) {
+ String input = e.getInput();
+ String pointer = " ".repeat(Math.max(0, e.getCursor())) + "^";
+ sender.sendMessage(input);
+ sender.sendMessage(pointer);
+ }
+ sender.sendMessage(e.getMessage());
+ }
+ }
+
}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/FileManager.java b/core/src/main/java/dev/foxikle/customnpcs/internal/FileManager.java
index f6e3d46a..47b6d930 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/FileManager.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/FileManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/InjectionManager.java b/core/src/main/java/dev/foxikle/customnpcs/internal/InjectionManager.java
index 118a7b17..cd6a46ca 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/InjectionManager.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/InjectionManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/LookAtAnchor.java b/core/src/main/java/dev/foxikle/customnpcs/internal/LookAtAnchor.java
index 612f2526..34e6a4d6 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/LookAtAnchor.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/LookAtAnchor.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CloneCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CloneCommand.java
deleted file mode 100644
index 3d3e93cc..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CloneCommand.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.foxikle.customnpcs.internal.interfaces.InternalNpc;
-import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.*;
-import dev.velix.imperat.command.AttachmentMode;
-import org.bukkit.entity.Player;
-
-import java.util.UUID;
-
-@SubCommand(value = "clone", attachment = AttachmentMode.MAIN)
-@Permission("customnpcs.commands.clone")
-@Description("Clones the the npcs.")
-public class CloneCommand {
- @Usage
- public void usage(
- BukkitSource source,
- @Named("npc") @SuggestionProvider("current_npc") @Greedy String npc
- ) {
- if (source.isConsole()) {
- source.reply(Msg.format("You can't do this :P"));
- return;
- }
-
- UUID uuid = CommandUtils.parseNpc(source, npc);
- if (uuid == null) {
- return;
- }
- if (!CommandUtils.checkNpc(source, uuid)) return;
-
- final Player p = source.asPlayer();
- final CustomNPCs plugin = CustomNPCs.getInstance();
- final InternalNpc finalNpc = plugin.getNPCByID(uuid);
-
- assert finalNpc != null;
- InternalNpc newNpc = finalNpc.clone();
- newNpc.setSpawnLoc(p.getLocation());
- newNpc.getSettings().setDirection(p.getLocation().getYaw());
- newNpc.createNPC();
- p.sendMessage(Msg.translate(p.locale(), "customnpcs.commands.clone.success"));
- }
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CommandUtils.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CommandUtils.java
index 8a69ad45..a52239f0 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CommandUtils.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CommandUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -26,13 +26,13 @@
import dev.foxikle.customnpcs.internal.CustomNPCs;
import dev.foxikle.customnpcs.internal.interfaces.InternalNpc;
import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
+import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -42,9 +42,6 @@
import java.util.UUID;
import java.util.stream.Collectors;
-/**
- * The class to handle the core command
- */
@UtilityClass
public class CommandUtils {
@@ -96,14 +93,13 @@ public static Component getListComponent(Locale p) {
return Msg.translate(p, "customnpcs.commands.manage.no_npcs");
}
-
Component message = Msg.translate(p, "customnpcs.commands.manage.header").appendNewline();
for (InternalNpc npc : plugin.getNPCs()) {
if (npc.getSettings().isResilient()) {
Component name = Msg.format("◆ ")
- .append(plugin.getMiniMessage().deserialize(npc.getSettings().getName()).appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.manage.copy_uuid")))).clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, npc.getUniqueID().toString()))
- .append(Msg.translate(p, "customnpcs.commands.manage.button.edit").appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.manage.button.edit.hover"))).clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/npc edit " + npc.getUniqueID())))
- .append(Msg.translate(p, "customnpcs.commands.manage.button.delete").appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.manage.button.delete.hover"))).clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/npc delete " + npc.getUniqueID())))
+ .append(plugin.getMiniMessage().deserialize(npc.getSettings().getName()).appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.manage.copy_uuid")))).clickEvent(ClickEvent.copyToClipboard(npc.getUniqueID().toString()))
+ .append(Msg.translate(p, "customnpcs.commands.manage.button.edit").appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.manage.button.edit.hover"))).clickEvent(ClickEvent.runCommand("/npc edit " + npc.getUniqueID())))
+ .append(Msg.translate(p, "customnpcs.commands.manage.button.delete").appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(p, "customnpcs.commands.manage.button.delete.hover"))).clickEvent(ClickEvent.suggestCommand("/npc delete " + npc.getUniqueID())))
.appendNewline();
message = message.append(name);
}
@@ -112,44 +108,38 @@ public static Component getListComponent(Locale p) {
return message;
}
- public static boolean checkNpc(BukkitSource source, UUID npc) {
+ public static boolean checkNpc(CommandSender source, UUID npc) {
Locale locale = Locale.getDefault();
- if (!source.isConsole()) locale = source.asPlayer().locale();
+ if (source instanceof Player player) locale = player.locale();
if (npc == null) {
- source.reply(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid"));
+ source.sendMessage(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid"));
return false;
}
boolean valid = CustomNPCs.getInstance().npcs.containsKey(npc);
- if (!valid) source.reply(Msg.translate(locale, "customnpcs.commands.invalid_uuid"));
+ if (!valid) source.sendMessage(Msg.translate(locale, "customnpcs.commands.invalid_uuid"));
return valid;
}
- public static UUID parseNpc(BukkitSource source, String data) {
-
+ public static UUID parseNpc(CommandSender source, String data) {
Locale locale = Locale.getDefault();
- if (!source.isConsole()) locale = source.asPlayer().locale();
+ if (source instanceof Player player) locale = player.locale();
final CustomNPCs plugin = CustomNPCs.getInstance();
UUID uuid;
try {
uuid = UUID.fromString(data);
if (plugin.getNPCByID(uuid) == null) {
- source.reply(Msg.translate(locale, "customnpcs.commands.invalid_uuid"));
+ source.sendMessage(Msg.translate(locale, "customnpcs.commands.invalid_uuid"));
return null;
}
-// npc = plugin.getNPCByID(uuid);
} catch (IllegalArgumentException ignored) {
-
- if (source.isConsole()) {
+ if (!(source instanceof Player p)) {
return null;
}
- Player p = source.asPlayer();
-
- // check for names instead
Locale finalLocale = locale;
Set uuids = plugin.npcs.values().stream().map(npc -> {
if (plugin.getMiniMessage().stripTags(npc.getSettings().getName()).equalsIgnoreCase(data)) {
@@ -160,17 +150,16 @@ public static UUID parseNpc(BukkitSource source, String data) {
uuids.removeIf(Objects::isNull);
if (uuids.isEmpty()) {
- source.reply(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid"));
+ source.sendMessage(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid"));
return null;
} else if (uuids.size() > 1) {
double value = Double.MAX_VALUE;
uuid = null;
for (UUID id : uuids) {
-
InternalNpc npc = plugin.getNPCByID(id);
assert npc != null : "Npc is null when parsing the closest NPC";
- if (p.getWorld() != npc.getWorld()) continue; // fix error tracing distanced across worlds
+ if (p.getWorld() != npc.getWorld()) continue;
double ds = npc.getCurrentLocation().distanceSquared(p.getLocation());
if (ds < value) {
@@ -180,21 +169,18 @@ public static UUID parseNpc(BukkitSource source, String data) {
}
if (uuid == null) {
- // meaning the player is in a different world than the npcs with the same name
uuid = Iterables.getFirst(uuids, null);
- // get the first one
}
-
} else {
uuid = Iterables.getFirst(uuids, null);
}
if (uuid == null) {
- source.reply(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid"));
+ source.sendMessage(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid"));
return null;
}
if (plugin.getNPCByID(uuid) == null) {
- p.sendMessage(Msg.translate(p.locale(), "customnpcs.commands.invalid_uuid"));
+ source.sendMessage(Msg.translate(p.locale(), "customnpcs.commands.invalid_uuid"));
return null;
}
return uuid;
@@ -202,25 +188,23 @@ public static UUID parseNpc(BukkitSource source, String data) {
InternalNpc npc = plugin.getNPCByID(uuid);
-
if (npc == null) {
- source.reply(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid"));
+ source.sendMessage(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid"));
return null;
}
boolean valid = CustomNPCs.getInstance().npcs.containsKey(uuid);
if (!valid) {
- source.reply(Msg.translate(locale, "customnpcs.commands.invalid_uuid"));
+ source.sendMessage(Msg.translate(locale, "customnpcs.commands.invalid_uuid"));
return null;
} else {
return uuid;
}
}
- public static Locale getLocale(BukkitSource source) {
- if (source.isConsole()) return Locale.getDefault();
- return source.asPlayer().locale();
+ public static Locale getLocale(CommandSender source) {
+ if (source instanceof Player player) return player.locale();
+ return Locale.getDefault();
}
-
-}
+}
\ No newline at end of file
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CreateCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CreateCommand.java
deleted file mode 100644
index c2a4b515..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/CreateCommand.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.foxikle.customnpcs.data.Equipment;
-import dev.foxikle.customnpcs.data.Settings;
-import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.foxikle.customnpcs.internal.interfaces.InternalNpc;
-import dev.foxikle.customnpcs.internal.menu.MenuUtils;
-import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.SubCommand;
-import dev.velix.imperat.annotations.Usage;
-import dev.velix.imperat.command.AttachmentMode;
-import org.bukkit.entity.Player;
-
-import java.util.ArrayList;
-import java.util.UUID;
-
-@SubCommand(value = {"create"}, attachment = AttachmentMode.MAIN)
-public class CreateCommand {
-
- @Usage
- public void usage(BukkitSource source) {
- if (source.isConsole()) {
- source.reply(Msg.format("You can't do this :P"));
- return;
- }
- final Player p = source.asPlayer();
- final UUID uuid = UUID.randomUUID();
- final CustomNPCs plugin = CustomNPCs.getInstance();
- InternalNpc npc = plugin.createNPC(p.getWorld(), p.getLocation(), new Equipment(), new Settings(), uuid, null, new ArrayList<>());
- plugin.getEditingNPCs().put(p.getUniqueId(), npc);
- plugin.getLotus().openMenu(p, MenuUtils.NPC_MAIN);
- }
-
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/DeleteCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/DeleteCommand.java
deleted file mode 100644
index 1c3995ea..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/DeleteCommand.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.foxikle.customnpcs.internal.interfaces.InternalNpc;
-import dev.foxikle.customnpcs.internal.menu.MenuUtils;
-import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.*;
-import dev.velix.imperat.command.AttachmentMode;
-import org.bukkit.entity.Player;
-
-import java.util.UUID;
-
-@SubCommand(value = "delete", attachment = AttachmentMode.MAIN)
-@Permission("customnpcs.delete")
-@Description("Deletes the specified NPC")
-public class DeleteCommand {
- @Usage
- public void usage(
- BukkitSource source,
- @Named("npc") @SuggestionProvider("current_npc") @Greedy String npc
- ) {
- if (source.isConsole()) {
- source.reply(Msg.format("You can't do this :P"));
- return;
- }
-
- UUID uuid = CommandUtils.parseNpc(source, npc);
- if (uuid == null) {
- return;
- }
- if (!CommandUtils.checkNpc(source, uuid)) return;
-
- final Player p = source.asPlayer();
- final CustomNPCs plugin = CustomNPCs.getInstance();
- final InternalNpc finalNpc = plugin.getNPCByID(uuid);
-
- assert finalNpc != null;
- plugin.getEditingNPCs().put(p.getUniqueId(), finalNpc);
- plugin.getDeletionReason().put(p.getUniqueId(), false);
- plugin.getLotus().openMenu(p, MenuUtils.NPC_DELETE);
- }
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/EditCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/EditCommand.java
deleted file mode 100644
index 86626360..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/EditCommand.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.foxikle.customnpcs.internal.interfaces.InternalNpc;
-import dev.foxikle.customnpcs.internal.menu.MenuUtils;
-import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.*;
-import dev.velix.imperat.command.AttachmentMode;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
-
-import java.util.UUID;
-
-@Permission("customnpcs.edit")
-@SubCommand(value = "edit", attachment = AttachmentMode.MAIN)
-@Description("Edits an NPC")
-public class EditCommand {
-
- @Usage
- public void editNpc(
- BukkitSource source,
- @Named("npc") @SuggestionProvider("current_npc") @Greedy String npc
- ) {
- if (source.isConsole()) {
- source.reply(Msg.format("You can't do this :P"));
- return;
- }
-
- UUID uuid = CommandUtils.parseNpc(source, npc);
- if (uuid == null) {
- return;
- }
-// if (!CommandUtils.checkNpc(source, uuid)) return;
-
- final Player p = source.asPlayer();
- final CustomNPCs plugin = CustomNPCs.getInstance();
- final InternalNpc finalNpc = plugin.getNPCByID(uuid);
- Bukkit.getScheduler().runTaskLater(plugin, () -> {
- InternalNpc newNpc = plugin.createNPC(p.getWorld(), finalNpc.getSpawnLoc(), finalNpc.getEquipment(), finalNpc.getSettings(), finalNpc.getUniqueID(), null, finalNpc.getActions());
- plugin.getEditingNPCs().put(p.getUniqueId(), newNpc);
- plugin.getLotus().openMenu(p, MenuUtils.NPC_MAIN);
- }, 1);
-
- }
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/FixConfigCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/FixConfigCommand.java
deleted file mode 100644
index aa043227..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/FixConfigCommand.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.foxikle.customnpcs.internal.FileManager;
-import dev.foxikle.customnpcs.internal.commands.enums.FixConfigWorldStrategy;
-import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.*;
-import dev.velix.imperat.command.AttachmentMode;
-import org.bukkit.Bukkit;
-import org.bukkit.FluidCollisionMode;
-import org.bukkit.Location;
-import org.bukkit.World;
-import org.bukkit.configuration.ConfigurationSection;
-import org.bukkit.configuration.file.YamlConfiguration;
-import org.bukkit.util.RayTraceResult;
-import org.bukkit.util.Vector;
-
-import java.util.Locale;
-import java.util.Map;
-import java.util.UUID;
-
-@SubCommand(value = "fixconfig", attachment = AttachmentMode.MAIN)
-@Permission("customnpcs.commands.fix_config")
-public class FixConfigCommand {
-
- @Usage
- public void execute(BukkitSource source) {
- Locale locale = CommandUtils.getLocale(source);
- source.reply(Msg.translate(locale, "customnpcs.commands.fix_config.usage"));
- }
-
- @Usage
- public void execute(BukkitSource source,
- @Named("mode") @Suggest({"world"}) String mode,
- @SuggestionProvider("worlds") @Default("world") @Named("world") String world,
- @Suggest({"NONE", "SAFE_LOCATION"}) @Default("NONE") @Named("strategy") String strategy,
- @Suggest({"all"}) @SuggestionProvider("broken_npc") @Default("all") @Named("target") String target
- ) {
- Locale locale = CommandUtils.getLocale(source);
- int totalFixed = 0;
- int movedbyStrategy = 0;
- int failedToFix = 0;
- int nonExistentNpcs = 0;
-
- if (!mode.equalsIgnoreCase("world")) {
- execute(source);
- return;
- }
-
- if (Bukkit.getWorld(world) == null) {
- // invalid world
- source.reply("INVALID_WORLD");
- return;
- }
-
- World w = Bukkit.getWorld(world);
- assert w != null;
-
- FixConfigWorldStrategy strat = FixConfigWorldStrategy.parse(strategy);
- if (strat == null) {
- //invalid strategy
- source.reply("INVALID_STRATEGY");
- return;
- }
-
- CustomNPCs plugin = CustomNPCs.getInstance();
- FileManager fileManager = plugin.getFileManager();
-
-
- if (target.equalsIgnoreCase("all")) {
- // apply it to all NPCs
-
- for (UUID uuid : fileManager.getBrokenNPCs().keySet()) {
- YamlConfiguration yml = fileManager.getNpcYaml();
- ConfigurationSection parent = yml.getConfigurationSection(uuid.toString());
- if (parent == null) {
- nonExistentNpcs++;
- continue;
- }
-
- ConfigurationSection location = parent.getConfigurationSection("location");
-
- Location loc;
- String locString;
-
- //Bukkit's terrible config api didn't wipe the section
- if (location != null) {
- assert location != null : "Location is null";
-
- double x = location.getDouble("x");
- double y = location.getDouble("y");
- double z = location.getDouble("z");
- float pitch = (float) location.getDouble("pitch");
- float yaw = (float) location.getDouble("yaw");
-
- loc = new Location(w, x, y, z, pitch, yaw);
- locString = "(" + x + "," + y + "," + z + ")";
- } else {
- loc = new Location(w, 0, 0, 0, 0, 0);
- locString = "(0, 0, 0)";
- plugin.getLogger().warning("Fixed an NPC whose location data was wiped by Bukkit's configuration API. Its location was set to (0,0,0)");
- source.reply(Msg.translate(locale, "customnpcs.commands.fix_config.bukkit_wiped_data"));
- }
-
-
- if (strat == FixConfigWorldStrategy.SAFE_LOCATION) {
-
- // in a wall
- if (w.getBlockAt(loc).isSolid() || w.getBlockAt(loc.add(0, 1, 0)).isSolid()) {
- RayTraceResult traceResult = w.rayTraceBlocks(loc.add(0, 329 - loc.y(), 0),
- new Vector(0, -1, 0), 320D, FluidCollisionMode.NEVER);
-
- if (traceResult == null) {
- // The location cannot be safe
-
- plugin.getLogger().warning("Failed to fix npc " + uuid + " at " + locString + " -- Location cannot be made safe.");
- failedToFix++;
- continue;
- }
- loc.setY(traceResult.getHitBlock().getY() + 1);
- }
- movedbyStrategy++;
-
- }
-
- parent.set("location", loc);
- totalFixed++;
- fileManager.saveNpcFile(yml);
- }
- } else {
- // find npc by flag
- try {
-
-
- if (!fileManager.getBrokenNPCs().containsValue(target)) {
- // it dont exist
- source.reply(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid "));
- return;
- }
- UUID uuid = null;
- for (Map.Entry entry : fileManager.getBrokenNPCs().entrySet()) {
- if (entry.getValue().equals(target)) {
- uuid = entry.getKey();
- break;
- }
- }
-
- if (uuid == null) {
- source.reply(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid"));
- return;
- }
-
- YamlConfiguration yml = fileManager.getNpcYaml();
- ConfigurationSection parent = yml.getConfigurationSection(uuid.toString());
- if (parent == null) {
- nonExistentNpcs++;
- // hacky hacky way to do this
- throw new RuntimeException("Catch me!");
- }
- ConfigurationSection location = parent.getConfigurationSection("location");
-
-
- Location loc;
- String locString;
-
- //Bukkit's terrible config api didn't wipe the section
- if (location != null) {
- assert location != null : "Location is null";
-
- double x = location.getDouble("x");
- double y = location.getDouble("y");
- double z = location.getDouble("z");
- float pitch = (float) location.getDouble("pitch");
- float yaw = (float) location.getDouble("yaw");
-
- loc = new Location(w, x, y, z, pitch, yaw);
- locString = "(" + x + "," + y + "," + z + ")";
- } else {
- loc = new Location(w, 0, 0, 0, 0, 0);
- locString = "(0, 0, 0)";
- plugin.getLogger().warning("Fixed an NPC whose location data was wiped by Bukkit's configuration API. Its location was set to (0,0,0)");
- source.reply(Msg.translate(locale, "customnpcs.commands.fix_config.bukkit_wiped_data"));
- }
-
-
- if (strat == FixConfigWorldStrategy.SAFE_LOCATION) {
-
- // in a wall
- if (w.getBlockAt(loc).isSolid() || w.getBlockAt(loc.add(0, 1, 0)).isSolid()) {
- RayTraceResult traceResult = w.rayTraceBlocks(loc.add(0, 329 - loc.y(), 0),
- new Vector(0, -1, 0), 320D, FluidCollisionMode.NEVER);
-
- if (traceResult == null) {
- // The location cannot be safe
- plugin.getLogger().warning("Failed to fix npc " + uuid + " at " + locString + " -- Location cannot be made safe.");
- failedToFix++;
- throw new RuntimeException("Catch me!");
- }
- loc.setY(traceResult.getHitBlock().getY() + 1);
- }
- movedbyStrategy++;
-
- }
-
- parent.set("location", loc);
- totalFixed++;
-
- fileManager.saveNpcFile(yml);
- } catch (Exception ignored) {
- }
-
- }
- source.reply(Msg.translate(locale, "customnpcs.commands.fix_config.report", totalFixed, movedbyStrategy, failedToFix, nonExistentNpcs));
- }
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/HelpCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/HelpCommand.java
deleted file mode 100644
index 40fb94ce..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/HelpCommand.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.Description;
-import dev.velix.imperat.annotations.Permission;
-import dev.velix.imperat.annotations.SubCommand;
-import dev.velix.imperat.annotations.Usage;
-import dev.velix.imperat.command.AttachmentMode;
-
-@SubCommand(value = "help", attachment = AttachmentMode.MAIN)
-@Permission("customnpcs.commands.help")
-@Description("Displays a help message")
-public class HelpCommand {
- @Usage
- public void usage(BukkitSource source) {
- source.reply(CommandUtils.getHelpComponent(CommandUtils.getLocale(source)));
- }
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/ListCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/ListCommand.java
deleted file mode 100644
index b5dd3e4c..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/ListCommand.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.Permission;
-import dev.velix.imperat.annotations.SubCommand;
-import dev.velix.imperat.annotations.Usage;
-import dev.velix.imperat.command.AttachmentMode;
-
-import java.util.Locale;
-
-@SubCommand(value = "list", attachment = AttachmentMode.MAIN)
-@Permission("customnpcs.commands.manage")
-public class ListCommand {
-
- @Usage
- public void usage(BukkitSource sender) {
- Locale locale = Locale.getDefault();
- if (!sender.isConsole()) locale = sender.asPlayer().locale();
- sender.reply(CommandUtils.getListComponent(locale));
- }
-
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/ManageCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/ManageCommand.java
deleted file mode 100644
index f2212303..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/ManageCommand.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.Permission;
-import dev.velix.imperat.annotations.SubCommand;
-import dev.velix.imperat.annotations.Usage;
-import dev.velix.imperat.command.AttachmentMode;
-
-import java.util.Locale;
-
-@SubCommand(value = {"manage"}, attachment = AttachmentMode.MAIN)
-@Permission("customnpcs.commands.manage")
-public class ManageCommand {
-
- @Usage
- public void usage(BukkitSource sender) {
- Locale locale = Locale.getDefault();
- if (!sender.isConsole()) locale = sender.asPlayer().locale();
- sender.reply(CommandUtils.getListComponent(locale));
- }
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/MoveCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/MoveCommand.java
deleted file mode 100644
index 376d829a..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/MoveCommand.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.foxikle.customnpcs.internal.interfaces.InternalNpc;
-import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.*;
-import dev.velix.imperat.command.AttachmentMode;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
-
-import java.util.UUID;
-
-@SubCommand(value = "movehere", attachment = AttachmentMode.MAIN)
-@Permission("customnpcs.commands.movehere")
-public class MoveCommand {
-
- @Usage
- public void usage(
- BukkitSource source,
- @Named("npc") @SuggestionProvider("current_npc") @Greedy String npc
- ) {
- if (source.isConsole()) {
- source.reply(Msg.format("You can't do this :P"));
- return;
- }
-
- UUID uuid = CommandUtils.parseNpc(source, npc);
- if (uuid == null) {
- return;
- }
- if (!CommandUtils.checkNpc(source, uuid)) return;
-
- final Player p = source.asPlayer();
- final CustomNPCs plugin = CustomNPCs.getInstance();
- final InternalNpc finalNpc = plugin.getNPCByID(uuid);
-
- p.sendMessage(Msg.translate(p.locale(), "customnpcs.commands.move.nudge"));
- assert finalNpc != null;
- finalNpc.teleport(p.getLocation());
- finalNpc.remove();
- finalNpc.createNPC();
- Bukkit.getOnlinePlayers().forEach(finalNpc::injectPlayer);
- }
-
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommand.java
deleted file mode 100644
index e74a0b69..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommand.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.*;
-
-import java.util.Locale;
-
-@Command("npc")
-@Description("The main CustomNPCs command")
-@Permission("customnpcs.commands.help")
-@Inherit(
- {
- CloneCommand.class, CreateCommand.class, DeleteCommand.class,
- EditCommand.class, FixConfigCommand.class, ListCommand.class, MoveCommand.class,
- ReloadCommand.class, SetsoundCommand.class, TeleportCommand.class,
- WikiCommand.class, HelpCommand.class, ManageCommand.class, DebugCommand.class
- }
-)
-public class NpcCommand {
-
- @Usage
- public void showHelp(BukkitSource sender) {
- Locale locale = Locale.getDefault();
- if (!sender.isConsole()) locale = sender.asPlayer().locale();
- sender.reply(CommandUtils.getHelpComponent(locale));
- }
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommandRegistrar.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommandRegistrar.java
new file mode 100644
index 00000000..a59e468d
--- /dev/null
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/NpcCommandRegistrar.java
@@ -0,0 +1,648 @@
+/*
+ * Copyright (c) 2024-2026. Foxikle
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package dev.foxikle.customnpcs.internal.commands;
+
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.builder.RequiredArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import dev.foxikle.customnpcs.internal.CustomNPCs;
+import dev.foxikle.customnpcs.internal.FileManager;
+import dev.foxikle.customnpcs.internal.commands.suggestion.NpcBrokenSuggester;
+import dev.foxikle.customnpcs.internal.commands.suggestion.NpcSuggester;
+import dev.foxikle.customnpcs.internal.commands.suggestion.SoundSuggester;
+import dev.foxikle.customnpcs.internal.commands.suggestion.WorldSuggester;
+import dev.foxikle.customnpcs.internal.interfaces.InternalNpc;
+import dev.foxikle.customnpcs.internal.menu.MenuUtils;
+import dev.foxikle.customnpcs.internal.utils.Msg;
+import dev.foxikle.customnpcs.internal.utils.WaitingType;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.event.HoverEvent;
+import org.bukkit.*;
+import org.bukkit.command.CommandSender;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.entity.Player;
+import org.bukkit.util.RayTraceResult;
+import org.bukkit.util.Vector;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.mojang.brigadier.arguments.StringArgumentType.greedyString;
+import static com.mojang.brigadier.arguments.StringArgumentType.word;
+
+@SuppressWarnings("UnstableApiUsage")
+public class NpcCommandRegistrar {
+
+ private static final String PERMISSION_CREATE = "customnpcs.create";
+ private static final String PERMISSION_EDIT = "customnpcs.edit";
+ private static final String PERMISSION_DELETE = "customnpcs.delete";
+ private static final String PERMISSION_CLONE = "customnpcs.commands.clone";
+ private static final String PERMISSION_MOVEHERE = "customnpcs.commands.movehere";
+ private static final String PERMISSION_GOTO = "customnpcs.commands.goto";
+ private static final String PERMISSION_MANAGE = "customnpcs.commands.manage";
+ private static final String PERMISSION_HELP = "customnpcs.commands.help";
+ private static final String PERMISSION_RELOAD = "customnpcs.commands.reload";
+ private static final String PERMISSION_WIKI = "customnpcs.command.wiki";
+ private static final String PERMISSION_DEBUG = "customnpcs.edit";
+ private static final String PERMISSION_FIXCONFIG = "customnpcs.commands.fix_config";
+ private static final String PERMISSION_SETSOUND = "customnpcs.edit";
+
+
+ public static LiteralCommandNode buildNode() {
+ LiteralCommandNode npcNode = LiteralArgumentBuilder.literal("npc")
+ .requires(source -> source.getSender().hasPermission(PERMISSION_HELP))
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ Locale locale = getLocale(sender);
+ sender.sendMessage(CommandUtils.getHelpComponent(locale));
+ return 1;
+ })
+ .build();
+
+ registerCreateCommand(npcNode);
+ registerEditCommand(npcNode);
+ registerDeleteCommand(npcNode);
+ registerCloneCommand(npcNode);
+ registerMoveHereCommand(npcNode);
+ registerGotoCommand(npcNode);
+ registerManageCommand(npcNode);
+ registerListCommand(npcNode);
+ registerHelpCommand(npcNode);
+ registerReloadCommand(npcNode);
+ registerWikiCommand(npcNode);
+ registerDebugCommand(npcNode);
+ registerFixConfigCommand(npcNode);
+ registerSetsoundCommand(npcNode);
+
+ return npcNode;
+ }
+
+ private static void registerCreateCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode createNode = LiteralArgumentBuilder.literal("create")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_CREATE))
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Msg.format("You can't do this :P"));
+ return 0;
+ }
+ UUID uuid = UUID.randomUUID();
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ InternalNpc npc = plugin.createNPC(
+ player.getWorld(),
+ player.getLocation(),
+ new dev.foxikle.customnpcs.data.Equipment(),
+ new dev.foxikle.customnpcs.data.Settings(),
+ uuid,
+ null,
+ new java.util.ArrayList<>()
+ );
+ plugin.getEditingNPCs().put(player.getUniqueId(), npc);
+ plugin.getLotus().openMenu(player, MenuUtils.NPC_MAIN);
+ return 1;
+ })
+ .build();
+ npcNode.addChild(createNode);
+ }
+
+ private static void registerEditCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode editNode = LiteralArgumentBuilder.literal("edit")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_EDIT))
+ .then(RequiredArgumentBuilder.argument("npc", greedyString())
+ .suggests(NpcSuggester.SUGGESTIONS)
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ String npcArg = StringArgumentType.getString(context, "npc");
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Msg.format("You can't do this :P"));
+ return 0;
+ }
+ UUID uuid = CommandUtils.parseNpc(player, npcArg);
+ if (uuid == null) {
+ return 0;
+ }
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ InternalNpc finalNpc = plugin.getNPCByID(uuid);
+ Bukkit.getScheduler().runTaskLater(plugin, () -> {
+ InternalNpc newNpc = plugin.createNPC(
+ player.getWorld(),
+ finalNpc.getSpawnLoc(),
+ finalNpc.getEquipment(),
+ finalNpc.getSettings(),
+ finalNpc.getUniqueID(),
+ null,
+ finalNpc.getActions()
+ );
+ plugin.getEditingNPCs().put(player.getUniqueId(), newNpc);
+ plugin.getLotus().openMenu(player, MenuUtils.NPC_MAIN);
+ }, 1);
+ return 1;
+ })
+ .build())
+ .build();
+ npcNode.addChild(editNode);
+ }
+
+ private static void registerDeleteCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode deleteNode = LiteralArgumentBuilder.literal("delete")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_DELETE))
+ .then(RequiredArgumentBuilder.argument("npc", greedyString())
+ .suggests(NpcSuggester.SUGGESTIONS)
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ String npcArg = StringArgumentType.getString(context, "npc");
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Msg.format("You can't do this :P"));
+ return 0;
+ }
+ UUID uuid = CommandUtils.parseNpc(player, npcArg);
+ if (uuid == null) {
+ return 0;
+ }
+ if (!CommandUtils.checkNpc(player, uuid)) {
+ return 0;
+ }
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ InternalNpc finalNpc = plugin.getNPCByID(uuid);
+ plugin.getEditingNPCs().put(player.getUniqueId(), finalNpc);
+ plugin.getDeletionReason().put(player.getUniqueId(), false);
+ plugin.getLotus().openMenu(player, MenuUtils.NPC_DELETE);
+ return 1;
+ })
+ .build())
+ .build();
+ npcNode.addChild(deleteNode);
+ }
+
+ private static void registerCloneCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode cloneNode = LiteralArgumentBuilder.literal("clone")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_CLONE))
+ .then(RequiredArgumentBuilder.argument("npc", greedyString())
+ .suggests(NpcSuggester.SUGGESTIONS)
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ String npcArg = StringArgumentType.getString(context, "npc");
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Msg.format("You can't do this :P"));
+ return 0;
+ }
+ UUID uuid = CommandUtils.parseNpc(player, npcArg);
+ if (uuid == null) {
+ return 0;
+ }
+ if (!CommandUtils.checkNpc(player, uuid)) {
+ return 0;
+ }
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ InternalNpc finalNpc = plugin.getNPCByID(uuid);
+ InternalNpc newNpc = finalNpc.clone();
+ newNpc.setSpawnLoc(player.getLocation());
+ newNpc.getSettings().setDirection(player.getLocation().getYaw());
+ newNpc.createNPC();
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.commands.clone.success"));
+ return 1;
+ })
+ .build())
+ .build();
+ npcNode.addChild(cloneNode);
+ }
+
+ private static void registerMoveHereCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode movehereNode = LiteralArgumentBuilder.literal(
+ "movehere")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_MOVEHERE))
+ .then(RequiredArgumentBuilder.argument("npc", greedyString())
+ .suggests(NpcSuggester.SUGGESTIONS)
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ String npcArg = StringArgumentType.getString(context, "npc");
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Msg.format("You can't do this :P"));
+ return 0;
+ }
+ UUID uuid = CommandUtils.parseNpc(player, npcArg);
+ if (uuid == null) {
+ return 0;
+ }
+ if (!CommandUtils.checkNpc(player, uuid)) {
+ return 0;
+ }
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ InternalNpc finalNpc = plugin.getNPCByID(uuid);
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.commands.move.nudge"));
+ finalNpc.teleport(player.getLocation());
+ finalNpc.remove();
+ finalNpc.createNPC();
+ Bukkit.getOnlinePlayers().forEach(finalNpc::injectPlayer);
+ return 1;
+ })
+ .build())
+ .build();
+ npcNode.addChild(movehereNode);
+ }
+
+ private static void registerGotoCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode gotoNode = LiteralArgumentBuilder.literal("goto")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_GOTO))
+ .then(RequiredArgumentBuilder.argument("npc", greedyString())
+ .suggests(NpcSuggester.SUGGESTIONS)
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ String npcArg = StringArgumentType.getString(context, "npc");
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage(Msg.format("You can't do this :P"));
+ return 0;
+ }
+ UUID uuid = CommandUtils.parseNpc(player, npcArg);
+ if (uuid == null) {
+ return 0;
+ }
+ if (!CommandUtils.checkNpc(player, uuid)) {
+ return 0;
+ }
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ InternalNpc finalNpc = plugin.getNPCByID(uuid);
+ player.teleportAsync(finalNpc.getCurrentLocation());
+ return 1;
+ })
+ .build())
+ .build();
+ npcNode.addChild(gotoNode);
+ }
+
+ private static void registerManageCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode manageNode = LiteralArgumentBuilder.literal("manage")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_MANAGE))
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ Locale locale = getLocale(sender);
+ sender.sendMessage(CommandUtils.getListComponent(locale));
+ return 1;
+ })
+ .build();
+ npcNode.addChild(manageNode);
+ }
+
+ private static void registerListCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode listNode = LiteralArgumentBuilder.literal("list")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_MANAGE))
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ Locale locale = getLocale(sender);
+ sender.sendMessage(CommandUtils.getListComponent(locale));
+ return 1;
+ })
+ .build();
+ npcNode.addChild(listNode);
+ }
+
+ private static void registerHelpCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode helpNode = LiteralArgumentBuilder.literal("help")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_HELP))
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ sender.sendMessage(CommandUtils.getHelpComponent(getLocale(sender)));
+ return 1;
+ })
+ .build();
+ npcNode.addChild(helpNode);
+ }
+
+ private static void registerReloadCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode reloadNode = LiteralArgumentBuilder.literal("reload")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_RELOAD))
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ plugin.setReloading(true);
+ Locale locale = getLocale(sender);
+ sender.sendMessage(Msg.translate(locale, "customnpcs.commands.reload.start"));
+ plugin.reloadConfig();
+ plugin.onDisable();
+ plugin.onEnable();
+ plugin.setReloading(false);
+ sender.sendMessage(Msg.translate(locale, "customnpcs.commands.reload.end"));
+ return 1;
+ })
+ .build();
+ npcNode.addChild(reloadNode);
+ }
+
+ private static void registerWikiCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode wikiNode = LiteralArgumentBuilder.literal("wiki")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_WIKI))
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ Locale locale = getLocale(sender);
+ sender.sendMessage(
+ Msg.translate(locale, "customnpcs.commands.wiki")
+ .clickEvent(ClickEvent.openUrl("https://docs.foxikle.dev"))
+ .appendSpace()
+ .hoverEvent(HoverEvent.showText(Msg.translate(locale, "customnpcs.commands.wiki" +
+ ".hover")))
+ );
+ return 1;
+ })
+ .build();
+ npcNode.addChild(wikiNode);
+ }
+
+ private static void registerDebugCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode debugNode = LiteralArgumentBuilder.literal("debug")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_DEBUG))
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ if (plugin.isDebug()) {
+ sender.sendMessage(Msg.translate(getLocale(sender), "customnpcs.commands.debug.message" +
+ ".disable"));
+ plugin.setDebug(false);
+ } else {
+ sender.sendMessage(Msg.translate(getLocale(sender), "customnpcs.commands.debug.message" +
+ ".enable"));
+ plugin.setDebug(true);
+ }
+ return 1;
+ })
+ .build();
+ npcNode.addChild(debugNode);
+ }
+
+ private static void registerFixConfigCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode fixconfigNode = LiteralArgumentBuilder.literal(
+ "fixconfig")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_FIXCONFIG))
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ Locale locale = getLocale(sender);
+ sender.sendMessage(Msg.translate(locale, "customnpcs.commands.fix_config.usage"));
+ return 0;
+ })
+ .then(LiteralArgumentBuilder.literal("world")
+ .executes(ctx -> executeFixConfig(ctx))
+ .then(RequiredArgumentBuilder.argument("world", word())
+ .suggests(WorldSuggester.SUGGESTIONS)
+ .executes(ctx -> executeFixConfig(ctx))
+ .then(RequiredArgumentBuilder.argument("strategy", word())
+ .suggests((ctx, builder) -> {
+ builder.suggest("NONE");
+ builder.suggest("SAFE_LOCATION");
+ return builder.buildFuture();
+ })
+ .executes(ctx -> executeFixConfig(ctx))
+ .then(RequiredArgumentBuilder.argument("target",
+ greedyString())
+ .suggests(NpcBrokenSuggester.SUGGESTIONS)
+ .executes(ctx -> executeFixConfig(ctx))
+ )
+ )
+ )
+ )
+ .build();
+ npcNode.addChild(fixconfigNode);
+ }
+
+ private static void registerSetsoundCommand(LiteralCommandNode npcNode) {
+ LiteralCommandNode setsoundNode = LiteralArgumentBuilder.literal(
+ "setsound")
+ .requires(sender -> sender.getSender().hasPermission(PERMISSION_SETSOUND))
+ .then(RequiredArgumentBuilder.argument("sound", greedyString())
+ .suggests(SoundSuggester.SUGGESTIONS)
+ .executes(context -> {
+ CommandSender sender = context.getSource().getSender();
+ if (!(sender instanceof Player player)) {
+ sender.sendMessage("You can't do this :P");
+ return 0;
+ }
+ String soundRaw = StringArgumentType.getString(context, "sound");
+ CustomNPCs plugin = CustomNPCs.getInstance();
+
+ if (plugin.isWaiting(player, WaitingType.SOUND)) {
+ String formatted = soundRaw.trim().toLowerCase();
+ if (Registry.SOUNDS.get(NamespacedKey.fromString(formatted)) == null) {
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.commands.setsound" +
+ ".unknown_sound"));
+ }
+
+ Bukkit.getScheduler().runTask(plugin, () -> {
+ plugin.waiting.remove(player.getUniqueId());
+ dev.foxikle.customnpcs.actions.Action actionImpl =
+ plugin.editingActions.get(player.getUniqueId());
+ if (actionImpl instanceof dev.foxikle.customnpcs.actions.defaultImpl.PlaySound action) {
+ action.setSound(formatted);
+ } else {
+ throw new IllegalArgumentException("Action " + actionImpl.getClass().getName() + " is not of type PlaySound");
+ }
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.commands.setsound" +
+ ".success", Component.text(formatted)));
+ plugin.getLotus().openMenu(player, actionImpl.getMenu());
+ });
+ } else {
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.commands.setsound" +
+ ".was_not_waiting"));
+ }
+ return 1;
+ })
+ .build())
+ .build();
+ npcNode.addChild(setsoundNode);
+ }
+
+ private static int executeFixConfig(CommandContext context) {
+ CommandSender sender = context.getSource().getSender();
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ FileManager fileManager = plugin.getFileManager();
+ Locale locale = getLocale(sender);
+
+ String worldArg = "";
+ String strategyArg = "";
+ String targetArg = "";
+
+ try {
+ worldArg = StringArgumentType.getString(context, "world");
+ } catch (Exception ignored) {
+ }
+
+ try {
+ strategyArg = StringArgumentType.getString(context, "strategy");
+ } catch (Exception ignored) {
+ }
+
+ try {
+ targetArg = StringArgumentType.getString(context, "target");
+ } catch (Exception ignored) {
+ }
+
+ if (worldArg.isEmpty()) {
+ sender.sendMessage(Msg.translate(locale, "customnpcs.commands.fix_config.usage"));
+ return 0;
+ }
+
+ if (Bukkit.getWorld(worldArg) == null) {
+ sender.sendMessage("INVALID_WORLD");
+ return 0;
+ }
+
+ World w = Bukkit.getWorld(worldArg);
+ assert w != null;
+
+ dev.foxikle.customnpcs.internal.commands.enums.FixConfigWorldStrategy strat =
+ dev.foxikle.customnpcs.internal.commands.enums.FixConfigWorldStrategy.parse(strategyArg);
+ if (strat == null) {
+ sender.sendMessage("INVALID_STRATEGY");
+ return 0;
+ }
+
+ AtomicInteger totalFixed = new AtomicInteger(0);
+ AtomicInteger movedbyStrategy = new AtomicInteger(0);
+ AtomicInteger failedToFix = new AtomicInteger(0);
+ AtomicInteger nonExistentNpcs = new AtomicInteger(0);
+
+ if (targetArg.isEmpty() || targetArg.equalsIgnoreCase("all")) {
+ for (UUID uuid : fileManager.getBrokenNPCs().keySet()) {
+ YamlConfiguration yml = fileManager.getNpcYaml();
+ ConfigurationSection parent = yml.getConfigurationSection(uuid.toString());
+ if (parent == null) {
+ nonExistentNpcs.incrementAndGet();
+ continue;
+ }
+
+ ConfigurationSection location = parent.getConfigurationSection("location");
+ Location loc;
+ String locString;
+
+ if (location != null) {
+ double x = location.getDouble("x");
+ double y = location.getDouble("y");
+ double z = location.getDouble("z");
+ float pitch = (float) location.getDouble("pitch");
+ float yaw = (float) location.getDouble("yaw");
+ loc = new Location(w, x, y, z, pitch, yaw);
+ locString = "(" + x + "," + y + "," + z + ")";
+ } else {
+ loc = new Location(w, 0, 0, 0, 0, 0);
+ locString = "(0, 0, 0)";
+ plugin.getLogger().warning("Fixed an NPC whose location data was wiped by Bukkit's configuration " +
+ "API. Its location was set to (0,0,0)");
+ sender.sendMessage(Msg.translate(locale, "customnpcs.commands.fix_config.bukkit_wiped_data"));
+ }
+
+ if (strat == dev.foxikle.customnpcs.internal.commands.enums.FixConfigWorldStrategy.SAFE_LOCATION) {
+ if (w.getBlockAt(loc).isSolid() || w.getBlockAt(loc.add(0, 1, 0)).isSolid()) {
+ RayTraceResult traceResult = w.rayTraceBlocks(loc.add(0, 329 - loc.y(), 0),
+ new Vector(0, -1, 0), 320D, FluidCollisionMode.NEVER);
+
+ if (traceResult == null) {
+ plugin.getLogger().warning("Failed to fix npc " + uuid + " at " + locString + " -- " +
+ "Location cannot be made safe.");
+ failedToFix.incrementAndGet();
+ continue;
+ }
+ loc.setY(traceResult.getHitBlock().getY() + 1);
+ }
+ movedbyStrategy.incrementAndGet();
+ }
+
+ parent.set("location", loc);
+ totalFixed.incrementAndGet();
+ fileManager.saveNpcFile(yml);
+ }
+ } else {
+ UUID uuid = null;
+ for (Map.Entry entry : fileManager.getBrokenNPCs().entrySet()) {
+ if (entry.getValue().equals(targetArg)) {
+ uuid = entry.getKey();
+ break;
+ }
+ }
+
+ if (uuid == null) {
+ sender.sendMessage(Msg.translate(locale, "customnpcs.commands.invalid_name_or_uuid"));
+ return 0;
+ }
+
+ YamlConfiguration yml = fileManager.getNpcYaml();
+ ConfigurationSection parent = yml.getConfigurationSection(uuid.toString());
+ if (parent == null) {
+ return 0;
+ }
+ ConfigurationSection location = parent.getConfigurationSection("location");
+ Location loc;
+ String locString;
+
+ if (location != null) {
+ double x = location.getDouble("x");
+ double y = location.getDouble("y");
+ double z = location.getDouble("z");
+ float pitch = (float) location.getDouble("pitch");
+ float yaw = (float) location.getDouble("yaw");
+ loc = new Location(w, x, y, z, pitch, yaw);
+ locString = "(" + x + "," + y + "," + z + ")";
+ } else {
+ loc = new Location(w, 0, 0, 0, 0, 0);
+ locString = "(0, 0, 0)";
+ plugin.getLogger().warning("Fixed an NPC whose location data was wiped by Bukkit's configuration API." +
+ " Its location was set to (0,0,0)");
+ sender.sendMessage(Msg.translate(locale, "customnpcs.commands.fix_config.bukkit_wiped_data"));
+ }
+
+ if (strat == dev.foxikle.customnpcs.internal.commands.enums.FixConfigWorldStrategy.SAFE_LOCATION) {
+ if (w.getBlockAt(loc).isSolid() || w.getBlockAt(loc.add(0, 1, 0)).isSolid()) {
+ RayTraceResult traceResult = w.rayTraceBlocks(loc.add(0, 329 - loc.y(), 0),
+ new Vector(0, -1, 0), 320D, FluidCollisionMode.NEVER);
+
+ if (traceResult == null) {
+ plugin.getLogger().warning("Failed to fix npc " + uuid + " at " + locString + " -- Location " +
+ "cannot be made safe.");
+ failedToFix.incrementAndGet();
+ return 0;
+ }
+ loc.setY(traceResult.getHitBlock().getY() + 1);
+ }
+ movedbyStrategy.incrementAndGet();
+ }
+
+ parent.set("location", loc);
+ totalFixed.incrementAndGet();
+ fileManager.saveNpcFile(yml);
+ }
+
+ sender.sendMessage(Msg.translate(locale, "customnpcs.commands.fix_config.report",
+ totalFixed.get(), movedbyStrategy.get(), failedToFix.get(), nonExistentNpcs.get()));
+ return 1;
+ }
+
+ private static Locale getLocale(CommandSender sender) {
+ if (sender instanceof Player player) {
+ return player.locale();
+ }
+ return Locale.getDefault();
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/ReloadCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/ReloadCommand.java
deleted file mode 100644
index 1a21ff41..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/ReloadCommand.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.Description;
-import dev.velix.imperat.annotations.Permission;
-import dev.velix.imperat.annotations.SubCommand;
-import dev.velix.imperat.annotations.Usage;
-import dev.velix.imperat.command.AttachmentMode;
-
-import java.util.Locale;
-
-@SubCommand(value = "reload", attachment = AttachmentMode.MAIN)
-@Permission("customnpcs.commands.reload")
-@Description("Reloads the CustomNPCs and its configuration.")
-public class ReloadCommand {
-
- @Usage
- public void usage(BukkitSource source) {
- final CustomNPCs plugin = CustomNPCs.getInstance();
- plugin.setReloading(true);
- Locale locale = Locale.getDefault();
- if (!source.isConsole()) source.asPlayer().locale();
- source.reply(Msg.translate(locale, "customnpcs.commands.reload.start"));
-
- plugin.reloadConfig();
- plugin.onDisable();
- plugin.onEnable();
- plugin.setReloading(false);
-
- source.reply(Msg.translate(locale, "customnpcs.commands.reload.end"));
- }
-
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/SetsoundCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/SetsoundCommand.java
deleted file mode 100644
index c4546964..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/SetsoundCommand.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.foxikle.customnpcs.actions.Action;
-import dev.foxikle.customnpcs.actions.defaultImpl.PlaySound;
-import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.foxikle.customnpcs.internal.utils.WaitingType;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.*;
-import dev.velix.imperat.command.AttachmentMode;
-import net.kyori.adventure.text.Component;
-import org.bukkit.Bukkit;
-import org.bukkit.NamespacedKey;
-import org.bukkit.Registry;
-import org.bukkit.entity.Player;
-
-@SubCommand(value = "setsound", attachment = AttachmentMode.MAIN)
-@Permission("customnpcs.edit")
-@Description("Sets the sound of the playsound action!")
-public class SetsoundCommand {
-
- @Usage
- public void usage(
- BukkitSource source,
- @Default("minecraft:ui.button.click") @Named("sound") @SuggestionProvider("sound") @Greedy String soundRaw) {
-
- if (source.isConsole()) {
- source.reply("You can't do this :P");
- return;
- }
-
- final Player p = source.asPlayer();
- final CustomNPCs plugin = CustomNPCs.getInstance();
- // converts `ui button click` to `UI_BUTTON_CLICK`, like {@link Sound#UI_BUTTON_CLICK}
- String formatted = soundRaw.trim().toLowerCase();
-
- if (plugin.isWaiting(p, WaitingType.SOUND)) {
- if (Registry.SOUNDS.get(NamespacedKey.fromString(formatted)) == null) {
- p.sendMessage(Msg.translate(p.locale(), "customnpcs.commands.setsound.unknown_sound"));
- }
-
- Bukkit.getScheduler().runTask(plugin, () -> {
- plugin.waiting.remove(p.getUniqueId());
- Action actionImpl = plugin.editingActions.get(p.getUniqueId());
- if (actionImpl instanceof PlaySound action) {
- action.setSound(formatted);
- } else
- throw new IllegalArgumentException("Action " + actionImpl.getClass().getName() + " is not of type PlaySound");
- p.sendMessage(Msg.translate(p.locale(), "customnpcs.commands.setsound.success", Component.text(formatted)));
- plugin.getLotus().openMenu(p, actionImpl.getMenu());
- });
- } else {
- p.sendMessage(Msg.translate(p.locale(), "customnpcs.commands.setsound.was_not_waiting"));
- }
-
- }
-
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/TeleportCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/TeleportCommand.java
deleted file mode 100644
index 2b616030..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/TeleportCommand.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.foxikle.customnpcs.internal.interfaces.InternalNpc;
-import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.*;
-import dev.velix.imperat.command.AttachmentMode;
-import org.bukkit.entity.Player;
-
-import java.util.UUID;
-
-@SubCommand(value = "goto", attachment = AttachmentMode.MAIN)
-@Permission("customnpcs.commands.goto")
-@Description("Teleports you to the specified NPC")
-public class TeleportCommand {
- @Usage
- public void usage(
- BukkitSource source,
- @Named("npc") @SuggestionProvider("current_npc") @Greedy String npc
- ) {
- if (source.isConsole()) {
- source.reply(Msg.format("You can't do this :P"));
- return;
- }
-
- UUID uuid = CommandUtils.parseNpc(source, npc);
- if (uuid == null) {
- return;
- }
- if (!CommandUtils.checkNpc(source, uuid)) return;
-
- final Player p = source.asPlayer();
- final CustomNPCs plugin = CustomNPCs.getInstance();
- final InternalNpc finalNpc = plugin.getNPCByID(uuid);
-
- assert finalNpc != null;
- p.teleportAsync(finalNpc.getCurrentLocation());
- }
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/WikiCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/WikiCommand.java
deleted file mode 100644
index ea32b757..00000000
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/WikiCommand.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (c) 2024-2025. Foxikle
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package dev.foxikle.customnpcs.internal.commands;
-
-import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.Description;
-import dev.velix.imperat.annotations.Permission;
-import dev.velix.imperat.annotations.SubCommand;
-import dev.velix.imperat.annotations.Usage;
-import dev.velix.imperat.command.AttachmentMode;
-import net.kyori.adventure.text.event.ClickEvent;
-import net.kyori.adventure.text.event.HoverEvent;
-
-import java.util.Locale;
-
-@Permission("customnpcs.command.wiki")
-@SubCommand(value = "wiki", attachment = AttachmentMode.MAIN)
-@Description("Supplies a link to the plugin's documentation")
-public class WikiCommand {
-
- @Usage
- public void usage(BukkitSource source) {
- Locale locale = Locale.getDefault();
- if (!source.isConsole()) source.asPlayer().locale();
- source.reply(Msg.translate(locale, "customnpcs.commands.wiki")
- .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://docs.foxikle.dev"))
- .appendSpace().hoverEvent(HoverEvent.showText(Msg.translate(locale, "customnpcs.commands.wiki.hover"))));
- }
-
-}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/enums/FixConfigWorldStrategy.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/enums/FixConfigWorldStrategy.java
index 3b2fe3bd..47c6816b 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/enums/FixConfigWorldStrategy.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/enums/FixConfigWorldStrategy.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcBrokenSuggester.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcBrokenSuggester.java
index 260dc2c8..af31357e 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcBrokenSuggester.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcBrokenSuggester.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025. Foxikle
+ * Copyright (c) 2025-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,24 +22,21 @@
package dev.foxikle.customnpcs.internal.commands.suggestion;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.foxikle.customnpcs.internal.FileManager;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.command.parameters.CommandParameter;
-import dev.velix.imperat.context.SuggestionContext;
-import dev.velix.imperat.resolvers.SuggestionResolver;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
-import java.util.List;
-import java.util.UUID;
+public class NpcBrokenSuggester {
-public class NpcBrokenSuggester implements SuggestionResolver {
- /**
- * {@inheritDoc}
- */
- @Override
- public List autoComplete(SuggestionContext context, CommandParameter parameter) {
- final CustomNPCs plugin = CustomNPCs.getInstance();
- FileManager fileManager = plugin.getFileManager();
- return fileManager.getBrokenNPCs().keySet().stream().map(UUID::toString).toList();
- }
+ public static final SuggestionProvider SUGGESTIONS = (context, builder) -> {
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ String input = builder.getRemaining().toLowerCase();
+
+ plugin.getFileManager().getBrokenNPCs().keySet().stream()
+ .map(uuid -> uuid.toString())
+ .filter(uuid -> uuid.toLowerCase().startsWith(input))
+ .forEach(builder::suggest);
+
+ return builder.buildFuture();
+ };
}
\ No newline at end of file
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcSuggester.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcSuggester.java
index a2e4b303..80d8369c 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcSuggester.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/NpcSuggester.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,21 +22,21 @@
package dev.foxikle.customnpcs.internal.commands.suggestion;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
import dev.foxikle.customnpcs.internal.CustomNPCs;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.command.parameters.CommandParameter;
-import dev.velix.imperat.context.SuggestionContext;
-import dev.velix.imperat.resolvers.SuggestionResolver;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
-import java.util.List;
+public class NpcSuggester {
-public class NpcSuggester implements SuggestionResolver {
- /**
- * {@inheritDoc}
- */
- @Override
- public List autoComplete(SuggestionContext context, CommandParameter parameter) {
- final CustomNPCs plugin = CustomNPCs.getInstance();
- return plugin.npcs.values().stream().map(npc -> plugin.getMiniMessage().stripTags(npc.getSettings().getName())).toList();
- }
-}
+ public static final SuggestionProvider SUGGESTIONS = (context, builder) -> {
+ CustomNPCs plugin = CustomNPCs.getInstance();
+ String input = builder.getRemaining().toLowerCase();
+
+ plugin.npcs.values().stream()
+ .map(npc -> plugin.getMiniMessage().stripTags(npc.getSettings().getName()))
+ .filter(name -> name.toLowerCase().startsWith(input))
+ .forEach(builder::suggest);
+
+ return builder.buildFuture();
+ };
+}
\ No newline at end of file
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/SoundSuggester.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/SoundSuggester.java
index 375b1a13..31e4fcd0 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/SoundSuggester.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/SoundSuggester.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,40 +22,29 @@
package dev.foxikle.customnpcs.internal.commands.suggestion;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.command.parameters.CommandParameter;
-import dev.velix.imperat.context.SuggestionContext;
-import dev.velix.imperat.resolvers.SuggestionResolver;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.Keyed;
import org.bukkit.Registry;
-import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
-@SuppressWarnings("unchecked")
-public class SoundSuggester implements SuggestionResolver {
+public class SoundSuggester {
- private static final List SUGGESTIONS = new ArrayList<>();
+ private static final List CACHED_SUGGESTIONS = new ArrayList<>();
static {
- // use reflection because why not
- try {
- Class> clazz = Class.forName("org.bukkit.Registry");
- Field registry = clazz.getField("SOUNDS");
- registry.setAccessible(true);
- Registry val = (Registry) registry.get(null);
- val.forEach(registry1 -> SUGGESTIONS.add(registry1.key().toString()));
- } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
- throw new RuntimeException(e);
+ for (Keyed keyed : Registry.SOUNDS) {
+ CACHED_SUGGESTIONS.add(keyed.key().toString());
}
}
- /**
- * {@inheritDoc}
- */
- @Override
- public List autoComplete(SuggestionContext context, CommandParameter parameter) {
- return SUGGESTIONS;
- }
-}
+ public static final SuggestionProvider SUGGESTIONS = (context, builder) -> {
+ String input = builder.getRemaining().toLowerCase();
+ CACHED_SUGGESTIONS.stream()
+ .filter(sound -> sound.toLowerCase().startsWith(input))
+ .forEach(builder::suggest);
+ return builder.buildFuture();
+ };
+}
\ No newline at end of file
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/WorldSuggester.java b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/WorldSuggester.java
index 8f7f1430..d06ed479 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/WorldSuggester.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/commands/suggestion/WorldSuggester.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,23 +22,19 @@
package dev.foxikle.customnpcs.internal.commands.suggestion;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.command.parameters.CommandParameter;
-import dev.velix.imperat.context.SuggestionContext;
-import dev.velix.imperat.resolvers.SuggestionResolver;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
import org.bukkit.Bukkit;
import org.bukkit.World;
-import java.util.List;
+public class WorldSuggester {
-public class WorldSuggester implements SuggestionResolver {
- /**
- * @param context the context for suggestions
- * @param parameter the parameter of the value to complete
- * @return the auto-completed suggestions of the current argument
- */
- @Override
- public List autoComplete(SuggestionContext context, CommandParameter parameter) {
- return Bukkit.getWorlds().stream().map(World::getName).toList();
- }
-}
+ public static final SuggestionProvider SUGGESTIONS = (context, builder) -> {
+ String input = builder.getRemaining().toLowerCase();
+ Bukkit.getWorlds().stream()
+ .map(World::getName)
+ .filter(name -> name.toLowerCase().startsWith(input))
+ .forEach(builder::suggest);
+ return builder.buildFuture();
+ };
+}
\ No newline at end of file
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/interfaces/InternalNpc.java b/core/src/main/java/dev/foxikle/customnpcs/internal/interfaces/InternalNpc.java
index eef23b32..8c1aee2f 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/interfaces/InternalNpc.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/interfaces/InternalNpc.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/listeners/Listeners.java b/core/src/main/java/dev/foxikle/customnpcs/internal/listeners/Listeners.java
index 50087feb..af066a5d 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/listeners/Listeners.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/listeners/Listeners.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,8 +22,8 @@
package dev.foxikle.customnpcs.internal.listeners;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
+import com.destroystokyo.paper.profile.PlayerProfile;
+import com.destroystokyo.paper.profile.ProfileProperty;
import dev.foxikle.customnpcs.actions.Action;
import dev.foxikle.customnpcs.actions.conditions.Condition;
import dev.foxikle.customnpcs.actions.defaultImpl.*;
@@ -41,6 +41,7 @@
import io.papermc.paper.event.world.WorldGameRuleChangeEvent;
import lombok.Getter;
import lombok.Setter;
+import me.clip.placeholderapi.PlaceholderAPIPlugin;
import org.bukkit.*;
import org.bukkit.block.BlockFace;
import org.bukkit.command.ConsoleCommandSender;
@@ -53,9 +54,11 @@
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.*;
+import org.bukkit.event.server.ServerLoadEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.util.Vector;
import org.mineskin.data.CodeAndMessage;
@@ -64,7 +67,6 @@
import org.mineskin.request.GenerateRequest;
import org.mineskin.response.MineSkinResponse;
-import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;
import java.util.concurrent.*;
@@ -114,15 +116,18 @@ public Listeners(CustomNPCs plugin) {
}
public void start() {
- Bukkit.getWorlds().forEach(world -> worldSleepingPercentages.put(world.getUID(), world.getGameRuleValue(GameRule.PLAYERS_SLEEPING_PERCENTAGE)));
+ Bukkit.getWorlds().forEach(world -> worldSleepingPercentages.put(world.getUID(),
+ world.getGameRuleValue(GameRule.PLAYERS_SLEEPING_PERCENTAGE)));
service = Executors.newSingleThreadScheduledExecutor();
- service.scheduleAtFixedRate(() -> Bukkit.getOnlinePlayers().forEach(this::actionPlayerMovement), 1000, plugin.getConfig().getInt("LookInterval") * 50L, TimeUnit.MILLISECONDS);
+ service.scheduleAtFixedRate(() -> Bukkit.getOnlinePlayers().forEach(this::actionPlayerMovement), 1000,
+ plugin.getConfig().getInt("LookInterval") * 50L, TimeUnit.MILLISECONDS);
}
public void stop() {
service.shutdown();
- Bukkit.getWorlds().forEach(world -> world.setGameRule(GameRule.PLAYERS_SLEEPING_PERCENTAGE, worldSleepingPercentages.get(world.getUID())));
+ Bukkit.getWorlds().forEach(world -> world.setGameRule(GameRule.PLAYERS_SLEEPING_PERCENTAGE,
+ worldSleepingPercentages.get(world.getUID())));
CompletableFuture.runAsync(() -> {
try {
if (!service.awaitTermination(2, TimeUnit.SECONDS)) {
@@ -152,7 +157,8 @@ private void actionPlayerMovement(Player player) {
}
}
- private void processPlayerMovement(final Player player, final InternalNpc npc, final World world, final World npcWorld, final Location location, final UUID uuid) {
+ private void processPlayerMovement(final Player player, final InternalNpc npc, final World world,
+ final World npcWorld, final Location location, final UUID uuid) {
if (player.getGameMode() == GameMode.SPECTATOR) return; // we don't care about spectators
final Location npcLocation = npc.getCurrentLocation();
MovementData oldMovementData; // difference in order of initialization in if/else statement
@@ -233,6 +239,16 @@ public void onPlayerInteract(PlayerInteractEntityEvent e) {
}
}
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onPlayerMove(PlayerMoveEvent e) {
+ Player player = e.getPlayer();
+ if (plugin.isWaiting(player, WaitingType.RECORDING)) {
+ FollowPresetPathAction.recordMovement(player, e.getTo());
+ return;
+ }
+ actionPlayerMovement(player);
+ }
+
/**
* The handler for text input
*
@@ -244,8 +260,33 @@ public void onPlayerInteract(PlayerInteractEntityEvent e) {
public void onChat(AsyncPlayerChatEvent e) {
Player player = e.getPlayer();
String message = e.getMessage();
- boolean cancel = message.equalsIgnoreCase("quit") || message.equalsIgnoreCase("exit") || message.equalsIgnoreCase("stop") || message.equalsIgnoreCase("cancel");
- if (plugin.isWaiting(player, WaitingType.COMMAND)) {
+ boolean cancel = message.equalsIgnoreCase("quit") || message.equalsIgnoreCase("exit")
+ || message.equalsIgnoreCase("stop") || message.equalsIgnoreCase("cancel");
+
+ if (plugin.isWaiting(player, WaitingType.RECORDING)) {
+ if (!message.equalsIgnoreCase("done")) return;
+ e.setCancelled(true);
+
+ Action actionImpl = plugin.editingActions.get(player.getUniqueId());
+ if (!(actionImpl instanceof FollowPresetPathAction follow)) {
+ plugin.getLogger().warning("Expected action to be an instance of 'FollowPresetPathAction', got " + actionImpl.getClass().getSimpleName());
+ return;
+ }
+
+ plugin.waiting.remove(player.getUniqueId());
+
+ if (cancel) {
+ SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, follow.getMenu()));
+ return;
+ }
+
+ List path = FollowPresetPathAction.stopRecording(player);
+ follow.setPath(path);
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.set.recording",
+ String.valueOf(path.size())));
+ SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, follow.getMenu()));
+ return;
+ } else if (plugin.isWaiting(player, WaitingType.COMMAND)) {
Action actionImpl = plugin.editingActions.get(player.getUniqueId());
if (!(actionImpl instanceof RunCommand runCommand)) {
plugin.getLogger().warning("Expected action to be an instance of 'RunCommand', got " + actionImpl.getClass().getSimpleName());
@@ -280,7 +321,8 @@ public void onChat(AsyncPlayerChatEvent e) {
plugin.waiting.remove(player.getUniqueId());
int index = HologramMenu.editingIndicies.get(player.getUniqueId());
if (npc.getSettings().getRawHolograms().length <= index) { // an addition
- npc.getSettings().setRawHolograms(Arrays.copyOf(npc.getSettings().getRawHolograms(), index + 1)); // extend it by 1
+ npc.getSettings().setRawHolograms(Arrays.copyOf(npc.getSettings().getRawHolograms(), index + 1)); //
+ // extend it by 1
}
npc.getSettings().getRawHolograms()[index] = message;
player.sendMessage(Msg.translate(player.locale(), "customnpcs.set.name", index + 1, Msg.format(message)));
@@ -340,7 +382,8 @@ public void onChat(AsyncPlayerChatEvent e) {
setTitle.setSubTitle(message);
- player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.set.subtitle", Msg.format(message)));
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.set.subtitle",
+ Msg.format(message)));
SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, actionImpl.getMenu()));
} else if (plugin.isWaiting(player, WaitingType.MESSAGE)) {
Action actionImpl = plugin.editingActions.get(player.getUniqueId());
@@ -356,7 +399,8 @@ public void onChat(AsyncPlayerChatEvent e) {
plugin.waiting.remove(player.getUniqueId());
sendMessage.setRawMessage(message);
- player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.set.message", Msg.format(message)));
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.set.message",
+ Msg.format(message)));
SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, actionImpl.getMenu()));
} else if (plugin.isWaiting(player, WaitingType.SERVER)) {
Action actionImpl = plugin.editingActions.get(player.getUniqueId());
@@ -373,7 +417,8 @@ public void onChat(AsyncPlayerChatEvent e) {
runServer.setServer(message);
- player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.set.server", Msg.format(message)));
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.set.server",
+ Msg.format(message)));
SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, actionImpl.getMenu()));
} else if (plugin.isWaiting(player, WaitingType.ACTIONBAR)) {
Action actionImpl = plugin.editingActions.get(player.getUniqueId());
@@ -388,7 +433,8 @@ public void onChat(AsyncPlayerChatEvent e) {
}
plugin.waiting.remove(player.getUniqueId());
actionBar.setRawMessage(message);
- player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.set.actionbar", Msg.format(message)));
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.actionImpls.set.actionbar",
+ Msg.format(message)));
SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, actionImpl.getMenu()));
} else if (plugin.isWaiting(player, WaitingType.PLAYER)) {
if (cancel) {
@@ -404,27 +450,28 @@ public void onChat(AsyncPlayerChatEvent e) {
return;
}
+
// this runs on an async thread, so there isn't any need to do this async :)
player.sendMessage(Msg.translate(player.locale(), "customnpcs.skins.fetching.player", message));
String name = e.getMessage();
- try {
- URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + name);
- InputStreamReader reader = new InputStreamReader(url.openStream());
- String uuid = new JsonParser().parse(reader).getAsJsonObject().get("id").getAsString();
- reader.close();
-
- URL url2 = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false");
- reader = new InputStreamReader(url2.openStream());
-
- JsonObject property = new JsonParser().parse(reader).getAsJsonObject().get("properties").getAsJsonArray().get(0).getAsJsonObject();
- String value = property.get("value").getAsString();
- String signature = property.get("signature").getAsString();
- npc.getSettings().setSkinData(signature, value, Msg.translatedString(player.locale(), "customnpcs.skins.imported_by.player_name", Msg.format(name)));
- } catch (Exception ignored) {
- player.sendMessage(Msg.translate(player.locale(), "customnpcs.skins.errors.player_does_not_exist", name));
+
+ PlayerProfile profile = Bukkit.createProfile(name);
+ if (!profile.complete()) {
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.skins.errors.player_does_not_exist",
+ name));
e.setCancelled(true);
return;
}
+ profile.complete();
+
+
+ for (ProfileProperty property : profile.getProperties()) {
+ if (!property.getName().equals("textures")) continue;
+ npc.getSettings().setSkinData(property.getSignature(), property.getValue(),
+ Msg.translatedString(player.locale(), "customnpcs" +
+ ".skins.imported_by.player_name", Msg.format(name)));
+ }
+
plugin.waiting.remove(player.getUniqueId());
player.sendMessage(Msg.translate(player.locale(), "customnpcs.skins.success.player_name", name));
SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, MenuUtils.NPC_SKIN));
@@ -449,7 +496,9 @@ public void onChat(AsyncPlayerChatEvent e) {
.name("URL Generated Skin")
.visibility(Visibility.UNLISTED);
SkinUtils.fetch(request).thenAccept(skin -> {
- npc.getSettings().setSkinData(skin.texture().data().signature(), skin.texture().data().value(), Msg.translatedString(player.locale(), "customnpcs.skins.imported_by.url"));
+ npc.getSettings().setSkinData(skin.texture().data().signature(),
+ skin.texture().data().value(), Msg.translatedString(player.locale(), "customnpcs" +
+ ".skins.imported_by.url"));
plugin.waiting.remove(player.getUniqueId());
player.sendMessage(Msg.translate(player.locale(), "customnpcs.skins.success.url", message));
SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, MenuUtils.NPC_SKIN));
@@ -466,17 +515,22 @@ public void onChat(AsyncPlayerChatEvent e) {
Optional detailsOptional = response.getErrorOrMessage();
Throwable finalThrowable = throwable;
detailsOptional.ifPresent(details -> {
- plugin.getLogger().log(Level.SEVERE, details.code() + " : " + details, finalThrowable);
+ plugin.getLogger().log(Level.SEVERE, details.code() + " : " + details,
+ finalThrowable);
System.out.println(details.code() + ": " + details.message());
});
}
- if (throwable.getMessage().equalsIgnoreCase("java.lang.RuntimeException: org.mineskin.data.MineskinException: Failed to find image from url")) {
- player.sendMessage(Msg.translate(player.locale(), "customnpcs.skins.errors.no_image_data"));
+ if (throwable.getMessage().equalsIgnoreCase("java.lang.RuntimeException: org.mineskin" +
+ ".data.MineskinException: Failed to find image from url")) {
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.skins.errors" +
+ ".no_image_data"));
return null;
}
- player.sendMessage(Msg.translate(player.locale(), "customnpcs.skins.errors.unknown_url_error"));
- plugin.getLogger().log(Level.SEVERE, "An error occurred whilst parsing this skin from a url.", throwable);
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.skins.errors" +
+ ".unknown_url_error"));
+ plugin.getLogger().log(Level.SEVERE, "An error occurred whilst parsing this skin from a " +
+ "url.", throwable);
return null;
});
} catch (Exception ex) {
@@ -496,7 +550,8 @@ public void onChat(AsyncPlayerChatEvent e) {
}
plugin.waiting.remove(player.getUniqueId());
e.setCancelled(true);
- player.sendMessage(Msg.translate(player.locale(), "customnpcs.set.clickable_hologram", Msg.format(message)));
+ player.sendMessage(Msg.translate(player.locale(), "customnpcs.set.clickable_hologram",
+ Msg.format(message)));
npc.getSettings().setCustomInteractableHologram(message);
SCHEDULER.runTask(plugin, () -> plugin.getLotus().openMenu(player, MenuUtils.NPC_EXTRA_SETTINGS));
} else if (plugin.isWaiting(player, WaitingType.FACING)) {
@@ -531,7 +586,8 @@ public void onChat(AsyncPlayerChatEvent e) {
public void onPlayerLogin(PlayerJoinEvent e) {
Player player = e.getPlayer();
- if (plugin.update && plugin.getConfig().getBoolean("AlertOnUpdate") && player.hasPermission("customnpcs.alert")) {
+ if (plugin.update && plugin.getConfig().getBoolean("AlertOnUpdate") && player.hasPermission("customnpcs" +
+ ".alert")) {
player.sendMessage(Msg.translate(player.locale(), "customnpcs.should_update").appendNewline()
.append(Msg.format("<#1bd96a>[Modrinth]")));
@@ -691,7 +747,8 @@ private void recalcSleepingPercentages() {
world.setGameRule(GameRule.PLAYERS_SLEEPING_PERCENTAGE, target);
return;
}
- world.setGameRule(GameRule.PLAYERS_SLEEPING_PERCENTAGE, (int) (((playercount - npcCount) / (double) playercount) * target));
+ world.setGameRule(GameRule.PLAYERS_SLEEPING_PERCENTAGE,
+ (int) (((playercount - npcCount) / (double) playercount) * target));
});
}
@@ -770,6 +827,25 @@ public void onSwapToOffhand(PlayerSwapHandItemsEvent event) {
}
}
+ @EventHandler
+ public void serverLoadEvent(ServerLoadEvent event) {
+ SCHEDULER.runTaskLater(plugin, () -> {
+ // setup papi, which happens after the server load event is fired.
+ Plugin placeholderAPI = Bukkit.getPluginManager().getPlugin("PlaceholderAPI");
+ if (placeholderAPI != null) {
+ PlaceholderAPIPlugin papiPlugin = (PlaceholderAPIPlugin) placeholderAPI;
+ plugin.papiPlayerExpansion = papiPlugin.getLocalExpansionManager().getExpansion("Player") != null;
+
+ plugin.getLogger().info("Successfully hooked into PlaceholderAPI.");
+ plugin.papi = true;
+ } else {
+ plugin.papi = false;
+ plugin.getLogger().warning("Could not find PlaceholderAPI! PlaceholderAPI isn't required, but " +
+ "CustomNPCs does support it.");
+ }
+ }, 30);
+ }
+
@Getter
private static class MovementData {
private final UUID uniqueId;
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ActionCustomizerMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ActionCustomizerMenu.java
index 80f33853..3b157920 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ActionCustomizerMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ActionCustomizerMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ActionMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ActionMenu.java
index f04e74a0..c357dd74 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ActionMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ActionMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionCustomizerMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionCustomizerMenu.java
index d19a4816..7c725e8d 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionCustomizerMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionCustomizerMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionMenu.java
index e63b93bf..efa5dcce 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ConditionMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteLineMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteLineMenu.java
index 1453336e..46570ffa 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteLineMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteLineMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteMenu.java
index 5b20c07c..851206e1 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/DeleteMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/EquipmentMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/EquipmentMenu.java
index c74e4751..03d866a5 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/EquipmentMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/EquipmentMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ExtraSettingsMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ExtraSettingsMenu.java
index d9fb4048..da961426 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ExtraSettingsMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/ExtraSettingsMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/HologramMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/HologramMenu.java
index fb104d9b..5c8e3756 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/HologramMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/HologramMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MainNPCMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MainNPCMenu.java
index 550a9d3c..bdf2b181 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MainNPCMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MainNPCMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuItems.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuItems.java
index 356996cc..04fa2465 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuItems.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuItems.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -112,7 +112,8 @@ public static ItemStack skinSelection(InternalNpc npc, Player player) {
profile.setProperty(new ProfileProperty("textures", texture));
skullMeta.setPlayerProfile(profile);
})
- .setLore(Msg.lore(player.locale(), "customnpcs.menus.main.items.skin.lore", Component.text(npc.getSettings().getSkinName())))
+ .setLore(Msg.lore(player.locale(), "customnpcs.menus.main.items.skin.lore",
+ npc.getSettings().getSkinName()))
.setDisplay(Msg.translate(player.locale(), "customnpcs.menus.main.items.skin.name"))
.build();
}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuUtils.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuUtils.java
index c7e66490..b216b7d6 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuUtils.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/MenuUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -177,7 +177,6 @@ public SkinIcon(String value, String signature, String name, CustomNPCs plugin,
public ItemStack toItem() {
return ItemBuilder.modern(PLAYER_HEAD).setDisplay(Msg.format("" + name))
.setLore(
- Msg.translate(locale, "customnpcs.menus.skin_catalog.items.icon.lore", name),
Component.empty(),
Msg.translate(locale, "customnpcs.items.click_to_select")
).modifyMeta(SkullMeta.class, skullMeta -> {
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/NewActionMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/NewActionMenu.java
index 41daf52b..482e1a5a 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/NewActionMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/NewActionMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/NewConditionMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/NewConditionMenu.java
index 5002518c..d9f18aff 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/NewConditionMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/NewConditionMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/PoseEditorMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/PoseEditorMenu.java
index 8adde4f6..1d81a7bc 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/PoseEditorMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/PoseEditorMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025. Foxikle
+ * Copyright (c) 2025-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinCatalog.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinCatalog.java
index d9760d5b..91954f8a 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinCatalog.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinCatalog.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinMenu.java b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinMenu.java
index e4d8d8eb..f55892b3 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinMenu.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/menu/SkinMenu.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/ActionbarRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/ActionbarRunnable.java
index f9737ffd..e5900d41 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/ActionbarRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/ActionbarRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/CommandRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/CommandRunnable.java
index 48cfff53..8c25dbd6 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/CommandRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/CommandRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/FacingDirectionRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/FacingDirectionRunnable.java
index f4221565..58af0edf 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/FacingDirectionRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/FacingDirectionRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/InteractableHologramRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/InteractableHologramRunnable.java
index 401d9d36..f5db2980 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/InteractableHologramRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/InteractableHologramRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/MessageRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/MessageRunnable.java
index 02c819e9..ddbfc315 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/MessageRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/MessageRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/NameRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/NameRunnable.java
index b17572d5..d58b7e18 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/NameRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/NameRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/NudgeRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/NudgeRunnable.java
index 27cf2052..24af17f6 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/NudgeRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/NudgeRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/PlayerNameRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/PlayerNameRunnable.java
index 63b9c818..93ad81ec 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/PlayerNameRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/PlayerNameRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/DebugCommand.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/RecordingRunnable.java
similarity index 54%
rename from core/src/main/java/dev/foxikle/customnpcs/internal/commands/DebugCommand.java
rename to core/src/main/java/dev/foxikle/customnpcs/internal/runnables/RecordingRunnable.java
index ae3dfd8d..638615c5 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/commands/DebugCommand.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/RecordingRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025. Foxikle
+ * Copyright (c) 2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -20,30 +20,36 @@
* SOFTWARE.
*/
-package dev.foxikle.customnpcs.internal.commands;
+package dev.foxikle.customnpcs.internal.runnables;
import dev.foxikle.customnpcs.internal.CustomNPCs;
import dev.foxikle.customnpcs.internal.utils.Msg;
-import dev.velix.imperat.BukkitSource;
-import dev.velix.imperat.annotations.Description;
-import dev.velix.imperat.annotations.Permission;
-import dev.velix.imperat.annotations.SubCommand;
-import dev.velix.imperat.annotations.Usage;
-import dev.velix.imperat.command.AttachmentMode;
+import dev.foxikle.customnpcs.internal.utils.WaitingType;
+import net.kyori.adventure.title.Title;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitRunnable;
-@Permission("customnpcs.edit")
-@SubCommand(value = "debug", attachment = AttachmentMode.MAIN)
-@Description("Enables debug message. (They can be very spammy)")
-public class DebugCommand {
- @Usage
- public void execute(BukkitSource sender) {
- CustomNPCs plugin = CustomNPCs.getInstance();
- if (plugin.isDebug()) {
- sender.reply(Msg.translate(CommandUtils.getLocale(sender), "customnpcs.commands.debug.message.disable"));
- plugin.setDebug(false);
- } else {
- sender.reply(Msg.translate(CommandUtils.getLocale(sender), "customnpcs.commands.debug.message.enable"));
- plugin.setDebug(true);
+import java.time.Duration;
+
+public class RecordingRunnable extends BukkitRunnable {
+ private final Player player;
+ private final CustomNPCs plugin;
+
+ public RecordingRunnable(Player player, CustomNPCs plugin) {
+ this.player = player;
+ this.plugin = plugin;
+ }
+
+ @Override
+ public void run() {
+ if (!plugin.isWaiting(player, WaitingType.RECORDING)) {
+ this.cancel();
+ return;
}
+ player.showTitle(Title.title(
+ Msg.translate(player.locale(), "customnpcs.data.recording.title"),
+ Msg.translate(player.locale(), "customnpcs.data.recording.subtitle"),
+ Title.Times.times(Duration.ofMillis(0), Duration.ofMillis(1000L), Duration.ofMillis(0))
+ ));
}
}
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/ServerRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/ServerRunnable.java
index 93fd7345..0137abf8 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/ServerRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/ServerRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/SoundRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/SoundRunnable.java
index 1a51c7da..626da5bf 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/SoundRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/SoundRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/SubtitleRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/SubtitleRunnable.java
index 4e850d88..34943154 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/SubtitleRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/SubtitleRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/TargetInputRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/TargetInputRunnable.java
index 3691c7fb..61ccddbd 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/TargetInputRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/TargetInputRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/TitleRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/TitleRunnable.java
index 104d9dfb..1f6d97c8 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/TitleRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/TitleRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/UrlRunnable.java b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/UrlRunnable.java
index 3eb78993..95fa8fb7 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/UrlRunnable.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/runnables/UrlRunnable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/translations/Translations.java b/core/src/main/java/dev/foxikle/customnpcs/internal/translations/Translations.java
index 3a274478..1a171cd0 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/translations/Translations.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/translations/Translations.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -24,39 +24,34 @@
import net.kyori.adventure.key.Key;
import net.kyori.adventure.translation.GlobalTranslator;
-import net.kyori.adventure.translation.TranslationRegistry;
-import net.kyori.adventure.util.UTF8ResourceBundleControl;
+import net.kyori.adventure.translation.TranslationStore;
+import java.text.MessageFormat;
+import java.util.HashMap;
import java.util.Locale;
+import java.util.Map;
import java.util.ResourceBundle;
public class Translations {
- TranslationRegistry registry = TranslationRegistry.create(Key.key("customnpcs:localization"));
-
-
public static final Locale VIETNAMESE = new Locale("vi");
+ public static final Locale RUSSIAN = new Locale("ru");
+ private static final TranslationStore STORE = TranslationStore.messageFormat(Key.key("customnpcs" +
+ ":root"));
+ private static final Map BUNDLES = new HashMap<>();
+ private static boolean setup = false;
public void setup() {
-
-
- ResourceBundle zh = ResourceBundle.getBundle("localization.Chinese", Locale.SIMPLIFIED_CHINESE, UTF8ResourceBundleControl.get());
- registry.registerAll(Locale.SIMPLIFIED_CHINESE, zh, true);
-
-
- ResourceBundle ru = ResourceBundle.getBundle("localization.Russian", new Locale("ru"), UTF8ResourceBundleControl.get());
- registry.registerAll(new Locale("ru"), ru, true);
-
- ResourceBundle de = ResourceBundle.getBundle("localization.German", Locale.GERMAN, UTF8ResourceBundleControl.get());
- registry.registerAll(Locale.GERMAN, de, true);
-
- ResourceBundle en = ResourceBundle.getBundle("localization.English", Locale.US, UTF8ResourceBundleControl.get());
- registry.registerAll(Locale.ENGLISH, en, true);
-
- ResourceBundle vi = ResourceBundle.getBundle("localization.Vietnamese", VIETNAMESE, UTF8ResourceBundleControl.get());
- registry.registerAll(VIETNAMESE, vi, true);
-
- registry.defaultLocale(Locale.ENGLISH);
-
- GlobalTranslator.translator().addSource(registry);
+ if (setup) return;
+ BUNDLES.put(Locale.SIMPLIFIED_CHINESE, ResourceBundle.getBundle("localization.Chinese",
+ Locale.SIMPLIFIED_CHINESE));
+ BUNDLES.put(RUSSIAN, ResourceBundle.getBundle("localization.Russian", RUSSIAN));
+ BUNDLES.put(Locale.GERMAN, ResourceBundle.getBundle("localization.German", Locale.GERMAN));
+ BUNDLES.put(Locale.US, ResourceBundle.getBundle("localization.English", Locale.US));
+ BUNDLES.put(VIETNAMESE, ResourceBundle.getBundle("localization.Vietnamese", VIETNAMESE));
+
+ BUNDLES.forEach((locale, b) -> STORE.registerAll(locale, b.keySet(), s -> new MessageFormat(b.getString(s))));
+
+ GlobalTranslator.translator().addSource(STORE);
+ setup = true;
}
-}
+}
\ No newline at end of file
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ActionRegistry.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ActionRegistry.java
index 36eccb21..afe65118 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ActionRegistry.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ActionRegistry.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/AutoUpdater.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/AutoUpdater.java
index 2a015a16..39f67c8c 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/AutoUpdater.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/AutoUpdater.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ComponentWrapper.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ComponentWrapper.java
index 4ec347f0..6a35c1fd 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ComponentWrapper.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ComponentWrapper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Msg.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Msg.java
index 85b37011..81075c61 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Msg.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Msg.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/OpenButtonAction.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/OpenButtonAction.java
index 9ad80e02..f0a5ef67 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/OpenButtonAction.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/OpenButtonAction.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ParameterizedSupplier.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ParameterizedSupplier.java
index 4bd1035e..a4619e87 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ParameterizedSupplier.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/ParameterizedSupplier.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/SkinUtils.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/SkinUtils.java
index 43ba7b6f..3ef3b175 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/SkinUtils.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/SkinUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025. Foxikle
+ * Copyright (c) 2025-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Utils.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Utils.java
index 2cbe8d8b..4e763467 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Utils.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/Utils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024-2025. Foxikle
+ * Copyright (c) 2024-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/WaitingType.java b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/WaitingType.java
index 66e79ff4..0e4fb2ea 100644
--- a/core/src/main/java/dev/foxikle/customnpcs/internal/utils/WaitingType.java
+++ b/core/src/main/java/dev/foxikle/customnpcs/internal/utils/WaitingType.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025. Foxikle
+ * Copyright (c) 2025-2026. Foxikle
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -25,5 +25,5 @@
public enum WaitingType {
COMMAND, NAME, TARGET, TITLE, MESSAGE, FACING, SOUND,
SERVER, ACTIONBAR, URL, PLAYER, HOLOGRAM, SUBTITLE,
- NUDGE
+ NUDGE, RECORDING
}
diff --git a/core/src/main/resources/localization/Chinese_zh_CN.properties b/core/src/main/resources/localization/Chinese_zh_CN.properties
index 75a107ec..cef68c7b 100644
--- a/core/src/main/resources/localization/Chinese_zh_CN.properties
+++ b/core/src/main/resources/localization/Chinese_zh_CN.properties
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2024-2025. Foxikle
+# Copyright (c) 2024-2026. Foxikle
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -36,7 +36,7 @@ customnpcs.commands.wiki =\u70B9\u5
customnpcs.commands.wiki.hover =\u70B9\u51FB\u8BBF\u95EE https\://docs.foxikle.dev/
customnpcs.commands.setsound.unknown_sound =\u65E0\u6CD5\u8BC6\u522B\u7684\u58F0\u97F3\uFF0C\u8BF7\u4F7F\u7528\u6807\u7B7E\u4E0D\u5168\u3002
# {0} is the name of the sound
-customnpcs.commands.setsound.success =\u6210\u529F\u5C06\u58F0\u97F3\u8BBE\u7F6E\u4E3A '{0}'
+customnpcs.commands.setsound.success=\u6210\u529F\u5C06\u58F0\u97F3\u8BBE\u7F6E\u4E3A ''{0}''
customnpcs.commands.setsound.was_not_waiting =\u672A\u80FD\u8BBE\u7F6ENPC\u58F0\u97F3.NPCs\u6CA1\u6709\u7B49\u5F85\u56DE\u5E94\u3002\u5982\u679C\u60A8\u8BA4\u4E3A\u8FD9\u662F\u9519\u8BEF\uFF0C\u8BF7\u8054\u7CFBFoxikle.
customnpcs.commands.invalid_uuid =\u63D0\u4F9B\u7684UUID\u4E0D\u5339\u914D\u4EFB\u4F55NPC.
customnpcs.commands.invalid_name_or_uuid =\u63D0\u4F9B\u7684UUID\u6216\u540D\u79F0\u65E0\u6548.
@@ -88,25 +88,25 @@ customnpcs.commands.help.wiki.hover =\u7ED9\u4F60\u8BB
#=====================================#
# Actions Messages #
#=====================================#
-customnpcs.actionImpls.set.command =\u6210\u529F\u5C06\u547D\u4EE4\u8BBE\u7F6E\u4E3A '{0}'
-customnpcs.actionImpls.set.title =\u6210\u529F\u5C06\u6807\u9898\u8BBE\u7F6E\u4E3A '{0}'
-customnpcs.actionImpls.set.subtitle =\u6210\u529F\u5C06\u526F\u6807\u9898\u8BBE\u7F6E\u4E3A '{0}'
-customnpcs.actionImpls.set.message =\u6210\u529F\u5C06\u6D88\u606F\u8BBE\u7F6E\u4E3A '{0}'
-customnpcs.actionImpls.set.server =\u6210\u529F\u5C06\u670D\u52A1\u5668\u8BBE\u7F6E\u4E3A '{0}'
-customnpcs.actionImpls.set.actionbar =\u6210\u529F\u5C06\u52A8\u4F5C\u6761\u8BBE\u7F6E\u4E3A '{0}'
-customnpcs.actionImpls.conditions.set.target =\u6210\u529F\u5C06\u76EE\u6807\u8BBE\u7F6E\u4E3A '{0}'
+customnpcs.actionImpls.set.command=\u6210\u529F\u5C06\u547D\u4EE4\u8BBE\u7F6E\u4E3A ''{0}''
+customnpcs.actionImpls.set.title=\u6210\u529F\u5C06\u6807\u9898\u8BBE\u7F6E\u4E3A ''{0}''
+customnpcs.actionImpls.set.subtitle=\u6210\u529F\u5C06\u526F\u6807\u9898\u8BBE\u7F6E\u4E3A ''{0}''
+customnpcs.actionImpls.set.message=\u6210\u529F\u5C06\u6D88\u606F\u8BBE\u7F6E\u4E3A ''{0}''
+customnpcs.actionImpls.set.server=\u6210\u529F\u5C06\u670D\u52A1\u5668\u8BBE\u7F6E\u4E3A ''{0}''
+customnpcs.actionImpls.set.actionbar=\u6210\u529F\u5C06\u52A8\u4F5C\u6761\u8BBE\u7F6E\u4E3A ''{0}''
+customnpcs.actionImpls.conditions.set.target=\u6210\u529F\u5C06\u76EE\u6807\u8BBE\u7F6E\u4E3A ''{0}''
#=====================================#
# General Messages #
#=====================================#
customnpcs.should_update =---------------- [\!] CustomNPCs [\!] ----------------\u6709\u65B0\u66F4\u65B0\u53EF\u7528\! \u5982\u679C\u60A8\u80FD\u66F4\u65B0\u4E00\u4E0B\uFF0C\u6211\u5C06\u975E\u5E38\u611F\u6FC0 \:) ~Foxikle
# {0} is the new name
-customnpcs.set.name =\u6210\u529F\u5C06\u540D\u79F0\u8BBE\u7F6E\u4E3A '{0}'
+customnpcs.set.name=\u6210\u529F\u5C06\u540D\u79F0\u8BBE\u7F6E\u4E3A ''{0}''
# {0} is the input
-customnpcs.error.parse_number =\u65E0\u6CD5\u89E3\u6790\u6570\u5B57 '{0}' \u8BF7\u518D\u8BD5\u4E00\u6B21.
+customnpcs.error.parse_number=\u65E0\u6CD5\u89E3\u6790\u6570\u5B57 ''{0}'' \u8BF7\u518D\u8BD5\u4E00\u6B21.
customnpcs.error.npc-menu-expired =\u54CE\u5440\! \u770B\u8D77\u6765\u60A8\u7684\u83DC\u5355\u5B9E\u4F8B\u5DF2\u8FC7\u671F\! \u60A8\u53EF\u4EE5\u91CD\u65B0\u6253\u5F00\u83DC\u5355\u6765\u89E3\u51B3\u8FD9\u4E2A\u95EE\u9898 \:)
customnpcs.error.forgot_bailout_response =\u54CE\u5440\! \u6211\u4EEC\u5FD8\u8BB0\u4E86\u5982\u4F55\u5904\u7406\u62D2\u7EDD\u5220\u9664\u8FD9\u4E2ANPC. \u522B\u62C5\u5FC3,\u5B83\u6CA1\u6709\u88AB\u5220\u9664\!
# {0} is the hologram text
-customnpcs.set.clickable_hologram =\u6210\u529F\u5C06NPC\u7684\u5168\u606F\u6587\u5B57\u8BBE\u7F6E\u4E3A'{0}'
+customnpcs.set.clickable_hologram=\u6210\u529F\u5C06NPC\u7684\u5168\u606F\u6587\u5B57\u8BBE\u7F6E\u4E3A''{0}''
customnpcs.set.facing_direction =\u6210\u529F\u8BBE\u7F6E\u671D\u5411\!
# {0} is the name of NPC
customnpcs.delete.success =\u6C38\u4E45\u5220\u9664\u4E86 {0}.
@@ -118,16 +118,16 @@ customnpcs.edit.fail =\u6B64\u64CD
# Skin Messages #
#=====================================#
# {0} is the players name
-customnpcs.skins.fetching.player =\u6B63\u5728\u5C1D\u8BD5\u4ECEMojang\u7684API\u83B7\u53D6 {0}' \u7684\u76AE\u80A4.\u8FD9\u53EF\u80FD\u9700\u8981\u4E00\u4E9B\u65F6\u95F4\!
+customnpcs.skins.fetching.player=\u6B63\u5728\u5C1D\u8BD5\u4ECEMojang\u7684API\u83B7\u53D6 {0}'' \u7684\u76AE\u80A4.\u8FD9\u53EF\u80FD\u9700\u8981\u4E00\u4E9B\u65F6\u95F4\!
customnpcs.skins.fetching.url =\u6B63\u5728\u5C1D\u8BD5\u4ECEURL\u83B7\u53D6\u76AE\u80A4.\u8FD9\u53EF\u80FD\u9700\u8981\u4E00\u4E9B\u65F6\u95F4\!
-customnpcs.skins.imported_by.player_name ={0}'\u7684\u76AE\u80A4 (\u901A\u8FC7\u540D\u79F0\u5BFC\u5165)
+customnpcs.skins.imported_by.player_name={0}''\u7684\u76AE\u80A4 (\u901A\u8FC7\u540D\u79F0\u5BFC\u5165)
customnpcs.skins.imported_by.url =\u901A\u8FC7URL\u5BFC\u5165\u7684\u76AE\u80A4
-customnpcs.skins.success.player_name =\u6210\u529F\u5C06NPC\u7684\u76AE\u80A4\u8BBE\u7F6E\u4E3A {0}' \u7684\u76AE\u80A4\!
+customnpcs.skins.success.player_name=\u6210\u529F\u5C06NPC\u7684\u76AE\u80A4\u8BBE\u7F6E\u4E3A {0}'' \u7684\u76AE\u80A4\!
customnpcs.skins.success.url =\u6210\u529F\u4ECE {0} \u5BFC\u5165NPC\u7684\u76AE\u80A4\!
customnpcs.skins.errors.invalid_url =\u89E3\u6790NPC\u76AE\u80A4\u65F6\u53D1\u751F\u9519\u8BEF.\u8FD9\u4E2AURL\u6709\u6548\u5417?
customnpcs.skins.errors.unknown_url_error =\u89E3\u6790\u6B64\u76AE\u80A4\u65F6\u53D1\u751F\u9519\u8BEF.\u8BF7\u68C0\u67E5\u63A7\u5236\u53F0\u4EE5\u83B7\u53D6\u8BE6\u7EC6\u4FE1\u606F.
customnpcs.skins.errors.no_image_data =\u63D0\u4F9B\u7684URL\u662F \u6709\u6548\u7684, \u4F46\u5B83\u4E0D\u5305\u542B\u4EFB\u4F55\u76AE\u80A4\u6570\u636E.\u62B1\u6B49\!
-customnpcs.skins.errors.player_does_not_exist =\u89E3\u6790 {0}'\u7684\u76AE\u80A4\u65F6\u51FA\u9519\! \u8FD9\u4E2A\u73A9\u5BB6\u5B58\u5728\u5417?
+customnpcs.skins.errors.player_does_not_exist=\u89E3\u6790 {0}''\u7684\u76AE\u80A4\u65F6\u51FA\u9519\! \u8FD9\u4E2A\u73A9\u5BB6\u5B58\u5728\u5417?
customnpcs.skins.changed_with_catalog =\u76AE\u80A4\u66F4\u6539\u4E3A {0}
#=====================================#
# Data Collection Messages #
@@ -180,7 +180,7 @@ customnpcs.directions.north_west =\u897F\u53
customnpcs.directions.player =\u73A9\u5BB6\u65B9\u5411
customnpcs.menus.skin_catalog.title =\u9009\u62E9\u4E00\u4E2A\u76AE\u80A4
# The skin name is {0}
-customnpcs.menus.skin_catalog.items.icon.lore ='{0} \u76AE\u80A4'
+customnpcs.menus.skin_catalog.items.icon.lore=''{0} \u76AE\u80A4''
customnpcs.menus.delete.title =\u5220\u9664 NPC
customnpcs.menus.delete.items.confirm.name =\u5220\u9664
customnpcs.menus.delete.items.confirm.lore =\u8FD9\u65E0\u6CD5\u88AB\u64A4\u6D88\u3002
@@ -358,7 +358,7 @@ customnpcs.menus.extra.hologram_visibility.description =\u4EA4\u4
customnpcs.menus.extra.hologram_visibility.description.hidden =\u9690\u85CF
customnpcs.menus.extra.hologram_visibility.description.shown =\u663E\u793A
customnpcs.menus.extra.hologram_text =\u66F4\u6539NPC\u70B9\u51FB\u5168\u606F\u56FE\u6587\u672C
-customnpcs.menus.extra.hologram_text.description =\u8FD9\u53EA\u4F1A\u66F4\u6539 \u6B64 NPC\u7684\u4EA4\u4E92\u5F0F\u5168\u606F\u56FE.\n \u679C\u60A8\u60F3\u66F4\u6539\u9ED8\u8BA4\u503C\uFF0C\u8BF7\u66F4\u6539config.yml\u4E2D\u7684'ClickText'\u5B57\u6BB5\!
+customnpcs.menus.extra.hologram_text.description=\u8FD9\u53EA\u4F1A\u66F4\u6539 \u6B64 NPC\u7684\u4EA4\u4E92\u5F0F\u5168\u606F\u56FE.\n \u679C\u60A8\u60F3\u66F4\u6539\u9ED8\u8BA4\u503C\uFF0C\u8BF7\u66F4\u6539config.yml\u4E2D\u7684''ClickText''\u5B57\u6BB5\!
customnpcs.menus.extra.hologram_text.type =\u5728\u804A\u5929\u4E2D\u8F93\u5165\u65B0\u7684\u5168\u606F\u56FE\u6587\u672C\!
customnpcs.favicons.teleport =\u4F20\u9001\u73A9\u5BB6
customnpcs.favicons.edit =\u5DE6\u952E\u70B9\u51FB\u4EE5\u7F16\u8F91
@@ -414,7 +414,7 @@ customnpcs.conditions.is_frozen =\u6B63\u5728\u51B
customnpcs.conditions.is_gliding =\u6B63\u5728\u6ED1\u7FD4
customnpcs.comparator =\u6BD4\u8F83\u5668
customnpcs.value.select =\u9009\u62E9\u76EE\u6807\u503C
-customnpcs.value.current =\u76EE\u6807\u503C\u4E3A '{0}'
+customnpcs.value.current=\u76EE\u6807\u503C\u4E3A ''{0}''
customnpcs.statistic =\u7EDF\u8BA1\u6570\u636E
customnpcs.menus.skins.title => \u7F16\u8F91 NPC \u76AE\u80A4
customnpcs.menus.skins.player =\u4ECE\u73A9\u5BB6\u5BFC\u5165
@@ -427,9 +427,9 @@ customnpcs.menus.actions.new.title =>
customnpcs.menus.conditions.title => \u7F16\u8F91\u52A8\u4F5C\u6761\u4EF6
customnpcs.menus.conditions.numeric =\u6570\u503C\u6761\u4EF6
customnpcs.menus.conditions.logical =\u903B\u8F91\u6761\u4EF6
-customnpcs.menus.conditions.comparator =\u6BD4\u8F83\u5668\: '{0}'
-customnpcs.menus.conditions.value =\u503C\: '{0}'
-customnpcs.menus.conditions.target =\u76EE\u6807\u503C\: '{0}'
+customnpcs.menus.conditions.comparator=\u6BD4\u8F83\u5668\: ''{0}''
+customnpcs.menus.conditions.value=\u503C\: ''{0}''
+customnpcs.menus.conditions.target=\u76EE\u6807\u503C\: ''{0}''
customnpcs.menus.conditions.new_condition =\u65B0\u5EFA\u6761\u4EF6
customnpcs.menus.conditions.mode.toggle =\u66F4\u6539\u6A21\u5F0F
customnpcs.menus.conditions.mode.all =\u5339\u914D\u6240\u6709\u6761\u4EF6
diff --git a/core/src/main/resources/localization/English_en_US.properties b/core/src/main/resources/localization/English_en_US.properties
index fa88b9c1..0c652994 100644
--- a/core/src/main/resources/localization/English_en_US.properties
+++ b/core/src/main/resources/localization/English_en_US.properties
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2024-2025. Foxikle
+# Copyright (c) 2024-2026. Foxikle
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -19,484 +19,506 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
-customnpcs.commands.no_permission =You do not have permission to do this!
+customnpcs.commands.no_permission=You do not have permission to do this!
# {0} is the plugin version
-customnpcs.commands.header = [!] CustomNPCs {0} [!]
-customnpcs.commands.manage.no_npcs =There are no NPCs!
-customnpcs.commands.manage.header = [!] Manage NPCs [!]
-customnpcs.commands.manage.copy_uuid =Click to copy this NPC\'s UUID.
-customnpcs.commands.manage.button.edit =[EDIT]
-customnpcs.commands.manage.button.edit.hover =Click to edit this NPC.
-customnpcs.commands.manage.button.delete =[DELETE]
-customnpcs.commands.manage.button.delete.hover =Click to permanently delete this NPC.
-customnpcs.commands.clear_holograms.removed =This command was removed in 1.6.1 because of a rework in NPC hologram names. If you wish to remove any holograms that remain, please use the following command. /kill @e[tag=npcHologram]
-customnpcs.commands.reload.start =Reloading NPCs!
-customnpcs.commands.reload.end =Successfully reloaded all NPCs!
-customnpcs.commands.wiki =Click here to visit the CustomNPCs Wiki!
-customnpcs.commands.wiki.hover =Click to visit https://docs.foxikle.dev/
-customnpcs.commands.setsound.unknown_sound =Unrecognised sound, please use tab completions.
+customnpcs.commands.header= [!] CustomNPCs {0} [!]
+customnpcs.commands.manage.no_npcs=There are no NPCs!
+customnpcs.commands.manage.header= [!] Manage NPCs [!]
+customnpcs.commands.manage.copy_uuid=Click to copy this NPC''s UUID.
+customnpcs.commands.manage.button.edit=[EDIT]
+customnpcs.commands.manage.button.edit.hover=Click to edit this NPC.
+customnpcs.commands.manage.button.delete=[DELETE]
+customnpcs.commands.manage.button.delete.hover=Click to permanently delete this NPC.
+customnpcs.commands.clear_holograms.removed=This command was removed in 1.6.1 because of a rework in NPC hologram names. If you wish to remove any holograms that remain, please use the following command. /kill @e[tag=npcHologram]
+customnpcs.commands.reload.start=Reloading NPCs!
+customnpcs.commands.reload.end=Successfully reloaded all NPCs!
+customnpcs.commands.wiki=Click here to visit the CustomNPCs Wiki!
+customnpcs.commands.wiki.hover=Click to visit https://docs.foxikle.dev/
+customnpcs.commands.setsound.unknown_sound=Unrecognised sound, please use tab completions.
# {0} is the name of the sound
-customnpcs.commands.setsound.success =Successfully set sound to be \'{0}\'
-customnpcs.commands.setsound.was_not_waiting =Unsuccessfully set NPC sound. I wasn't waiting for a response. Please contact Foxikle if you think this is a mistake.
-customnpcs.commands.invalid_uuid =The UUID provided does not match any NPC.
-customnpcs.commands.invalid_name_or_uuid =Invalid UUID or Name provided.
-customnpcs.commands.clone.success =