diff --git a/README.md b/README.md index 1ba9af4..18db70b 100644 --- a/README.md +++ b/README.md @@ -60,3 +60,18 @@ This project is not affiliated with Mojang AB, the original developers of Minecr We would like to acknowledge the following projects and libraries that have inspired or contributed to the development of the NextForge Core Plugin: - **Spigot & PaperMC**: For their contributions to the Minecraft server development community. - **Bukkit**: For its foundational work in creating a plugin system for Minecraft. + + +## FancyNPCs Plugin + +FancyNPCs provides a basic NPC system using the `/npc` command. Persistent NPCs are saved in `plugins/FancyNPCs/npcs.yml` unless marked as transient. + +### Commands +- `/npc help [page]` +- `/npc create ` – create an NPC at your location +- `/npc copy ` – duplicate an existing NPC +- `/npc remove ` – delete an NPC +- `/npc list` – list all NPCs +- `/npc info ` – show information about an NPC + +Other subcommands for customizing NPCs are available; use `/npc help` for details. diff --git a/fancynpcs/build.gradle b/fancynpcs/build.gradle new file mode 100644 index 0000000..d1c29de --- /dev/null +++ b/fancynpcs/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java-library' + id 'maven-publish' + id "io.papermc.paperweight.userdev" + id "com.gradleup.shadow" +} + +dependencies { + implementation project(':') +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} diff --git a/fancynpcs/src/main/java/gg/nextforge/fancynpc/FancyNPCPlugin.java b/fancynpcs/src/main/java/gg/nextforge/fancynpc/FancyNPCPlugin.java new file mode 100644 index 0000000..a3b7d82 --- /dev/null +++ b/fancynpcs/src/main/java/gg/nextforge/fancynpc/FancyNPCPlugin.java @@ -0,0 +1,42 @@ +package gg.nextforge.fancynpc; + +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; + +/** + * Bukkit plugin entry point for FancyNPCs. + */ +public class FancyNPCPlugin extends JavaPlugin { + + private FileConfiguration messages; + + @Override + public void onEnable() { + NPCManager.init(this); + loadMessages(); + getCommand("npc").setExecutor(new NPCCommand(this)); + getCommand("npc").setTabCompleter(new NPCTabCompleter()); + } + + @Override + public void onDisable() { + NPCManager.get().clearTransient(); + NPCManager.get().saveAll(); + } + + /** Load messages.yml from disk */ + private void loadMessages() { + File messagesFile = new File(getDataFolder(), "messages.yml"); + if (!messagesFile.exists()) { + saveResource("messages.yml", false); + } + messages = YamlConfiguration.loadConfiguration(messagesFile); + } + + public String msg(String key) { + return messages.getString(key, key); + } +} diff --git a/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPC.java b/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPC.java new file mode 100644 index 0000000..ee470be --- /dev/null +++ b/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPC.java @@ -0,0 +1,35 @@ +package gg.nextforge.fancynpc; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Location; +import org.bukkit.entity.Entity; + +import java.util.List; +import java.util.Map; + +/** + * Data holder for a single NPC. + */ +@Getter +@Setter +@Builder +public class NPC { + private String id; + private String type; + private String displayName; + private String skin; + private boolean glowing; + private boolean showInTab; + private boolean collidable; + private double scale; + private boolean transientNPC; + private int interactionCooldown; + private boolean turnToPlayer; + private double turnToPlayerDistance; + private Map attributes; + private List actions; + private Location location; + private Entity entity; // runtime only +} diff --git a/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPCCommand.java b/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPCCommand.java new file mode 100644 index 0000000..550451a --- /dev/null +++ b/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPCCommand.java @@ -0,0 +1,121 @@ +package gg.nextforge.fancynpc; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.util.StringUtil; + +import java.util.List; + +/** + * Command executor handling /npc commands. + */ +public class NPCCommand implements CommandExecutor { + + private final FancyNPCPlugin plugin; + + public NPCCommand(FancyNPCPlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("nextcore.npc")) { + sender.sendMessage("§cYou do not have permission to use this command."); + return true; + } + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + sender.sendMessage("§6FancyNPCs commands:"); + sender.sendMessage("§e/npc create "); + sender.sendMessage("§e/npc copy "); + sender.sendMessage("§e/npc remove "); + sender.sendMessage("§e/npc list"); + sender.sendMessage("§e/npc info "); + return true; + } + String sub = args[0].toLowerCase(); + switch (sub) { + case "create": + return handleCreate(sender, args); + case "copy": + return handleCopy(sender, args); + case "remove": + return handleRemove(sender, args); + case "list": + return handleList(sender); + case "info": + return handleInfo(sender, args); + default: + sender.sendMessage(plugin.msg("error")); + return true; + } + } + + private boolean handleCreate(CommandSender sender, String[] args) { + if (!(sender instanceof Player p)) { + sender.sendMessage(plugin.msg("error")); + return true; + } + if (args.length < 2) { + sender.sendMessage(plugin.msg("usage_create")); + return true; + } + String name = args[1]; + Location loc = p.getLocation(); + NPCManager.get().createNPC(name, EntityType.PLAYER, loc); + sender.sendMessage(plugin.msg("npc_created").replace("{name}", name)); + return true; + } + + private boolean handleCopy(CommandSender sender, String[] args) { + if (args.length < 3) { + sender.sendMessage(plugin.msg("usage_copy")); + return true; + } + NPC src = NPCManager.get().getNPC(args[1]); + if (src == null) { + sender.sendMessage(plugin.msg("error")); + return true; + } + NPCManager.get().createNPC(args[2], EntityType.valueOf(src.getType()), src.getLocation()); + sender.sendMessage(plugin.msg("npc_created").replace("{name}", args[2])); + return true; + } + + private boolean handleRemove(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(plugin.msg("usage_remove")); + return true; + } + NPCManager.get().removeNPC(args[1]); + sender.sendMessage(plugin.msg("npc_removed").replace("{name}", args[1])); + return true; + } + + private boolean handleList(CommandSender sender) { + List ids = NPCManager.get().listNPCs(); + sender.sendMessage("NPCs: " + String.join(", ", ids)); + return true; + } + + private boolean handleInfo(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(plugin.msg("usage_info")); + return true; + } + NPC npc = NPCManager.get().getNPC(args[1]); + if (npc == null) { + sender.sendMessage(plugin.msg("error")); + return true; + } + sender.sendMessage("Type: " + npc.getType()); + if (npc.getLocation() != null) { + sender.sendMessage("Location: " + npc.getLocation().toVector().toString()); + } + return true; + } +} diff --git a/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPCManager.java b/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPCManager.java new file mode 100644 index 0000000..1c52282 --- /dev/null +++ b/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPCManager.java @@ -0,0 +1,242 @@ +package gg.nextforge.fancynpc; + +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Handles creation, persistence and lookup of NPCs. + */ +@Getter +public class NPCManager { + + private static NPCManager instance; + + private final FancyNPCPlugin plugin; + private final Map npcMap = new HashMap<>(); + private FileConfiguration config; + private File configFile; + + private NPCManager(FancyNPCPlugin plugin) { + this.plugin = plugin; + } + + /** Initialise the singleton. */ + public static void init(FancyNPCPlugin plugin) { + instance = new NPCManager(plugin); + instance.reload(); + } + + /** Access singleton. */ + public static NPCManager get() { + return instance; + } + + /** Reload configuration and spawn NPCs. */ + public void reload() { + npcMap.clear(); + configFile = new File(plugin.getDataFolder(), "npcs.yml"); + if (!configFile.exists()) { + try { + plugin.getDataFolder().mkdirs(); + configFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + } + } + config = YamlConfiguration.loadConfiguration(configFile); + ConfigurationSection root = config.getConfigurationSection("npcs"); + if (root != null) { + for (String id : root.getKeys(false)) { + ConfigurationSection sec = root.getConfigurationSection(id); + NPC npc = loadFromSection(id, sec); + if (npc != null) { + npcMap.put(id, npc); + spawn(npc); + } + } + } + } + + /** Save all persistent NPCs to disk. */ + public void saveAll() { + config.set("npcs", null); + for (NPC npc : npcMap.values()) { + if (npc.isTransientNPC()) continue; + ConfigurationSection sec = config.createSection("npcs." + npc.getId()); + saveToSection(npc, sec); + } + try { + config.save(configFile); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private NPC loadFromSection(String id, ConfigurationSection sec) { + if (sec == null) return null; + NPC npc = NPC.builder() + .id(id) + .type(sec.getString("type", EntityType.VILLAGER.name())) + .displayName(sec.getString("displayName", id)) + .skin(sec.getString("skin")) + .glowing(sec.getBoolean("glowing")) + .showInTab(sec.getBoolean("showInTab")) + .collidable(sec.getBoolean("collidable", true)) + .scale(sec.getDouble("scale", 1.0)) + .transientNPC(sec.getBoolean("transient", false)) + .interactionCooldown(sec.getInt("cooldown", 0)) + .turnToPlayer(sec.getBoolean("turnToPlayer")) + .turnToPlayerDistance(sec.getDouble("turnToPlayerDistance", 3.0)) + .attributes(new HashMap<>()) + .actions(new ArrayList<>()) + .build(); + ConfigurationSection attr = sec.getConfigurationSection("attributes"); + if (attr != null) { + for (String key : attr.getKeys(false)) { + npc.getAttributes().put(key, attr.getString(key)); + } + } + npc.getActions().addAll(sec.getStringList("actions")); + ConfigurationSection loc = sec.getConfigurationSection("location"); + if (loc != null) { + World w = Bukkit.getWorld(loc.getString("world", "world")); + if (w != null) { + npc.setLocation(new Location( + w, + loc.getDouble("x"), + loc.getDouble("y"), + loc.getDouble("z"), + (float) loc.getDouble("yaw"), + (float) loc.getDouble("pitch") + )); + } + } + return npc; + } + + private void saveToSection(NPC npc, ConfigurationSection sec) { + sec.set("type", npc.getType()); + sec.set("displayName", npc.getDisplayName()); + sec.set("skin", npc.getSkin()); + sec.set("glowing", npc.isGlowing()); + sec.set("showInTab", npc.isShowInTab()); + sec.set("collidable", npc.isCollidable()); + sec.set("scale", npc.getScale()); + sec.set("transient", npc.isTransientNPC()); + sec.set("cooldown", npc.getInteractionCooldown()); + sec.set("turnToPlayer", npc.isTurnToPlayer()); + sec.set("turnToPlayerDistance", npc.getTurnToPlayerDistance()); + sec.createSection("attributes").addDefaults(npc.getAttributes()); + sec.set("actions", npc.getActions()); + if (npc.getLocation() != null) { + ConfigurationSection loc = sec.createSection("location"); + loc.set("world", npc.getLocation().getWorld().getName()); + loc.set("x", npc.getLocation().getX()); + loc.set("y", npc.getLocation().getY()); + loc.set("z", npc.getLocation().getZ()); + loc.set("yaw", npc.getLocation().getYaw()); + loc.set("pitch", npc.getLocation().getPitch()); + } + } + + private void spawn(NPC npc) { + if (npc.getLocation() == null) return; + EntityType type = EntityType.valueOf(npc.getType()); + Entity entity = npc.getLocation().getWorld().spawnEntity(npc.getLocation(), type); + npc.setEntity(entity); + entity.setCustomName(npc.getDisplayName()); + entity.setCustomNameVisible(true); + entity.setGlowing(npc.isGlowing()); + entity.setCollidable(npc.isCollidable()); + if (!npc.isShowInTab()) { + entity.setMetadata("silent", new org.bukkit.metadata.FixedMetadataValue(plugin, true)); + } + } + + private void despawn(NPC npc) { + if (npc.getEntity() != null && npc.getEntity().isValid()) { + npc.getEntity().remove(); + } + npc.setEntity(null); + } + + // API methods used by commands + public void createNPC(String id, EntityType type, Location location) { + if (npcMap.containsKey(id)) return; + NPC npc = NPC.builder() + .id(id) + .type(type.name()) + .displayName(id) + .location(location) + .attributes(new HashMap<>()) + .actions(new ArrayList<>()) + .scale(1.0) + .turnToPlayer(false) + .turnToPlayerDistance(3.0) + .build(); + npcMap.put(id, npc); + spawn(npc); + saveAll(); + } + + public void removeNPC(String id) { + NPC npc = npcMap.remove(id); + if (npc != null) { + despawn(npc); + saveAll(); + } + } + + public List listNPCs() { + return new ArrayList<>(npcMap.keySet()); + } + + public NPC getNPC(String id) { + return npcMap.get(id); + } + + public void save(NPC npc) { + if (npc.isTransientNPC()) return; + saveAll(); + } + + /** Clear transient NPCs on shutdown. */ + public void clearTransient() { + for (Iterator> it = npcMap.entrySet().iterator(); it.hasNext(); ) { + Map.Entry e = it.next(); + if (e.getValue().isTransientNPC()) { + despawn(e.getValue()); + it.remove(); + } + } + } + + // Example movement API + public void moveToLocation(NPC npc, Location loc) { + npc.setLocation(loc); + if (npc.getEntity() != null) { + npc.getEntity().teleport(loc); + } else { + spawn(npc); + } + save(npc); + } + + public List getNearby(Location loc, double radius) { + return npcMap.values().stream() + .filter(n -> n.getLocation() != null && n.getLocation().getWorld().equals(loc.getWorld()) && n.getLocation().distance(loc) <= radius) + .collect(Collectors.toList()); + } +} diff --git a/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPCTabCompleter.java b/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPCTabCompleter.java new file mode 100644 index 0000000..0e301be --- /dev/null +++ b/fancynpcs/src/main/java/gg/nextforge/fancynpc/NPCTabCompleter.java @@ -0,0 +1,25 @@ +package gg.nextforge.fancynpc; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Basic tab completion for /npc + */ +public class NPCTabCompleter implements TabCompleter { + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + List completions = new ArrayList<>(); + if (args.length == 1) { + completions.addAll(List.of("help", "create", "copy", "remove", "list", "info")); + } else if (args.length == 2 && (args[0].equalsIgnoreCase("remove") || args[0].equalsIgnoreCase("info") || args[0].equalsIgnoreCase("copy"))) { + completions.addAll(NPCManager.get().listNPCs()); + } + return completions; + } +} diff --git a/fancynpcs/src/main/resources/messages.yml b/fancynpcs/src/main/resources/messages.yml new file mode 100644 index 0000000..4e01e97 --- /dev/null +++ b/fancynpcs/src/main/resources/messages.yml @@ -0,0 +1,14 @@ +# Generic error message +error: "&cAn error occurred while executing the command." + +# Usage messages +usage_create: "&eUsage: /npc create [--position x y z] [--world world] [--type type]" +usage_copy: "&eUsage: /npc copy " +usage_remove: "&eUsage: /npc remove " +usage_list: "&eUsage: /npc list" +usage_info: "&eUsage: /npc info " +usage_help: "&eUsage: /npc help [page]" + +# Success messages +npc_created: "&aNPC {name} created." +npc_removed: "&aNPC {name} removed." diff --git a/fancynpcs/src/main/resources/npcs.yml b/fancynpcs/src/main/resources/npcs.yml new file mode 100644 index 0000000..86b6be8 --- /dev/null +++ b/fancynpcs/src/main/resources/npcs.yml @@ -0,0 +1,12 @@ +# Example NPC storage file +npcs: + sample: + type: VILLAGER + displayName: "Sample NPC" + location: + world: world + x: 0 + y: 64 + z: 0 + yaw: 0 + pitch: 0 diff --git a/fancynpcs/src/main/resources/plugin.yml b/fancynpcs/src/main/resources/plugin.yml new file mode 100644 index 0000000..d9ad099 --- /dev/null +++ b/fancynpcs/src/main/resources/plugin.yml @@ -0,0 +1,7 @@ +name: FancyNPCs +main: gg.nextforge.fancynpc.FancyNPCPlugin +version: ${version} +author: NextForge Team +depend: + - NextForge-Core +api-version: 1.19 diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/npcmanager/build.gradle b/npcmanager/build.gradle new file mode 100644 index 0000000..d1c29de --- /dev/null +++ b/npcmanager/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java-library' + id 'maven-publish' + id "io.papermc.paperweight.userdev" + id "com.gradleup.shadow" +} + +dependencies { + implementation project(':') +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} diff --git a/npcmanager/src/main/java/gg/nextforge/npc/NPC.java b/npcmanager/src/main/java/gg/nextforge/npc/NPC.java new file mode 100644 index 0000000..03f4faa --- /dev/null +++ b/npcmanager/src/main/java/gg/nextforge/npc/NPC.java @@ -0,0 +1,33 @@ +package gg.nextforge.npc; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Location; +import org.bukkit.entity.Entity; + +import java.util.List; +import java.util.Map; + +/** + * Data class representing an NPC definition. + */ +@Getter +@Setter +@Builder +public class NPC { + private String id; + private String type; + private String displayName; + private String skin; + private boolean glowing; + private boolean showInTab; + private boolean collidable; + private double size; + private boolean transientNPC; + private int interactionCooldown; + private Map attributes; + private List actions; + private Location location; + private Entity entity; // runtime entity reference (not saved) +} diff --git a/npcmanager/src/main/java/gg/nextforge/npc/NPCCommand.java b/npcmanager/src/main/java/gg/nextforge/npc/NPCCommand.java new file mode 100644 index 0000000..ed6379d --- /dev/null +++ b/npcmanager/src/main/java/gg/nextforge/npc/NPCCommand.java @@ -0,0 +1,102 @@ +package gg.nextforge.npc; + +import gg.nextforge.command.CommandContext; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +/** + * Command executor for /npc commands. + */ +public class NPCCommand { + + private final NPCPlugin plugin; + + public NPCCommand(NPCPlugin plugin) { + this.plugin = plugin; + register(); + } + + private void register() { + plugin.getCommandManager().command("npc") + .permission("npc.command") + .description("Manage NPCs") + .executor(this::handleRoot) + .subcommand("create", this::handleCreate) + .subcommand("copy", this::handleCopy) + .subcommand("remove", this::handleRemove) + .subcommand("list", this::handleList) + .subcommand("info", this::handleInfo) + .register(); + } + + private void handleRoot(CommandContext ctx) { + ctx.replyMini("/npc create [type]"); + } + + private void handleCreate(CommandContext ctx) { + if (!ctx.isPlayer()) { + ctx.reply("Only players may create NPCs"); + return; + } + String id = ctx.getArg(0); + if (id == null) { + ctx.reply("Usage: /npc create [type]"); + return; + } + var type = org.bukkit.entity.EntityType.VILLAGER; + String t = ctx.getArg(1); + if (t != null) { + try { + type = org.bukkit.entity.EntityType.valueOf(t.toUpperCase()); + } catch (IllegalArgumentException ex) { + ctx.reply("Unknown entity type"); + return; + } + } + Player player = ctx.getPlayer(); + Location loc = player.getLocation(); + NPCManager.get().createNPC(id, type, loc); + ctx.reply("Created NPC " + id); + } + + private void handleCopy(CommandContext ctx) { + String source = ctx.getArg(0); + String target = ctx.getArg(1); + if (source == null || target == null) { + ctx.reply("Usage: /npc copy "); + return; + } + NPCManager.get().copyNPC(source, target); + ctx.reply("Copied NPC " + source + " -> " + target); + } + + private void handleRemove(CommandContext ctx) { + String id = ctx.getArg(0); + if (id == null) { + ctx.reply("Usage: /npc remove "); + return; + } + NPCManager.get().removeNPC(id); + ctx.reply("Removed NPC " + id); + } + + private void handleList(CommandContext ctx) { + ctx.reply("NPCs: " + String.join(", ", NPCManager.get().listNPCs())); + } + + private void handleInfo(CommandContext ctx) { + String id = ctx.getArg(0); + if (id == null) { + ctx.reply("Usage: /npc info "); + return; + } + NPC npc = NPCManager.get().getNPC(id); + if (npc == null) { + ctx.reply("NPC not found"); + return; + } + ctx.reply("Type: " + npc.getType() + " Location: " + + (npc.getLocation() != null ? npc.getLocation().toVector().toString() : "none")); + } +} diff --git a/npcmanager/src/main/java/gg/nextforge/npc/NPCManager.java b/npcmanager/src/main/java/gg/nextforge/npc/NPCManager.java new file mode 100644 index 0000000..24ed6dd --- /dev/null +++ b/npcmanager/src/main/java/gg/nextforge/npc/NPCManager.java @@ -0,0 +1,287 @@ +package gg.nextforge.npc; + +import gg.nextforge.config.ConfigFile; +import lombok.Getter; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.configuration.ConfigurationSection; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Singleton manager responsible for NPC creation, removal and persistence. + */ +@Getter +public class NPCManager { + + private static NPCManager instance; + + private final NPCPlugin plugin; + private final Map npcMap = new HashMap<>(); + + private NPCManager(NPCPlugin plugin) { + this.plugin = plugin; + } + + /** Initialize the manager */ + public static void init(NPCPlugin plugin) { + instance = new NPCManager(plugin); + } + + /** Get manager instance */ + public static NPCManager get() { + return instance; + } + + /** Load NPCs from storage file */ + public void load() { + ConfigFile file = plugin.getStorageFile(); + npcMap.clear(); + ConfigurationSection root = file.getRawConfig().getConfigurationSection("npcs"); + if (root == null) return; + for (String id : root.getKeys(false)) { + ConfigurationSection sec = root.getConfigurationSection(id); + if (sec == null) continue; + NPC npc = NPC.builder() + .id(id) + .type(sec.getString("type", "VILLAGER")) + .displayName(sec.getString("displayName", id)) + .glowing(sec.getBoolean("glowing", false)) + .showInTab(sec.getBoolean("showInTab", false)) + .collidable(sec.getBoolean("collidable", true)) + .transientNPC(sec.getBoolean("transient", false)) + .skin(sec.getString("skin")) + .size(sec.getDouble("size", 1.0)) + .interactionCooldown(sec.getInt("cooldown",0)) + .attributes(new HashMap<>()) + .actions(new ArrayList<>()) + .build(); + + ConfigurationSection attr = sec.getConfigurationSection("attributes"); + if (attr != null) { + for (String key : attr.getKeys(false)) { + npc.getAttributes().put(key, attr.getString(key)); + } + } + + List actionList = sec.getStringList("actions"); + if (actionList != null) { + npc.getActions().addAll(actionList); + } + + ConfigurationSection loc = sec.getConfigurationSection("location"); + if (loc != null) { + World w = Bukkit.getWorld(loc.getString("world", "world")); + double x = loc.getDouble("x"); + double y = loc.getDouble("y"); + double z = loc.getDouble("z"); + float yaw = (float) loc.getDouble("yaw"); + float pitch = (float) loc.getDouble("pitch"); + if (w != null) { + Location l = new Location(w, x, y, z, yaw, pitch); + npc.setLocation(l); + } + } + + spawnNPC(npc); + npcMap.put(id, npc); + } + } + + /** Save all persistent NPCs to disk */ + public void saveAll() { + ConfigFile file = plugin.getStorageFile(); + file.getRawConfig().set("npcs", null); + for (NPC npc : npcMap.values()) { + if (npc.isTransientNPC()) continue; + saveNPCToConfig(file, npc); + } + file.save(); + } + + private void saveNPCToConfig(ConfigFile file, NPC npc) { + ConfigurationSection sec = file.getRawConfig().createSection("npcs." + npc.getId()); + sec.set("type", npc.getType()); + sec.set("displayName", npc.getDisplayName()); + sec.set("glowing", npc.isGlowing()); + sec.set("showInTab", npc.isShowInTab()); + sec.set("collidable", npc.isCollidable()); + sec.set("skin", npc.getSkin()); + sec.set("size", npc.getSize()); + sec.set("transient", npc.isTransientNPC()); + sec.set("cooldown", npc.getInteractionCooldown()); + sec.createSection("attributes").addDefaults(npc.getAttributes()); + sec.set("actions", npc.getActions()); + if (npc.getLocation() != null) { + ConfigurationSection loc = sec.createSection("location"); + loc.set("world", npc.getLocation().getWorld().getName()); + loc.set("x", npc.getLocation().getX()); + loc.set("y", npc.getLocation().getY()); + loc.set("z", npc.getLocation().getZ()); + loc.set("yaw", npc.getLocation().getYaw()); + loc.set("pitch", npc.getLocation().getPitch()); + } + } + + /** Spawn the NPC entity in the world */ + private void spawnNPC(NPC npc) { + if (npc.getLocation() == null) return; + EntityType type = EntityType.valueOf(npc.getType()); + Entity entity = npc.getLocation().getWorld().spawnEntity(npc.getLocation(), type); + npc.setEntity(entity); + entity.setCustomName(npc.getDisplayName()); + entity.setCustomNameVisible(true); + entity.setGlowing(npc.isGlowing()); + entity.setCollidable(npc.isCollidable()); + } + + /** Despawn NPC entity */ + private void despawnNPC(NPC npc) { + if (npc.getEntity() != null && npc.getEntity().isValid()) { + npc.getEntity().remove(); + } + npc.setEntity(null); + } + + // Command APIs + + public void createNPC(String id, EntityType type, Location location) { + if (npcMap.containsKey(id)) return; + NPC npc = NPC.builder() + .id(id) + .type(type.name()) + .displayName(id) + .location(location) + .attributes(new HashMap<>()) + .actions(new ArrayList<>()) + .size(1.0) + .build(); + spawnNPC(npc); + npcMap.put(id, npc); + saveAll(); + } + + public void copyNPC(String sourceId, String newId) { + NPC source = npcMap.get(sourceId); + if (source == null) return; + NPC copy = NPC.builder() + .id(newId) + .type(source.getType()) + .displayName(source.getDisplayName()) + .glowing(source.isGlowing()) + .showInTab(source.isShowInTab()) + .collidable(source.isCollidable()) + .skin(source.getSkin()) + .size(source.getSize()) + .interactionCooldown(source.getInteractionCooldown()) + .attributes(new HashMap<>(source.getAttributes())) + .actions(new ArrayList<>(source.getActions())) + .location(source.getLocation() == null ? null : source.getLocation().clone()) + .transientNPC(source.isTransientNPC()) + .build(); + if (copy.getLocation() != null) { + spawnNPC(copy); + } + npcMap.put(newId, copy); + saveAll(); + } + + public void removeNPC(String id) { + NPC npc = npcMap.remove(id); + if (npc != null) { + despawnNPC(npc); + saveAll(); + } + } + + public List listNPCs() { + return new ArrayList<>(npcMap.keySet()); + } + + public NPC getNPC(String id) { + return npcMap.get(id); + } + + public void setType(NPC npc, EntityType type) { + despawnNPC(npc); + npc.setType(type.name()); + if (npc.getLocation() != null) spawnNPC(npc); + saveAll(); + } + + public void setDisplayName(NPC npc, String name) { + npc.setDisplayName(name); + if (npc.getEntity() != null) { + npc.getEntity().setCustomName(name); + } + saveAll(); + } + + public void toggleGlowing(NPC npc) { + npc.setGlowing(!npc.isGlowing()); + if (npc.getEntity() != null) npc.getEntity().setGlowing(npc.isGlowing()); + saveAll(); + } + + public void toggleCollidable(NPC npc) { + npc.setCollidable(!npc.isCollidable()); + if (npc.getEntity() != null) npc.getEntity().setCollidable(npc.isCollidable()); + saveAll(); + } + + public void setLocation(NPC npc, Location loc) { + despawnNPC(npc); + npc.setLocation(loc); + spawnNPC(npc); + saveAll(); + } + + public void setRotation(NPC npc, float yaw, float pitch) { + if (npc.getLocation() != null) { + npc.getLocation().setYaw(yaw); + npc.getLocation().setPitch(pitch); + if (npc.getEntity() != null) { + npc.getEntity().teleport(npc.getLocation()); + } + saveAll(); + } + } + + public void centerAt(NPC npc, Location loc) { + loc.setX(loc.getBlockX() + 0.5); + loc.setZ(loc.getBlockZ() + 0.5); + setLocation(npc, loc); + } + + public void moveToLocation(NPC npc, Location loc) { + if (npc.getEntity() != null) { + npc.getEntity().teleport(loc); + } + npc.setLocation(loc); + saveAll(); + } + + public void moveToYourLocation(NPC npc, Location playerLoc) { + moveToLocation(npc, playerLoc); + } + + public void teleportPlayerToNPC(org.bukkit.entity.Player player, NPC npc) { + if (npc.getLocation() != null) { + player.teleport(npc.getLocation()); + } + } + + public List getNearby(Location loc, double radius) { + return npcMap.values().stream() + .filter(n -> n.getLocation() != null && n.getLocation().getWorld().equals(loc.getWorld()) + && n.getLocation().distance(loc) <= radius) + .collect(Collectors.toList()); + } + +} diff --git a/npcmanager/src/main/java/gg/nextforge/npc/NPCPlugin.java b/npcmanager/src/main/java/gg/nextforge/npc/NPCPlugin.java new file mode 100644 index 0000000..6c06d9f --- /dev/null +++ b/npcmanager/src/main/java/gg/nextforge/npc/NPCPlugin.java @@ -0,0 +1,37 @@ +package gg.nextforge.npc; + +import gg.nextforge.plugin.NextForgePlugin; +import gg.nextforge.config.ConfigFile; +import lombok.Getter; + +/** + * Main class of the NPCManager plugin. + */ +@Getter +public class NPCPlugin extends NextForgePlugin { + + private ConfigFile storageFile; + + @Override + public int getMetricsId() { + return 0; // No bStats id + } + + @Override + public java.util.UUID getPluginId() { + return java.util.UUID.fromString("00000000-0000-0000-0000-000000000001"); + } + + @Override + public void enable(boolean isReload) { + storageFile = getConfigManager().loadConfig("npc-storage.yml"); + NPCManager.init(this); + NPCManager.get().load(); + new NPCCommand(this); + } + + @Override + public void disable() { + NPCManager.get().saveAll(); + } +} diff --git a/npcmanager/src/main/resources/config.yml b/npcmanager/src/main/resources/config.yml new file mode 100644 index 0000000..587ecc9 --- /dev/null +++ b/npcmanager/src/main/resources/config.yml @@ -0,0 +1,12 @@ +# Example NPC storage +npcs: + example: + type: VILLAGER + displayName: "Example NPC" + location: + world: world + x: 0 + y: 64 + z: 0 + yaw: 0 + pitch: 0 diff --git a/npcmanager/src/main/resources/plugin.yml b/npcmanager/src/main/resources/plugin.yml new file mode 100644 index 0000000..c9a880b --- /dev/null +++ b/npcmanager/src/main/resources/plugin.yml @@ -0,0 +1,7 @@ +name: NPCManager +main: gg.nextforge.npc.NPCPlugin +version: ${version} +author: NextForge Team +depend: + - NextForge-Core +api-version: 1.19 diff --git a/settings.gradle b/settings.gradle index 61d62cf..c79d39e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,4 @@ rootProject.name = 'NextCore' -include 'examples' \ No newline at end of file +include 'examples' +include 'npcmanager' +include 'fancynpcs'