diff --git a/main/src/main/java/net/citizensnpcs/Citizens.java b/main/src/main/java/net/citizensnpcs/Citizens.java
index 51c93c136..c760b2e26 100644
--- a/main/src/main/java/net/citizensnpcs/Citizens.java
+++ b/main/src/main/java/net/citizensnpcs/Citizens.java
@@ -405,8 +405,10 @@ public class Citizens extends JavaPlugin implements CitizensPlugin {
traitFactory = new CitizensTraitFactory(this);
traitFactory.registerTrait(TraitInfo.create(ShopTrait.class).withSupplier(() -> new ShopTrait(shops)));
selector = new NPCSelector(this);
- templateRegistry = new TemplateRegistry(new File(this.getDataFolder(), "templates").toPath());
-
+ templateRegistry = new TemplateRegistry(new File(getDataFolder(), "templates").toPath());
+ if (!new File(getDataFolder(), "skins").exists()) {
+ new File(getDataFolder(), "skins").mkdir();
+ }
Bukkit.getPluginManager().registerEvents(new EventListen(), this);
Bukkit.getPluginManager().registerEvents(new Placeholders(), this);
diff --git a/main/src/main/java/net/citizensnpcs/EventListen.java b/main/src/main/java/net/citizensnpcs/EventListen.java
index a3ae230c9..02320e259 100644
--- a/main/src/main/java/net/citizensnpcs/EventListen.java
+++ b/main/src/main/java/net/citizensnpcs/EventListen.java
@@ -456,6 +456,7 @@ public class EventListen implements Listener {
@EventHandler
public void onNPCKnockback(NPCKnockbackEvent event) {
+ event.setCancelled(event.getNPC().isProtected());
if (event.getNPC().data().has(NPC.Metadata.KNOCKBACK)) {
event.setCancelled(!event.getNPC().data().get(NPC.Metadata.KNOCKBACK, true));
}
diff --git a/main/src/main/java/net/citizensnpcs/Settings.java b/main/src/main/java/net/citizensnpcs/Settings.java
index cb31f65e0..8529ba4ab 100644
--- a/main/src/main/java/net/citizensnpcs/Settings.java
+++ b/main/src/main/java/net/citizensnpcs/Settings.java
@@ -110,7 +110,7 @@ public class Settings {
"npc.pathfinding.default-distance-margin", 1),
DEFAULT_HOLOGRAM_RENDERER(
"The default renderer for holograms, must be one of the following:
interaction - matches inbuilt nametags most closely
display - allows for different colored backgrounds
armorstand - creates an armorstand and teleports it to the player",
- "npc.hologram.default-renderer", "interaction"),
+ "npc.hologram.default-renderer", "display"),
DEFAULT_LOOK_CLOSE("Enable look close by default", "npc.default.look-close.enabled", false),
DEFAULT_LOOK_CLOSE_RANGE("Default look close range in blocks", "npc.default.look-close.range", 10),
DEFAULT_NPC_HOLOGRAM_LINE_HEIGHT("Default distance between hologram lines", "npc.hologram.default-line-height",
diff --git a/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java b/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java
index 5033b2762..4537bf89d 100644
--- a/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java
+++ b/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java
@@ -23,6 +23,7 @@ import org.bukkit.DyeColor;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
import org.bukkit.OfflinePlayer;
import org.bukkit.Rotation;
import org.bukkit.Sound;
@@ -893,7 +894,9 @@ public class NPCCommands {
StringBuilder builder = new StringBuilder();
for (String part : parts) {
if (part.contains(":")) {
- Template template = templateRegistry.getTemplateByNamespacedKey(part);
+ int idx = part.indexOf(':');
+ Template template = templateRegistry
+ .getTemplateByKey(new NamespacedKey(part.substring(0, idx), part.substring(idx + 1)));
if (template == null)
continue;
template.apply(npc);
@@ -3347,7 +3350,8 @@ public class NPCCommands {
modifiers = { "tpto" },
min = 2,
max = 3,
- permission = "citizens.npc.tpto")
+ permission = "citizens.npc.tpto",
+ parsePlaceholders = true)
@Requirements
public void tpto(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
Entity from = null, to = null;
diff --git a/main/src/main/java/net/citizensnpcs/commands/TemplateCommands.java b/main/src/main/java/net/citizensnpcs/commands/TemplateCommands.java
index c82b385a0..e3396dcca 100644
--- a/main/src/main/java/net/citizensnpcs/commands/TemplateCommands.java
+++ b/main/src/main/java/net/citizensnpcs/commands/TemplateCommands.java
@@ -3,6 +3,7 @@ package net.citizensnpcs.commands;
import java.util.Collection;
import java.util.stream.Collectors;
+import org.bukkit.NamespacedKey;
import org.bukkit.command.CommandSender;
import com.google.common.base.Joiner;
@@ -36,25 +37,47 @@ public class TemplateCommands {
min = 2,
permission = "citizens.templates.apply")
public void apply(CommandContext args, CommandSender sender, NPC npc,
- @Arg(value = 1, completionsProvider = TemplateCompletions.class) String templateName)
+ @Arg(value = 1, completionsProvider = TemplateCompletions.class) String templateKey)
throws CommandException {
Template template = null;
- if (templateName.contains(":")) {
- template = registry.getTemplateByNamespacedKey(templateName);
+ if (templateKey.contains(":")) {
+ int idx = templateKey.indexOf(':');
+ template = registry
+ .getTemplateByKey(new NamespacedKey(templateKey.substring(0, idx), templateKey.substring(idx + 1)));
} else {
- Collection templates = registry.getTemplates(templateName);
+ Collection templates = registry.getTemplates(templateKey);
if (templates.isEmpty())
throw new CommandException(Messages.TEMPLATE_MISSING);
if (templates.size() > 1)
- throw new CommandException(Messages.TEMPLATE_PICKER, templateName, Joiner.on(", ").join(templates
- .stream().map(t -> t.getNamespace() + ":" + t.getName()).collect(Collectors.toList())));
+ throw new CommandException(Messages.TEMPLATE_PICKER, templateKey,
+ Joiner.on(", ").join(templates.stream().map(t -> t.getKey()).collect(Collectors.toList())));
template = templates.iterator().next();
}
if (template == null)
throw new CommandException(Messages.TEMPLATE_MISSING);
template.apply(npc);
- Messaging.sendTr(sender, Messages.TEMPLATE_APPLIED, template.getName(), npc.getName());
+ Messaging.sendTr(sender, Messages.TEMPLATE_APPLIED, template.getKey().getKey(), npc.getName());
+ }
+
+ @Command(
+ aliases = { "template", "tpl" },
+ usage = "generate (template namespace:)[name]",
+ desc = "",
+ modifiers = { "generate" },
+ min = 2,
+ max = 2,
+ permission = "citizens.templates.generate")
+ public void generate(CommandContext args, CommandSender sender, NPC npc,
+ @Arg(value = 1, completionsProvider = TemplateCompletions.class) String templateName)
+ throws CommandException {
+ int idx = templateName.indexOf(':');
+ NamespacedKey key = idx == -1 ? new NamespacedKey("generated", templateName)
+ : new NamespacedKey(templateName.substring(0, idx), templateName.substring(idx + 1));
+ if (registry.getTemplateByKey(key) != null)
+ throw new CommandException(Messages.TEMPLATE_CONFLICT);
+ registry.generateTemplateFromNPC(key, npc);
+ Messaging.sendTr(sender, Messages.TEMPLATE_GENERATED, npc.getName());
}
@Command(
@@ -68,7 +91,7 @@ public class TemplateCommands {
public void list(CommandContext args, CommandSender sender, NPC npc) throws CommandException {
Messaging.sendTr(sender, Messages.TEMPLATE_LIST_HEADER);
for (Template template : registry.getAllTemplates()) {
- Messaging.send(sender, "[[-]] " + template.getNamespace() + ":" + template.getName());
+ Messaging.send(sender, "[[-]] " + template.getKey());
}
}
@@ -81,7 +104,7 @@ public class TemplateCommands {
@Override
public Collection getCompletions(CommandContext args, CommandSender sender, NPC npc) {
- return templateRegistry.getAllTemplates().stream().map(t -> t.getNamespace() + ":" + t.getName())
+ return templateRegistry.getAllTemplates().stream().map(t -> t.getKey().toString())
.collect(Collectors.toList());
}
}
diff --git a/main/src/main/java/net/citizensnpcs/trait/CurrentLocation.java b/main/src/main/java/net/citizensnpcs/trait/CurrentLocation.java
index e4cfa3e7c..80ef3994d 100644
--- a/main/src/main/java/net/citizensnpcs/trait/CurrentLocation.java
+++ b/main/src/main/java/net/citizensnpcs/trait/CurrentLocation.java
@@ -9,7 +9,8 @@ import net.citizensnpcs.api.util.DataKey;
import net.citizensnpcs.util.NMS;
/**
- * Persists the current {@link Location} of the {@link NPC}. Will cache last known location if despawned.
+ * Persists the current {@link Location} of the {@link net.citizensnpcs.api.npc.NPC}. Will cache last known location if
+ * despawned.
*/
@TraitName("location")
public class CurrentLocation extends Trait {
diff --git a/main/src/main/java/net/citizensnpcs/trait/shop/ItemAction.java b/main/src/main/java/net/citizensnpcs/trait/shop/ItemAction.java
index 6b1e958eb..660f4ff55 100644
--- a/main/src/main/java/net/citizensnpcs/trait/shop/ItemAction.java
+++ b/main/src/main/java/net/citizensnpcs/trait/shop/ItemAction.java
@@ -28,6 +28,8 @@ import net.citizensnpcs.api.util.SpigotUtil;
import net.citizensnpcs.util.InventoryMultiplexer;
import net.citizensnpcs.util.NMS;
import net.citizensnpcs.util.Util;
+import net.kyori.adventure.platform.bukkit.BukkitComponentSerializer;
+import net.kyori.adventure.text.Component;
public class ItemAction extends NPCShopAction {
@Persist
@@ -53,11 +55,11 @@ public class ItemAction extends NPCShopAction {
@Override
public String describe() {
if (items.size() == 1)
- return items.get(0).getAmount() + " " + Util.prettyEnum(items.get(0).getType());
+ return stringify(items.get(0));
String description = items.size() + " items";
for (int i = 0; i < items.size(); i++) {
ItemStack item = items.get(i);
- description += "\n" + item.getAmount() + " " + Util.prettyEnum(item.getType());
+ description += "\n" + stringify(item);
if (i == 3) {
description += "...";
break;
@@ -155,6 +157,7 @@ public class ItemAction extends NPCShopAction {
return true;
}
+ @SuppressWarnings("unchecked")
private boolean metaMatches(ItemStack needle, ItemStack haystack, List meta) {
Map source = NMS.getComponentMap(needle);
Map compare = NMS.getComponentMap(haystack);
@@ -178,6 +181,14 @@ public class ItemAction extends NPCShopAction {
return true;
}
+ private String stringify(ItemStack item) {
+ if (SUPPORT_TRANSLATABLE) {
+ return BukkitComponentSerializer.legacy().serialize(Component.text(item.getAmount() + " ")
+ .append(Component.translatable().key(item.getTranslationKey())));
+ }
+ return item.getAmount() + " " + Util.prettyEnum(item.getType());
+ }
+
@Override
public Transaction take(Entity entity, InventoryMultiplexer im, int repeats) {
if (!(entity instanceof InventoryHolder))
@@ -314,4 +325,14 @@ public class ItemAction extends NPCShopAction {
return action instanceof ItemAction;
}
}
+
+ private static boolean SUPPORT_TRANSLATABLE = true;
+
+ static {
+ try {
+ Class.forName("org.bukkit.Translatable");
+ } catch (ClassNotFoundException e) {
+ SUPPORT_TRANSLATABLE = false;
+ }
+ }
}
\ No newline at end of file
diff --git a/main/src/main/java/net/citizensnpcs/util/Messages.java b/main/src/main/java/net/citizensnpcs/util/Messages.java
index 2e3366a7b..9c70e30c6 100644
--- a/main/src/main/java/net/citizensnpcs/util/Messages.java
+++ b/main/src/main/java/net/citizensnpcs/util/Messages.java
@@ -414,6 +414,7 @@ public class Messages {
public static final String TEMPLATE_APPLIED = "citizens.commands.template.applied";
public static final String TEMPLATE_CONFLICT = "citizens.commands.template.conflict";
public static final String TEMPLATE_DELETED = "citizens.commands.template.delete.deleted";
+ public static final String TEMPLATE_GENERATED = "citizens.commands.template.generate.generated";
public static final String TEMPLATE_LIST_HEADER = "citizens.commands.template.list.header";
public static final String TEMPLATE_MISSING = "citizens.commands.template.missing";
public static final String TEMPLATE_NAMESPACE_EXISTS = "citizens.commands.template.namespace-already-exists";
diff --git a/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java b/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java
index 965eaae8b..a4dc45cb6 100644
--- a/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java
+++ b/main/src/main/java/net/citizensnpcs/util/MojangSkinGenerator.java
@@ -30,7 +30,7 @@ public class MojangSkinGenerator {
con.setRequestProperty("User-Agent", "Citizens/2.0");
con.setRequestProperty("Cache-Control", "no-cache");
con.setRequestProperty("Content-Type", "multipart/form-data;boundary=*****");
- con.setConnectTimeout(1000);
+ con.setConnectTimeout(2000);
con.setReadTimeout(30000);
out = new DataOutputStream(con.getOutputStream());
out.writeBytes("--*****\r\n");
@@ -91,7 +91,7 @@ public class MojangSkinGenerator {
con.setRequestProperty("Cache-Control", "no-cache");
con.setRequestProperty("Accept", "application/json");
con.setRequestProperty("Content-Type", "application/json");
- con.setConnectTimeout(1000);
+ con.setConnectTimeout(2000);
con.setReadTimeout(30000);
out = new DataOutputStream(con.getOutputStream());
JSONObject req = new JSONObject();
diff --git a/main/src/main/resources/en.json b/main/src/main/resources/en.json
index bb47092a3..159c2bb22 100644
--- a/main/src/main/resources/en.json
+++ b/main/src/main/resources/en.json
@@ -618,6 +618,8 @@
"citizens.commands.requirements.must-have-selected" : "You must have an NPC selected to execute that command.",
"citizens.commands.requirements.too-few-arguments" : "Too few arguments.",
"citizens.commands.requirements.too-many-arguments" : "Too many arguments. ",
+ "citizens.commands.template.generate.generated" : "Generated template from [[{0}]]",
+ "citizens.commands.template.generate.description" : "Generate a template from the selected NPC",
"citizens.commands.template.applied" : "Applied [[{0}]] to [[{1}]].",
"citizens.commands.template.apply.description" : "Applies a template to the selected NPC",
"citizens.commands.template.apply.help" : "",
diff --git a/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java b/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java
index 7a03ad376..ffc3d1a1d 100644
--- a/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java
+++ b/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java
@@ -700,7 +700,8 @@ public class NMSImpl implements NMSBridge {
@Override
public float getRidingHeightOffset(org.bukkit.entity.Entity entity, org.bukkit.entity.Entity mount) {
- return (float) getHandle(entity).getPassengerRidingPosition(getHandle(mount)).y;
+ Entity handle = getHandle(entity);
+ return (float) (handle.getPassengerRidingPosition(getHandle(mount)).y - handle.position().y);
}
@Override
@@ -911,8 +912,7 @@ public class NMSImpl implements NMSBridge {
@Override
public void linkTextInteraction(org.bukkit.entity.Player player, org.bukkit.entity.Entity entity,
org.bukkit.entity.Entity mount, double offset) {
- Interaction handle = (Interaction) getHandle(entity);
- offset += handle.getPassengerRidingPosition(getHandle(mount)).y;
+ offset += getRidingHeightOffset(entity, mount);
sendPacket(player,
new ClientboundBundlePacket(List.of(
new ClientboundSetEntityDataPacket(entity.getEntityId(),