Jan 1 місяць тому
коміт
50c09cbea1
32 змінених файлів з 2136 додано та 0 видалено
  1. 113 0
      .gitignore
  2. 82 0
      pom.xml
  3. 36 0
      src/main/java/me/lethunderhawk/bazaarflux/service/Services.java
  4. 66 0
      src/main/java/me/lethunderhawk/bazaarflux/util/CustomHeadCreator.java
  5. 12 0
      src/main/java/me/lethunderhawk/bazaarflux/util/MessageSender.java
  6. 53 0
      src/main/java/me/lethunderhawk/bazaarflux/util/animation/Animation.java
  7. 71 0
      src/main/java/me/lethunderhawk/bazaarflux/util/command/CommandNode.java
  8. 141 0
      src/main/java/me/lethunderhawk/bazaarflux/util/command/CustomCommand.java
  9. 46 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/ConfirmationMenu.java
  10. 28 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/GoBackItem.java
  11. 152 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryGUI.java
  12. 104 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryManager.java
  13. 55 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/PlayerHeadListGUI.java
  14. 137 0
      src/main/java/me/lethunderhawk/bazaarflux/util/gui/input/SignMenuFactory.java
  15. 29 0
      src/main/java/me/lethunderhawk/bazaarflux/util/interfaces/BazaarFluxModule.java
  16. 254 0
      src/main/java/me/lethunderhawk/bazaarflux/util/itemdesign/LoreDesigner.java
  17. 25 0
      src/main/java/me/lethunderhawk/bazaarflux/util/lang/RomanNumbers.java
  18. 142 0
      src/main/java/me/lethunderhawk/bazaarflux/util/loottables/RiggedChanceGenerator.java
  19. 15 0
      src/main/java/me/lethunderhawk/main/Main.java
  20. 36 0
      src/main/java/me/lethunderhawk/main/util/UnItalic.java
  21. 34 0
      src/main/java/me/lethunderhawk/npc/NPCModule.java
  22. 34 0
      src/main/java/me/lethunderhawk/npc/command/NPCCommand.java
  23. 16 0
      src/main/java/me/lethunderhawk/npc/event/NPCClickAction.java
  24. 42 0
      src/main/java/me/lethunderhawk/npc/event/NPCInteractionEvent.java
  25. 15 0
      src/main/java/me/lethunderhawk/npc/event/NPCListener.java
  26. 104 0
      src/main/java/me/lethunderhawk/npc/manager/NPCManager.java
  27. 5 0
      src/main/java/me/lethunderhawk/npc/util/NMSHelper.java
  28. 13 0
      src/main/java/me/lethunderhawk/npc/util/NPC.java
  29. 58 0
      src/main/java/me/lethunderhawk/npc/util/NPCOptions.java
  30. 22 0
      src/main/java/me/lethunderhawk/npc/util/string/StringUtility.java
  31. 173 0
      src/main/java/me/lethunderhawk/npc/util/versioned/NPC_1_21_10.java
  32. 23 0
      src/main/resources/plugin.yml

+ 113 - 0
.gitignore

@@ -0,0 +1,113 @@
+# User-specific stuff
+.idea/
+
+*.iml
+*.ipr
+*.iws
+
+# IntelliJ
+out/
+
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+target/
+
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+.flattened-pom.xml
+
+# Common working directory
+run/

+ 82 - 0
pom.xml

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>me.lethunderhawk</groupId>
+    <artifactId>FluxNPC</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <name>FluxNPC</name>
+
+    <properties>
+        <java.version>21</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>21</source>
+                    <target>21</target>
+                    <release>21</release>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.2.4</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <createDependencyReducedPom>false</createDependencyReducedPom>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+    </build>
+
+    <repositories>
+        <repository>
+            <id>papermc</id>
+            <url>https://repo.papermc.io/repository/maven-public/</url>
+        </repository>
+        <repository>
+            <id>sonatype</id>
+            <url>https://oss.sonatype.org/content/groups/public/</url>
+        </repository>
+    </repositories>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.papermc.paper</groupId>
+            <artifactId>paper-api</artifactId>
+            <version>1.21.10-R0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.spigotmc</groupId>
+            <artifactId>spigot</artifactId>
+            <version>1.21.10-R0.1-SNAPSHOT</version>
+            <classifier>remapped-mojang</classifier>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 36 - 0
src/main/java/me/lethunderhawk/bazaarflux/service/Services.java

@@ -0,0 +1,36 @@
+package me.lethunderhawk.bazaarflux.service;
+
+import me.lethunderhawk.bazaarflux.util.interfaces.BazaarFluxModule;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public final class Services {
+
+    private static final Map<Class<?>, Object> services = new ConcurrentHashMap<>();
+
+    public static <T> T register(Class<T> type, T service) {
+        services.put(type, service);
+        return service;
+    }
+    public static <T> void registerModule(Class<? extends BazaarFluxModule> type, T service) {
+        services.put(type, service);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T get(Class<T> type) {
+        T service = (T) services.get(type);
+        if(service == null) {
+            throw new RuntimeException("No service registered for " + type);
+        }
+        return service;
+    }
+    @SuppressWarnings("unchecked")
+    public static <T> T unregister(Class<T> type) {
+        return (T) services.remove(type);
+    }
+
+    public static <T> void unregisterModule(Class<? extends BazaarFluxModule> type) {
+        services.remove(type);
+    }
+}

+ 66 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/CustomHeadCreator.java

@@ -0,0 +1,66 @@
+package me.lethunderhawk.bazaarflux.util;
+
+import com.destroystokyo.paper.profile.PlayerProfile;
+import com.destroystokyo.paper.profile.ProfileProperty;
+import me.lethunderhawk.main.util.UnItalic;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.SkullMeta;
+
+import java.util.List;
+import java.util.UUID;
+
+public class CustomHeadCreator {
+
+    public static ItemStack createCustomHead(String textureValue, Component displayName, Component loreText) {
+        ItemStack playerHead = createCustomHead(textureValue);
+        SkullMeta meta = (SkullMeta) playerHead.getItemMeta();
+
+        meta.displayName(displayName);
+        meta.lore(List.of(loreText));
+
+        playerHead.setItemMeta(meta);
+
+        return playerHead;
+    }
+    public static ItemStack createCustomHead(String textureValue, Component displayName, List<Component> loreText) {
+        ItemStack playerHead = createCustomHead(textureValue);
+        SkullMeta meta = (SkullMeta) playerHead.getItemMeta();
+
+        meta.displayName(displayName);
+        meta.lore(loreText);
+
+        playerHead.setItemMeta(meta);
+
+        return playerHead;
+    }
+    public static ItemStack createCustomHead(Player player, Component displayName, List<Component> loreText) {
+
+        ItemStack playerHead = new ItemStack(Material.PLAYER_HEAD);
+        SkullMeta meta = (SkullMeta) playerHead.getItemMeta();
+
+        meta.setOwningPlayer(player);
+        meta.displayName(displayName);
+        meta.lore(loreText);
+
+        playerHead.setItemMeta(UnItalic.removeItalicFromMeta(meta));
+        return playerHead;
+    }
+
+    public static ItemStack createCustomHead(String textureValue) {
+        ItemStack head = new ItemStack(Material.PLAYER_HEAD);
+        SkullMeta meta = (SkullMeta) head.getItemMeta();
+
+        if (meta != null) {
+            PlayerProfile profile = Bukkit.createProfile(UUID.randomUUID(), null);
+            profile.setProperty(new ProfileProperty("textures", textureValue));
+            meta.setPlayerProfile(profile);
+            head.setItemMeta(meta);
+        }
+
+        return head;
+    }
+}

+ 12 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/MessageSender.java

@@ -0,0 +1,12 @@
+package me.lethunderhawk.bazaarflux.util;
+
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+
+public class MessageSender {
+    public static void sendText(Audience receiver, Component message, String prefix) {
+        receiver.sendMessage(Component.text(prefix + " ", NamedTextColor.GOLD)
+                .append(message));
+    }
+}

+ 53 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/animation/Animation.java

@@ -0,0 +1,53 @@
+package me.lethunderhawk.bazaarflux.util.animation;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.main.Main;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.scheduler.BukkitTask;
+
+public abstract class Animation {
+
+    protected final JavaPlugin plugin;
+    protected final World world;
+    protected int tick = 0;
+    protected int duration;
+
+    private BukkitTask task;
+    private Runnable onComplete;
+
+    protected Animation(World world, int duration) {
+        this.plugin = Services.get(Main.class);
+        this.world = world;
+        this.duration = duration;
+    }
+
+    public final void start() {
+        onStart();
+        task = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
+            if (tick >= duration) {
+                stop(true);
+                return;
+            }
+            onTick(tick);
+            tick++;
+        }, 0L, 1L);
+    }
+
+    public final void stop(boolean completed) {
+        if (task != null) task.cancel();
+        onEnd();
+        if (completed && onComplete != null) {
+            onComplete.run();
+        }
+    }
+
+    public void onComplete(Runnable runnable) {
+        this.onComplete = runnable;
+    }
+
+    protected abstract void onStart();
+    protected abstract void onTick(int tick);
+    protected abstract void onEnd();
+}

+ 71 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/command/CommandNode.java

@@ -0,0 +1,71 @@
+package me.lethunderhawk.bazaarflux.util.command;
+
+import org.bukkit.command.CommandSender;
+
+import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+
+public class CommandNode {
+    private final String name;
+    private final String description;
+    private BiConsumer<CommandSender, String[]> executor;
+    private BiFunction<CommandSender, String[], List<String>> tabCompleter;
+    private Map<String, CommandNode> subCommands = new HashMap<>();
+    private CommandNode parent;
+
+    public CommandNode(String name, String description, BiConsumer<CommandSender, String[]> executor) {
+        this.name = name;
+        this.description = description;
+        this.executor = executor;
+    }
+
+    public CommandNode registerSubCommand(String name, String description, BiConsumer<CommandSender, String[]> executor) {
+        CommandNode subCommand = new CommandNode(name, description, executor);
+        subCommand.parent = this;
+        subCommands.put(name.toLowerCase(), subCommand);
+        return subCommand;
+    }
+
+    public void addSubCommand(CommandNode subCommand) {
+        subCommand.parent = this;
+        subCommands.put(subCommand.getName().toLowerCase(), subCommand);
+    }
+    public void addSubCommands(CommandNode... subCommandNodes) {
+        for(CommandNode subCommand : subCommandNodes) {
+            subCommand.parent = this;
+            subCommands.put(subCommand.getName().toLowerCase(), subCommand);
+        }
+    }
+
+    public CommandNode getSubCommand(String name) {
+        return subCommands.get(name.toLowerCase());
+    }
+
+    public boolean hasSubCommand(String name) {
+        return subCommands.containsKey(name.toLowerCase());
+    }
+
+    public Collection<CommandNode> getSubCommands() {
+        return subCommands.values();
+    }
+
+    public List<String> getSubCommandNames() {
+        return new ArrayList<>(subCommands.keySet());
+    }
+
+    public void setTabCompleter(BiFunction<CommandSender, String[], List<String>> tabCompleter) {
+        this.tabCompleter = tabCompleter;
+    }
+
+    // Getters
+    public String getName() { return name; }
+    public String getDescription() { return description; }
+    public BiConsumer<CommandSender, String[]> getExecutor() { return executor; }
+    public BiFunction<CommandSender, String[], List<String>> getTabCompleter() { return tabCompleter; }
+    public CommandNode getParent() { return parent; }
+
+    public void setExecutor(BiConsumer<CommandSender, String[]> executor) {
+        this.executor = executor;
+    }
+}

+ 141 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/command/CustomCommand.java

@@ -0,0 +1,141 @@
+package me.lethunderhawk.bazaarflux.util.command;
+
+import me.lethunderhawk.bazaarflux.util.interfaces.BazaarFluxModule;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+public abstract class CustomCommand implements CommandExecutor, TabCompleter {
+
+    protected final CommandNode rootCommand;
+    protected final BazaarFluxModule module;
+    public CustomCommand(CommandNode rootCommand, BazaarFluxModule module) {
+        this.rootCommand = rootCommand;
+        this.module = module;
+        createHelpCommand();
+        createCommands();
+    }
+
+    private void createHelpCommand() {
+        rootCommand.addSubCommand(new CommandNode("help", "Displays this help menu", this::sendHelp));
+    }
+
+    public abstract void createCommands();
+
+    @Override
+    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+        if (args.length == 0) {
+            sendHelp(sender);
+            return true;
+        }
+
+        List<String> argList = new ArrayList<>(Arrays.asList(args));
+        CommandNode currentNode = rootCommand;
+        CommandNode targetNode = null;
+
+        // Traverse the command tree
+        while (!argList.isEmpty()) {
+            String nextArg = argList.get(0);
+            CommandNode nextNode = currentNode.getSubCommand(nextArg);
+
+            if (nextNode == null) {
+                // No matching subcommand found, use current node if it has an executor
+                targetNode = currentNode.getExecutor() != null ? currentNode : null;
+                break;
+            }
+
+            currentNode = nextNode;
+            argList.remove(0);
+
+            // If this is the last argument or node has no further subcommands
+            if (argList.isEmpty() || nextNode.getSubCommands().isEmpty()) {
+                targetNode = nextNode;
+                break;
+            }
+        }
+
+        if (targetNode == null) {
+            module.sendText(sender, Component.text("Unknown command. Use /" +rootCommand.getName() +" help for available commands.", NamedTextColor.RED));
+            return true;
+        }
+
+        if (targetNode.getExecutor() == null) {
+            module.sendText(sender,Component.text("This command requires additional arguments.", NamedTextColor.RED));
+            sendSubCommands(sender, targetNode);
+            return true;
+        }
+
+        // Execute the command with remaining arguments
+        String[] remainingArgs = argList.toArray(new String[0]);
+        targetNode.getExecutor().accept(sender, remainingArgs);
+        return true;
+    }
+    private void sendHelp(CommandSender sender, String[] strings) {
+        sendHelp(sender);
+    }
+    protected void sendHelp(CommandSender sender) {
+        sender.sendMessage("§6=== Available Commands ===");
+        for (CommandNode cmd : rootCommand.getSubCommands()) {
+            sender.sendMessage("/" + rootCommand.getName() + " " + cmd.getName() + " §7- " + cmd.getDescription());
+        }
+    }
+
+    private void sendSubCommands(CommandSender sender, CommandNode node) {
+        sender.sendMessage("§6Available subcommands:");
+        for (CommandNode subCmd : node.getSubCommands()) {
+            sender.sendMessage("§e" + subCmd.getName() + " §7- " + subCmd.getDescription());
+        }
+    }
+
+    public CommandNode registerCommand(String name, String description, BiConsumer<CommandSender, String[]> executor) {
+        return rootCommand.registerSubCommand(name, description, executor);
+    }
+
+    @Override
+    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
+        List<String> suggestions = new ArrayList<>();
+
+        if (args.length == 0) {
+            return suggestions;
+        }
+
+        // Start at root and traverse
+        CommandNode currentNode = rootCommand;
+        List<String> argList = new ArrayList<>(Arrays.asList(args));
+        String lastArg = args[args.length - 1].toLowerCase();
+
+        // Try to traverse as far as possible
+        for (int i = 0; i < argList.size() - 1; i++) {
+            String arg = argList.get(i);
+            CommandNode nextNode = currentNode.getSubCommand(arg);
+            if (nextNode == null) {
+                break;
+            }
+            currentNode = nextNode;
+        }
+
+        // Get suggestions from current node
+        if (currentNode.getTabCompleter() != null) {
+            suggestions.addAll(currentNode.getTabCompleter().apply(sender, args));
+        } else {
+            // Suggest subcommands
+            suggestions.addAll(currentNode.getSubCommandNames().stream()
+                    .filter(name -> name.toLowerCase().startsWith(lastArg))
+                    .collect(Collectors.toList()));
+        }
+
+        return suggestions;
+    }
+    public void reload(){
+        createCommands();
+    }
+}

+ 46 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/gui/ConfirmationMenu.java

@@ -0,0 +1,46 @@
+package me.lethunderhawk.bazaarflux.util.gui;
+
+import me.lethunderhawk.main.util.UnItalic;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.function.Consumer;
+
+public class ConfirmationMenu extends InventoryGUI {
+    private final Consumer<Player> callback;
+
+    public ConfirmationMenu(String title, Consumer<Player> callback ) {
+        super(title, 27);
+        this.callback = callback;
+        setupItems();
+    }
+
+    private void setupItems() {
+        fillGlassPaneBackground();
+
+        ItemStack IAmSure = new ItemStack(Material.LIME_STAINED_GLASS_PANE);
+        ItemMeta meta = IAmSure.getItemMeta();
+        meta.displayName(Component.text("Yes, i am sure", NamedTextColor.GREEN));
+        IAmSure.setItemMeta(UnItalic.removeItalicFromMeta(meta));
+        setItemWithClickAction(11, IAmSure, (p, type)->{
+            callback.accept(p);
+            openPrevious(p);
+        });
+        ItemStack NoPleaseNot = new ItemStack(Material.BARRIER);
+        ItemMeta noMeta = NoPleaseNot.getItemMeta();
+        noMeta.displayName(Component.text("No, i changed my mind", NamedTextColor.RED));
+        NoPleaseNot.setItemMeta(UnItalic.removeItalicFromMeta(noMeta));
+        setItemWithClickAction(15, NoPleaseNot, (p, type)->{
+            openPrevious(p);
+        });
+    }
+
+    @Override
+    public void update() {
+
+    }
+}

+ 28 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/gui/GoBackItem.java

@@ -0,0 +1,28 @@
+package me.lethunderhawk.bazaarflux.util.gui;
+
+import me.lethunderhawk.main.util.UnItalic;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.List;
+
+public class GoBackItem extends ItemStack {
+    public GoBackItem() {
+        super(Material.ARROW);
+
+        setItemMeta(buildItemMeta());
+    }
+
+    private ItemMeta buildItemMeta() {
+        ItemMeta meta = getItemMeta();
+        meta.displayName(Component.text("Go back", NamedTextColor.RED));
+        meta.lore(List.of(
+                Component.text("Go back to the previous menu!", NamedTextColor.GRAY)
+        ));
+        UnItalic.removeItalicFromMeta(meta);
+        return meta;
+    }
+}

+ 152 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryGUI.java

@@ -0,0 +1,152 @@
+package me.lethunderhawk.bazaarflux.util.gui;
+
+import me.lethunderhawk.main.util.UnItalic;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryHolder;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Stack;
+import java.util.function.BiConsumer;
+
+/**
+ * Generic, reusable Inventory GUI.
+ */
+public abstract class InventoryGUI implements InventoryHolder {
+
+    private final String title;
+    private final int size;
+    private final Inventory inventory;
+
+    private final Map<Integer, BiConsumer<Player, ClickType>> slotActions = new HashMap<>();
+    private final Stack<InventoryGUI> previousGuis = new Stack<>();
+
+    public InventoryGUI(String title, int size) {
+        this.title = title;
+        this.size = size;
+        this.inventory = Bukkit.createInventory(this, size, title);
+    }
+
+    public void handleClick(Player player, int slot, ClickType type) {
+        BiConsumer<Player, ClickType> action = slotActions.get(slot);
+        if (action != null) {
+            action.accept(player, type);
+        }
+    }
+    /**
+     * Sets an item in a specific slot.
+     */
+    public void setItem(int slot, ItemStack item) {
+        inventory.setItem(slot, item);
+    }
+
+    /**
+     * Sets an item and assigns a click action for that slot.
+     */
+    public void setItemWithClickAction(int slot, ItemStack item, BiConsumer<Player, ClickType> action) {
+        inventory.setItem(slot, item);
+        slotActions.put(slot, action);
+    }
+
+    /**
+     * Fills all empty slots with a background item.
+     */
+    public void fillGlassPaneBackground(){
+        ItemStack background = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
+        ItemMeta meta = background.getItemMeta();
+        meta.displayName(Component.text(" "));
+        meta.setHideTooltip(true);
+        background.setItemMeta(meta);
+        for (int i = 0; i < size; i++) {
+            if (inventory.getItem(i) == null) {
+                inventory.setItem(i, background);
+            }
+        }
+    }
+    public void fillBackground(Material material, String displayName) {
+        ItemStack background = new ItemStack(material);
+        ItemMeta meta = background.getItemMeta();
+        meta.displayName(Component.text(displayName));
+        meta.setHideTooltip(true);
+        background.setItemMeta(meta);
+        for (int i = 0; i < size; i++) {
+            if (inventory.getItem(i) == null) {
+                inventory.setItem(i, background);
+            }
+        }
+    }
+    public void openWithListener(Player player){
+        InventoryManager.openFor(player, this);
+    }
+    /**
+     * Opens this GUI for a player.
+     */
+    public void open(Player player) {
+        player.openInventory(inventory);
+    }
+
+    /**
+     * Opens another GUI and remembers this one for navigation.
+     */
+    public void openNext(Player player, InventoryGUI nextGui) {
+        nextGui.previousGuis.push(this);
+        InventoryManager.openFor(player, nextGui);
+    }
+    public boolean hasPreviousGUI() {
+        return !previousGuis.isEmpty();
+    }
+
+    /**
+     * Opens the previous GUI if available.
+     */
+    public void openPrevious(Player player) {
+        if (!previousGuis.isEmpty()) {
+            InventoryGUI previousGui = previousGuis.pop();
+            previousGui.update();
+            InventoryManager.openFor(player, previousGui);
+        }
+    }
+    public void setBackButton(int slot){
+        ItemStack item = new ItemStack(Material.ARROW);
+        ItemMeta meta = item.getItemMeta();
+        meta.displayName(Component.text("Go Back", NamedTextColor.GREEN));
+        item.setItemMeta(UnItalic.removeItalicFromMeta(meta));
+        setItemWithClickAction(slot, item, (p, type) -> {
+            openPrevious(p);
+        });
+    }
+    public void setCloseButton(int slot){
+        ItemStack item = new ItemStack(Material.BARRIER);
+        ItemMeta meta = item.getItemMeta();
+        meta.displayName(Component.text("Close", NamedTextColor.RED));
+        item.setItemMeta(UnItalic.removeItalicFromMeta(meta));
+        setItemWithClickAction(slot, item, (p, type) -> {
+            InventoryManager.close(p.getUniqueId());
+        });
+    }
+
+    // Also need to update the overloaded version:
+    public void openPrevious(Player player, ClickType type) {
+        openPrevious(player);
+    }
+
+    @Override
+    public @NotNull Inventory getInventory() {
+        return inventory;
+    }
+
+    public abstract void update();
+
+    public interface AutoCloseHandler {
+        void onClosedByPlayer(Player player);
+    }
+}

+ 104 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/gui/InventoryManager.java

@@ -0,0 +1,104 @@
+package me.lethunderhawk.bazaarflux.util.gui;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Central registry & listener for open InventoryGUI instances.
+ * Register once: InventoryManager.register(plugin);
+ */
+public final class InventoryManager implements Listener {
+    private static final Map<UUID, InventoryGUI> openGuis = new ConcurrentHashMap<>();
+    private static volatile boolean registered = false;
+    private static Plugin providerPlugin;
+
+    public static void register(Plugin plugin) {
+        if (registered) return;
+        providerPlugin = plugin;
+        Bukkit.getPluginManager().registerEvents(new InventoryManager(), plugin);
+        registered = true;
+    }
+
+    public static void openFor(Player player, InventoryGUI gui) {
+        // ensure main thread
+        if (!Bukkit.isPrimaryThread()) {
+            Bukkit.getScheduler().runTask(providerPlugin, () -> openFor(player, gui));
+            return;
+        }
+
+        // Close current inventory if open
+        InventoryGUI current = openGuis.get(player.getUniqueId());
+        if (current != null) {
+            openGuis.remove(player.getUniqueId());
+        }
+
+        // Open new GUI
+        openGuis.put(player.getUniqueId(), gui);
+        gui.open(player);
+    }
+
+    public static InventoryGUI getOpen(UUID playerId) {
+        return openGuis.get(playerId);
+    }
+
+    public static void close(@NotNull UUID playerId) {
+        InventoryGUI gui = openGuis.remove(playerId);
+        if (gui != null) {
+            Player p = Bukkit.getPlayer(playerId);
+            if(p!= null) p.closeInventory();
+        }
+    }
+
+    @EventHandler
+    public void onInventoryClick(InventoryClickEvent event) {
+        if (!(event.getWhoClicked() instanceof Player player)) return;
+        if (!(event.getView().getTopInventory().getHolder() instanceof InventoryGUI gui)) return;
+
+        // ignore clicks outside top inventory
+        if (event.getRawSlot() >= event.getView().getTopInventory().getSize()) return;
+
+        InventoryGUI open = openGuis.get(player.getUniqueId());
+        if (open == null || open != gui) return; // ensure it's the tracked instance
+
+        event.setCancelled(true);
+        gui.handleClick(player, event.getRawSlot(), event.getClick());
+    }
+
+    @EventHandler
+    public void onInventoryClose(InventoryCloseEvent event) {
+        if (!(event.getPlayer() instanceof Player player)) return;
+        InventoryGUI gui = openGuis.get(player.getUniqueId());
+        if (gui == null) return;
+
+        // If the closed inventory is the tracked GUI
+        if (event.getView().getTopInventory().getHolder() == gui) {
+            // Check if this is an intentional navigation (opening a new GUI)
+            // If the player is opening a new GUI, it will be handled by openFor
+            // We should only treat it as accidental if reason is not OPEN_NEW
+            // and the player doesn't have another GUI queued up
+            if (event.getReason() != InventoryCloseEvent.Reason.OPEN_NEW) {
+                // Small delay to check if another GUI is being opened
+                Bukkit.getScheduler().runTask(providerPlugin, () -> {
+                    // If still the same GUI (no new GUI was opened), treat as accidental close
+                    if (openGuis.get(player.getUniqueId()) == gui) {
+                        if (gui instanceof InventoryGUI.AutoCloseHandler handler) {
+                            handler.onClosedByPlayer(player);
+                        }
+                        openGuis.remove(player.getUniqueId());
+                    }
+                });
+            }
+        }
+    }
+}

+ 55 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/gui/PlayerHeadListGUI.java

@@ -0,0 +1,55 @@
+package me.lethunderhawk.bazaarflux.util.gui;
+
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.SkullMeta;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+
+/**
+ * GUI displaying a list of players as clickable heads.
+ */
+public class PlayerHeadListGUI extends InventoryGUI {
+
+    public PlayerHeadListGUI(
+            String title,
+            int size,
+            List<Player> players,
+            BiConsumer<Player, Player> onHeadClick
+    ) {
+        super(title, size);
+
+        int slot = 0;
+        for (Player target : players) {
+            if (slot >= size) break;
+
+            ItemStack head = createPlayerHead(target);
+            int finalSlot = slot;
+
+            setItemWithClickAction(finalSlot, head, (clickingPlayer, type) ->
+                    onHeadClick.accept(clickingPlayer, target)
+            );
+
+            slot++;
+        }
+
+        fillBackground(Material.GRAY_STAINED_GLASS_PANE, " ");
+    }
+
+
+    private ItemStack createPlayerHead(Player player) {
+        ItemStack head = new ItemStack(Material.PLAYER_HEAD);
+        SkullMeta meta = (SkullMeta) head.getItemMeta();
+        meta.setOwningPlayer(player);
+        meta.setDisplayName(player.getName());
+        head.setItemMeta(meta);
+        return head;
+    }
+
+    @Override
+    public void update() {
+
+    }
+}

+ 137 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/gui/input/SignMenuFactory.java

@@ -0,0 +1,137 @@
+package me.lethunderhawk.bazaarflux.util.gui.input;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.ProtocolLibrary;
+import com.comphenix.protocol.events.PacketAdapter;
+import com.comphenix.protocol.events.PacketContainer;
+import com.comphenix.protocol.events.PacketEvent;
+import com.comphenix.protocol.wrappers.BlockPosition;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.Plugin;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiPredicate;
+
+public final class SignMenuFactory {
+
+    private final Plugin plugin;
+
+    private final Map<Player, Menu> inputs;
+
+    public SignMenuFactory(Plugin plugin) {
+        this.plugin = plugin;
+        this.inputs = new ConcurrentHashMap<>();
+        this.listen();
+    }
+
+    public Menu newMenu(List<String> text) {
+        return new Menu(text);
+    }
+
+    private void listen() {
+        ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(this.plugin, PacketType.Play.Client.UPDATE_SIGN) {
+            @Override
+            public void onPacketReceiving(PacketEvent event) {
+                Player player = event.getPlayer();
+                Menu menu = inputs.remove(player);
+                if (menu == null) return;
+
+                event.setCancelled(true);
+
+                String[] lines = event.getPacket().getStringArrays().read(0);
+
+                Bukkit.getScheduler().runTask(plugin, () -> {
+                    boolean success = menu.response.test(player, lines);
+
+                    if (!success && menu.reopenIfFail && !menu.forceClose) {
+                        Bukkit.getScheduler().runTaskLater(plugin, () -> menu.open(player), 2L);
+                    }
+                    if (player.isOnline()) {
+                        player.sendBlockChange(
+                                menu.location,
+                                menu.location.getBlock().getBlockData()
+                        );
+                    }
+                });
+            }
+        });
+    }
+
+    public final class Menu {
+
+        private final List<String> text;
+
+        private BiPredicate<Player, String[]> response;
+        private boolean reopenIfFail;
+
+        private Location location;
+
+        private boolean forceClose;
+
+        Menu(List<String> text) {
+            this.text = text;
+        }
+
+        public Menu reopenIfFail(boolean value) {
+            this.reopenIfFail = value;
+            return this;
+        }
+
+        public Menu response(BiPredicate<Player, String[]> response) {
+            this.response = response;
+            return this;
+        }
+
+        public void open(Player player) {
+            Objects.requireNonNull(player, "player");
+            if (!player.isOnline()) {
+                return;
+            }
+            location = player.getLocation();
+            location.setY(location.getBlockY() - 4);
+
+            player.sendBlockChange(location, Material.OAK_SIGN.createBlockData());
+            player.sendSignChange(
+                    location,
+                    text.stream().map(this::color).toList().toArray(new String[4])
+            );
+
+            PacketContainer openSign = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.OPEN_SIGN_EDITOR);
+            BlockPosition position = new BlockPosition(location.getBlockX(), location.getBlockY(), location.getBlockZ());
+            openSign.getBlockPositionModifier().write(0, position);
+            openSign.getBooleans().write(0, true); // ADDED
+            ProtocolLibrary.getProtocolManager().sendServerPacket(player, openSign);
+
+            inputs.put(player, this);
+        }
+
+        /**
+         * closes the menu. if force is true, the menu will close and will ignore the reopen
+         * functionality. false by default.
+         *
+         * @param player the player
+         * @param force  decides whether it will reopen if reopen is enabled
+         */
+        public void close(Player player, boolean force) {
+            this.forceClose = force;
+            if (player.isOnline()) {
+                player.closeInventory();
+            }
+        }
+
+        public void close(Player player) {
+            close(player, false);
+        }
+
+        private String color(String input) {
+            return ChatColor.translateAlternateColorCodes('&', input);
+        }
+    }
+}

+ 29 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/interfaces/BazaarFluxModule.java

@@ -0,0 +1,29 @@
+package me.lethunderhawk.bazaarflux.util.interfaces;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.bazaarflux.util.MessageSender;
+import me.lethunderhawk.main.Main;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import org.bukkit.command.CommandSender;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public abstract class BazaarFluxModule{
+    protected JavaPlugin plugin;
+    public BazaarFluxModule() {
+        this.plugin = Services.get(Main.class);
+    }
+    public abstract String getPrefix();
+    public abstract void onEnable();
+    public abstract void onDisable();
+    public void sendText(Audience receiver, Component infoText){
+        MessageSender.sendText(receiver, infoText, getPrefix());
+    }
+
+    public void reload(CommandSender sender, String[] strings) {
+        if(sender.isOp()){
+            onDisable();
+            onEnable();
+        }
+    }
+}

+ 254 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/itemdesign/LoreDesigner.java

@@ -0,0 +1,254 @@
+package me.lethunderhawk.bazaarflux.util.itemdesign;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.ComponentBuilder;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+public final class LoreDesigner {
+
+    private LoreDesigner() {}
+    public static Component createSingle(String text) {
+        if (text == null || text.isBlank()) {
+            return Component.empty();
+        }
+
+        StyleState style = new StyleState();
+        ComponentBuilder<TextComponent, TextComponent.Builder> builder = Component.text();
+
+        int i = 0;
+        while (i < text.length()) {
+            if (text.charAt(i) == '<') {
+                int end = text.indexOf('>', i);
+                if (end != -1) {
+                    String token = text.substring(i + 1, end).toLowerCase();
+                    applyToken(style, token);
+                    i = end + 1;
+                    continue;
+                }
+            }
+
+            char c = text.charAt(i);
+            builder.append(Component.text(String.valueOf(c), style.toStyle()));
+            i++;
+        }
+
+        return builder.build();
+    }
+    public static List<Component> createLore(String text, String widthReference) {
+        int maxLength = widthReference.length();
+        if (text == null || text.isBlank() || maxLength <= 0) {
+            return List.of();
+        }
+
+        List<Component> lore = new ArrayList<>();
+
+        StyleState style = new StyleState();
+        ComponentBuilder<TextComponent, TextComponent.Builder> lineBuilder = Component.text();
+        int currentLength = 0;
+
+        String[] lines = text.split("<br>", -1);
+
+        for (int i = 0; i < lines.length; i++) {
+
+            String[] wrapParts = lines[i].split(" ");
+
+            for (int p = 0; p < wrapParts.length; p++) {
+
+                ParsedWord parsed = parseWord(wrapParts[p], style);
+                appendWord(
+                        lore,
+                        parsed,
+                        maxLength,
+                        style,
+                        Holder.of(lineBuilder, currentLength)
+                );
+
+                currentLength = Holder.length;
+                lineBuilder = Holder.builder;
+
+                // add space between parts
+                if (p < wrapParts.length - 1) {
+                    lineBuilder.append(Component.space());
+                    currentLength++;
+                }
+            }
+
+            currentLength = Holder.length;
+            lineBuilder = Holder.builder;
+
+            // force line break if <br> occurred
+            if (i < lines.length - 1) {
+                lore.add(lineBuilder.build());
+                lineBuilder = Component.text().style(style.toStyle());
+                currentLength = 0;
+            }
+        }
+
+        if (currentLength > 0) {
+            lore.add(lineBuilder.build());
+        }
+
+        return lore;
+    }
+
+    // -------------------- Append Logic --------------------
+
+    private static void appendWord(
+            List<Component> lore,
+            ParsedWord parsed,
+            int maxLength,
+            StyleState style,
+            Holder holder
+    ) {
+        int wordLength = parsed.visibleLength();
+
+        if (holder.length > 0 && holder.length + 1 + wordLength > maxLength) {
+            lore.add(holder.builder.build());
+            holder.builder = Component.text().style(style.toStyle());
+            holder.length = 0;
+        }
+
+        holder.builder.append(parsed.component());
+        holder.length += wordLength;
+    }
+
+    // -------------------- Parsing --------------------
+
+    private static ParsedWord parseWord(String raw, StyleState style) {
+        ComponentBuilder<TextComponent, TextComponent.Builder> builder = Component.text();
+        int visibleLength = 0;
+
+        int i = 0;
+        while (i < raw.length()) {
+            if (raw.charAt(i) == '<') {
+                int end = raw.indexOf('>', i);
+                if (end != -1) {
+                    String token = raw.substring(i + 1, end).toLowerCase();
+                    applyToken(style, token);
+                    i = end + 1;
+                    continue;
+                }
+            }
+
+            char c = raw.charAt(i);
+            builder.append(Component.text(String.valueOf(c), style.toStyle()));
+            visibleLength++;
+            i++;
+        }
+
+        return new ParsedWord(builder.build(), visibleLength);
+    }
+
+    private static void applyToken(StyleState style, String token) {
+
+        if (token.startsWith("/")) {
+            String closing = token.substring(1);
+
+            switch (closing) {
+                case "bold" -> style.undecorate(TextDecoration.BOLD);
+                case "italic" -> style.undecorate(TextDecoration.ITALIC);
+                case "underlined" -> style.undecorate(TextDecoration.UNDERLINED);
+                case "strikethrough" -> style.undecorate(TextDecoration.STRIKETHROUGH);
+                case "obfuscated" -> style.undecorate(TextDecoration.OBFUSCATED);
+                default -> {
+                    // closing color tag like </red>
+                    if (NAMED_COLORS.containsKey(closing)) {
+                        style.color(style.defaultColor);
+                    }
+                }
+            }
+            return;
+        }
+
+        switch (token) {
+            case "reset" -> style.reset();
+
+            case "bold" -> style.decorate(TextDecoration.BOLD);
+            case "italic" -> style.decorate(TextDecoration.ITALIC);
+            case "underlined" -> style.decorate(TextDecoration.UNDERLINED);
+            case "strikethrough" -> style.decorate(TextDecoration.STRIKETHROUGH);
+            case "obfuscated" -> style.decorate(TextDecoration.OBFUSCATED);
+
+            default -> {
+                NamedTextColor color = NAMED_COLORS.get(token);
+                if (color != null) {
+                    style.color(color);
+                }
+            }
+        }
+    }
+
+    // -------------------- Helpers --------------------
+
+    private static final Map<String, NamedTextColor> NAMED_COLORS = Map.ofEntries(
+            Map.entry("black", NamedTextColor.BLACK),
+            Map.entry("dark_blue", NamedTextColor.DARK_BLUE),
+            Map.entry("dark_green", NamedTextColor.DARK_GREEN),
+            Map.entry("dark_aqua", NamedTextColor.DARK_AQUA),
+            Map.entry("dark_red", NamedTextColor.DARK_RED),
+            Map.entry("dark_purple", NamedTextColor.DARK_PURPLE),
+            Map.entry("gold", NamedTextColor.GOLD),
+            Map.entry("gray", NamedTextColor.GRAY),
+            Map.entry("dark_gray", NamedTextColor.DARK_GRAY),
+            Map.entry("blue", NamedTextColor.BLUE),
+            Map.entry("green", NamedTextColor.GREEN),
+            Map.entry("aqua", NamedTextColor.AQUA),
+            Map.entry("red", NamedTextColor.RED),
+            Map.entry("light_purple", NamedTextColor.LIGHT_PURPLE),
+            Map.entry("yellow", NamedTextColor.YELLOW),
+            Map.entry("white", NamedTextColor.WHITE)
+    );
+
+    private record ParsedWord(Component component, int visibleLength) {}
+
+    private static final class StyleState {
+        private final NamedTextColor defaultColor = NamedTextColor.GRAY;
+
+        private NamedTextColor color = defaultColor;
+        private final EnumSet<TextDecoration> decorations = EnumSet.noneOf(TextDecoration.class);
+
+        void color(NamedTextColor color) {
+            this.color = color;
+        }
+        void undecorate(TextDecoration decoration) {
+            decorations.remove(decoration);
+        }
+        void decorate(TextDecoration decoration) {
+            decorations.add(decoration);
+        }
+
+        void reset() {
+            color = defaultColor;
+            decorations.clear();
+        }
+
+        net.kyori.adventure.text.format.Style toStyle() {
+            net.kyori.adventure.text.format.Style.Builder style =
+                    net.kyori.adventure.text.format.Style.style();
+            if (color != null) style.color(color);
+            for (TextDecoration d : decorations) style.decorate(d);
+            return style.build();
+        }
+    }
+
+    /**
+     * Mutable holder to avoid excessive object creation
+     */
+    private static final class Holder {
+        static ComponentBuilder builder;
+        static int length;
+
+        static Holder of(ComponentBuilder b, int l) {
+            builder = b;
+            length = l;
+            return null;
+        }
+    }
+}

+ 25 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/lang/RomanNumbers.java

@@ -0,0 +1,25 @@
+package me.lethunderhawk.bazaarflux.util.lang;
+
+public class RomanNumbers {
+    public static String intToRoman(int num) {
+        // Arrays to hold the Roman numeral symbols and their corresponding integer values
+        String[] romanSymbols = {
+                "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"
+        };
+        int[] romanValues = {
+                1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1
+        };
+
+        StringBuilder result = new StringBuilder();
+
+        // Loop through the values to create the Roman numeral
+        for (int i = 0; i < romanValues.length; i++) {
+            while (num >= romanValues[i]) {
+                result.append(romanSymbols[i]);
+                num -= romanValues[i];
+            }
+        }
+
+        return result.toString();
+    }
+}

+ 142 - 0
src/main/java/me/lethunderhawk/bazaarflux/util/loottables/RiggedChanceGenerator.java

@@ -0,0 +1,142 @@
+package me.lethunderhawk.bazaarflux.util.loottables;
+
+import java.util.*;
+import java.util.function.Supplier;
+
+public class RiggedChanceGenerator<T> {
+
+    private static final Random RANDOM = new Random();
+
+    // Sequential rigged steps
+    private final List<ChanceStep<T>> steps = new ArrayList<>();
+
+    // Final weighted pool
+    private final List<WeightedChance<T>> finalChances = new ArrayList<>();
+
+    private Supplier<T> fallback;
+
+    /* =======================
+       PRE-STEPS (RIGGED)
+       ======================= */
+
+    public RiggedChanceGenerator<T> addStep(int numerator, int denominator, T result) {
+        steps.add(new ChanceStep<>(numerator, denominator, () -> result));
+        return this;
+    }
+
+    public RiggedChanceGenerator<T> addStep(int numerator, int denominator, Supplier<T> resultSupplier) {
+        steps.add(new ChanceStep<>(numerator, denominator, resultSupplier));
+        return this;
+    }
+
+    /* =======================
+       FINAL POOL (DICE)
+       ======================= */
+
+    /**
+     * Adds a weighted chance to the final roll.
+     * Weights do NOT need to sum to 100.
+     */
+    public RiggedChanceGenerator<T> addChance(int weight, T result) {
+        finalChances.add(new WeightedChance<>(weight, () -> result));
+        return this;
+    }
+
+    public RiggedChanceGenerator<T> addChance(int weight, Supplier<T> resultSupplier) {
+        finalChances.add(new WeightedChance<>(weight, resultSupplier));
+        return this;
+    }
+
+    /* =======================
+       FALLBACK
+       ======================= */
+
+    public RiggedChanceGenerator<T> fallback(T fallbackResult) {
+        this.fallback = () -> fallbackResult;
+        return this;
+    }
+
+    public RiggedChanceGenerator<T> fallback(Supplier<T> fallbackSupplier) {
+        this.fallback = fallbackSupplier;
+        return this;
+    }
+
+    /* =======================
+       EXECUTION
+       ======================= */
+
+    public T roll() {
+
+        // 1. Rigged pre-steps
+        for (ChanceStep<T> step : steps) {
+            if (step.roll()) {
+                return step.result();
+            }
+        }
+
+        // 2. Final weighted dice
+        if (!finalChances.isEmpty()) {
+            int totalWeight = finalChances.stream().mapToInt(c -> c.weight).sum();
+            int roll = RANDOM.nextInt(totalWeight);
+
+            int cumulative = 0;
+            for (WeightedChance<T> chance : finalChances) {
+                cumulative += chance.weight;
+                if (roll < cumulative) {
+                    return chance.result();
+                }
+            }
+        }
+
+        // 3. Fallback
+        if (fallback == null) {
+            throw new IllegalStateException("No fallback defined");
+        }
+        return fallback.get();
+    }
+
+    /* =======================
+       INTERNAL CLASSES
+       ======================= */
+
+    private static final class ChanceStep<T> {
+        private final int numerator;
+        private final int denominator;
+        private final Supplier<T> result;
+
+        private ChanceStep(int numerator, int denominator, Supplier<T> result) {
+            if (numerator <= 0 || denominator <= 0 || numerator > denominator) {
+                throw new IllegalArgumentException("Invalid chance: " + numerator + "/" + denominator);
+            }
+            this.numerator = numerator;
+            this.denominator = denominator;
+            this.result = result;
+        }
+
+        private boolean roll() {
+            return RANDOM.nextInt(denominator) < numerator;
+        }
+
+        private T result() {
+            return result.get();
+        }
+    }
+
+    private static final class WeightedChance<T> {
+        private final int weight;
+        private final Supplier<T> result;
+
+        private WeightedChance(int weight, Supplier<T> result) {
+            if (weight <= 0) {
+                throw new IllegalArgumentException("Weight must be > 0");
+            }
+            this.weight = weight;
+            this.result = result;
+        }
+
+        private T result() {
+            return result.get();
+        }
+    }
+}
+

+ 15 - 0
src/main/java/me/lethunderhawk/main/Main.java

@@ -0,0 +1,15 @@
+package me.lethunderhawk.main;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.npc.manager.NPCManager;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public class Main extends JavaPlugin {
+
+    @Override
+    public void onEnable() {
+        NPCManager manager = new NPCManager();
+        Services.register(NPCManager.class, manager);
+
+    }
+}

+ 36 - 0
src/main/java/me/lethunderhawk/main/util/UnItalic.java

@@ -0,0 +1,36 @@
+package me.lethunderhawk.main.util;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class UnItalic {
+    public static Component removeItalic(Component component){
+        return component.decoration(TextDecoration.ITALIC, false);
+    }
+    public static List<Component> removeItalic(List<Component> components){
+        List<Component> newComponents = new ArrayList<>();
+        for(Component component : components){
+            newComponents.add(component.decoration(TextDecoration.ITALIC, false));
+        }
+        return newComponents;
+    }
+    public static Component text(String text){
+        return removeItalic(Component.text(text, NamedTextColor.GRAY));
+    }
+
+    /**
+     *
+     * @param itemMeta The {@link ItemMeta} you want to remove the Italic from
+     * @return The {@link ItemMeta} with Italic style removed from lore and displayName
+     */
+    public static ItemMeta removeItalicFromMeta(ItemMeta itemMeta) {
+        if(itemMeta.hasLore()) itemMeta.lore(removeItalic(itemMeta.lore()));
+        if(itemMeta.hasDisplayName()) itemMeta.displayName(removeItalic(itemMeta.displayName()));
+        return itemMeta;
+    }
+}

+ 34 - 0
src/main/java/me/lethunderhawk/npc/NPCModule.java

@@ -0,0 +1,34 @@
+package me.lethunderhawk.npc;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.bazaarflux.util.interfaces.BazaarFluxModule;
+import me.lethunderhawk.npc.command.NPCCommand;
+import me.lethunderhawk.npc.event.NPCListener;
+import me.lethunderhawk.npc.manager.NPCManager;
+import org.bukkit.event.HandlerList;
+
+public class NPCModule extends BazaarFluxModule{
+
+    private NPCManager npcManager;
+    private NPCListener npcListener;
+
+    public String getPrefix() {
+        return "[NPC]";
+    }
+
+    public void onEnable() {
+        this.npcManager = new NPCManager();
+        plugin.getCommand("npc").setExecutor(new NPCCommand(this));
+        Services.register(NPCManager.class, npcManager);
+
+        this.npcListener = new NPCListener();
+        plugin.getServer().getPluginManager().registerEvents(npcListener, plugin);
+    }
+
+    public void onDisable() {
+        npcManager.deleteAllNPCs();
+        HandlerList.unregisterAll(npcListener);
+        npcManager = null;
+        npcListener = null;
+    }
+}

+ 34 - 0
src/main/java/me/lethunderhawk/npc/command/NPCCommand.java

@@ -0,0 +1,34 @@
+package me.lethunderhawk.npc.command;
+
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.bazaarflux.util.command.CommandNode;
+import me.lethunderhawk.bazaarflux.util.command.CustomCommand;
+import me.lethunderhawk.bazaarflux.util.interfaces.BazaarFluxModule;
+import me.lethunderhawk.npc.manager.NPCManager;
+import me.lethunderhawk.npc.util.NPC;
+import me.lethunderhawk.npc.util.NPCOptions;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class NPCCommand extends CustomCommand {
+    public NPCCommand(BazaarFluxModule module) {
+        super(new CommandNode("npc", "Base npc command", null), module);
+    }
+
+    @Override
+    public void createCommands() {
+        registerCommand("create", "Test creation command", this::createNPC);
+    }
+
+    private void createNPC(CommandSender sender, String[] strings) {
+        if(!(sender instanceof Player p)) return;
+        NPCOptions npcOptions = new NPCOptions();
+        npcOptions.setName(p.getName());
+        npcOptions.setLocation(p.getLocation());
+        npcOptions.setHideNametag(false);
+
+        NPC npc = Services.get(NPCManager.class).newNPC(npcOptions);
+        npc.showTo(p);
+    }
+
+}

+ 16 - 0
src/main/java/me/lethunderhawk/npc/event/NPCClickAction.java

@@ -0,0 +1,16 @@
+package me.lethunderhawk.npc.event;
+
+import com.comphenix.protocol.wrappers.EnumWrappers;
+
+public enum NPCClickAction {
+    INTERACT, INTERACT_AT, ATTACK;
+
+    public static NPCClickAction fromProtocolLibAction(EnumWrappers.EntityUseAction action) {
+        switch (action) {
+            case ATTACK: return ATTACK;
+            case INTERACT: return INTERACT;
+            case INTERACT_AT: return INTERACT_AT;
+            default: return null;
+        }
+    }
+}

+ 42 - 0
src/main/java/me/lethunderhawk/npc/event/NPCInteractionEvent.java

@@ -0,0 +1,42 @@
+package me.lethunderhawk.npc.event;
+
+import me.lethunderhawk.npc.util.NPC;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+
+public class NPCInteractionEvent extends Event {
+    private static final HandlerList HANDLER_LIST = new HandlerList();
+
+    private final NPC clicked;
+
+    private final Player player;
+
+    private final NPCClickAction clickAction;
+
+    public NPCInteractionEvent(NPC clicked, Player player, NPCClickAction clickAction) {
+        this.clicked = clicked;
+        this.player = player;
+        this.clickAction = clickAction;
+    }
+
+    @Override
+    public HandlerList getHandlers() {
+        return HANDLER_LIST;
+    }
+
+    public static HandlerList getHandlerList() {
+        return HANDLER_LIST;
+    }
+
+    public NPC getClicked() {
+        return clicked;
+    }
+
+    public Player getPlayer() {
+        return player;
+    }
+    public NPCClickAction getClickAction() {
+        return clickAction;
+    }
+}

+ 15 - 0
src/main/java/me/lethunderhawk/npc/event/NPCListener.java

@@ -0,0 +1,15 @@
+package me.lethunderhawk.npc.event;
+
+import me.lethunderhawk.npc.util.NPC;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+
+public class NPCListener implements Listener {
+
+    @EventHandler
+    private void onNPCClick(NPCInteractionEvent event) {
+        NPC clicked = event.getClicked();
+        event.getPlayer().sendMessage("<" + clicked.getName() + "> Sorry, I don't think you're looking for me.");
+    }
+}
+

+ 104 - 0
src/main/java/me/lethunderhawk/npc/manager/NPCManager.java

@@ -0,0 +1,104 @@
+package me.lethunderhawk.npc.manager;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.ProtocolLibrary;
+import com.comphenix.protocol.events.PacketAdapter;
+import com.comphenix.protocol.events.PacketContainer;
+import com.comphenix.protocol.events.PacketEvent;
+import com.comphenix.protocol.wrappers.EnumWrappers;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import me.lethunderhawk.bazaarflux.service.Services;
+import me.lethunderhawk.main.Main;
+import me.lethunderhawk.npc.event.NPCClickAction;
+import me.lethunderhawk.npc.event.NPCInteractionEvent;
+import me.lethunderhawk.npc.util.NPC;
+import me.lethunderhawk.npc.util.NPCOptions;
+import me.lethunderhawk.npc.util.versioned.NPC_1_21_10;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+public class NPCManager {
+
+    private final Set<NPC> registeredNPCs = new HashSet<>();
+    private final JavaPlugin plugin;
+
+    public NPCManager() {
+        this.plugin = Services.get(Main.class);
+        ProtocolLibrary.getProtocolManager().addPacketListener(
+                new PacketAdapter(plugin, PacketType.Play.Client.USE_ENTITY) {
+                    @Override
+                    public void onPacketReceiving(PacketEvent event) {
+                        PacketContainer packet = event.getPacket();
+
+                        int id = packet.getIntegers().read(0);
+                        com.comphenix.protocol.wrappers.WrappedEnumEntityUseAction useAction = packet.getEnumEntityUseActions().read(0);
+                        if(useAction == null) return;
+                        EnumWrappers.Hand hand = useAction.getHand();
+                        EnumWrappers.EntityUseAction action = packet.getEntityUseActions().readSafely(0);
+                        if(action == null) return;
+                        if(hand == EnumWrappers.Hand.MAIN_HAND && action == EnumWrappers.EntityUseAction.INTERACT){
+                            handleEntityClick(event.getPlayer(), id, NPCClickAction.INTERACT);
+                            return;
+                        }
+                        if(hand == EnumWrappers.Hand.MAIN_HAND && action == EnumWrappers.EntityUseAction.ATTACK){
+                            handleEntityClick(event.getPlayer(), id, NPCClickAction.ATTACK);
+                            return;
+                        }
+
+                    }
+                }
+        );
+    }
+
+    private final Cache<Player, NPC> clickedNPCCache = CacheBuilder.newBuilder()
+            .expireAfterWrite(1L, TimeUnit.SECONDS)
+            .build();
+
+    private void handleEntityClick(Player player, int entityId, NPCClickAction action) {
+        registeredNPCs.stream()
+                .filter(npc -> npc.getId() == entityId)
+                .forEach(npc -> Bukkit.getServer().getScheduler().runTaskLater(plugin, () -> {
+                    /*NPC previouslyClickedNPC = clickedNPCCache.getIfPresent(player);
+                    if (previouslyClickedNPC != null && previouslyClickedNPC.equals(npc)) return; // If they've clicked this same NPC in the last 0.5 seconds ignore this click
+                    clickedNPCCache.put(player, npc);*/
+
+                    NPCInteractionEvent event = new NPCInteractionEvent(npc, player, action);
+                    Bukkit.getPluginManager().callEvent(event);
+                }, 2));
+    }
+
+    public NPC newNPC(NPCOptions options) {
+        NPC npc = new NPC_1_21_10(options.getName(), options.getLocation());
+        registeredNPCs.add(npc);
+        return npc;
+    }
+
+    public Optional<NPC> findNPC(String name) {
+        return registeredNPCs.stream()
+                .filter(npc -> npc.getName().equalsIgnoreCase(name))
+                .findFirst();
+    }
+
+    public void deleteNPC(NPC npc) {
+        npc.delete();
+        registeredNPCs.remove(npc);
+    }
+
+    public void deleteAllNPCs() {
+        // Copy the set to prevent concurrent modification exception
+        Set<NPC> npcsCopy = new HashSet<>(registeredNPCs);
+        npcsCopy.forEach(this::deleteNPC);
+    }
+    public NPC getNPCbyId(int id) {
+        if(registeredNPCs.isEmpty()) return null;
+        Optional<NPC> optionalNPC = registeredNPCs.stream().filter(npc -> npc.getId() == id).findFirst();
+        return optionalNPC.orElse(null);
+    }
+}

+ 5 - 0
src/main/java/me/lethunderhawk/npc/util/NMSHelper.java

@@ -0,0 +1,5 @@
+package me.lethunderhawk.npc.util;
+
+public class NMSHelper {
+
+}

+ 13 - 0
src/main/java/me/lethunderhawk/npc/util/NPC.java

@@ -0,0 +1,13 @@
+package me.lethunderhawk.npc.util;
+
+import org.bukkit.Location;
+import org.bukkit.entity.Player;
+
+public interface NPC {
+    String getName();
+    void showTo(Player player);
+    void hideFrom(Player player);
+    void delete();
+    Location getLocation();
+    int getId();
+}

+ 58 - 0
src/main/java/me/lethunderhawk/npc/util/NPCOptions.java

@@ -0,0 +1,58 @@
+package me.lethunderhawk.npc.util;
+
+import org.bukkit.Location;
+
+public class NPCOptions {
+    public String getName() {
+        return name;
+    }
+
+    public String getTexture() {
+        return texture;
+    }
+
+    public String getSignature() {
+        return signature;
+    }
+
+    public Location getLocation() {
+        return location;
+    }
+
+    public boolean isHideNametag() {
+        return hideNametag;
+    }
+
+
+
+    private String name;
+    private String texture;
+    private String signature;
+    private Location location;
+    private boolean hideNametag;
+
+    public NPCOptions setName(String name) {
+        this.name = name;
+        return this;
+    }
+
+    public NPCOptions setTexture(String texture) {
+        this.texture = texture;
+        return this;
+    }
+
+    public NPCOptions setSignature(String signature) {
+        this.signature = signature;
+        return this;
+    }
+
+    public NPCOptions setLocation(Location location) {
+        this.location = location;
+        return this;
+    }
+
+    public NPCOptions setHideNametag(boolean hideNametag) {
+        this.hideNametag = hideNametag;
+        return this;
+    }
+}

+ 22 - 0
src/main/java/me/lethunderhawk/npc/util/string/StringUtility.java

@@ -0,0 +1,22 @@
+package me.lethunderhawk.npc.util.string;
+
+import java.util.Random;
+
+public class StringUtility {
+    public static String randomCharacters(int length) {
+        if (length < 1) {
+            throw new IllegalArgumentException("Invalid length. Length must be at least 1 characters");
+        }
+
+        int leftLimit = 97; // letter 'a'
+        int rightLimit = 122; // letter 'z'
+        Random random = new Random();
+        StringBuilder buffer = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            int randomLimitedInt = leftLimit + (int)
+                    (random.nextFloat() * (rightLimit - leftLimit + 1));
+            buffer.append((char) randomLimitedInt);
+        }
+        return buffer.toString();
+    }
+}

+ 173 - 0
src/main/java/me/lethunderhawk/npc/util/versioned/NPC_1_21_10.java

@@ -0,0 +1,173 @@
+package me.lethunderhawk.npc.util.versioned;
+
+import com.mojang.authlib.GameProfile;
+import me.lethunderhawk.npc.util.NPC;
+import net.minecraft.network.Connection;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.PacketFlow;
+import net.minecraft.network.protocol.game.ClientGamePacketListener;
+import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket;
+import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ClientInformation;
+import net.minecraft.server.level.ServerEntity;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.network.CommonListenerCookie;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.craftbukkit.v1_21_R6.CraftServer;
+import org.bukkit.craftbukkit.v1_21_R6.CraftWorld;
+import org.bukkit.craftbukkit.v1_21_R6.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Predicate;
+
+public class NPC_1_21_10 implements NPC {
+    private final UUID uuid = UUID.randomUUID();
+    private final String name;
+
+    private ServerPlayer npc;
+    private ServerLevel level;
+
+    private final Set<UUID> viewers = new HashSet<>();
+    private MinecraftServer server;
+
+    public NPC_1_21_10(String name, Location location) {
+        this.name = name;
+        spawn(location);
+    }
+
+    // -----------------------------
+    // Core spawn logic
+    // -----------------------------
+    private void spawn(Location location) {
+        if (location.getWorld() == null) {
+            throw new IllegalArgumentException("World cannot be null");
+        }
+
+        this.server =
+                ((CraftServer) Bukkit.getServer()).getServer();
+
+        this.level =
+                ((CraftWorld) location.getWorld()).getHandle();
+
+        GameProfile profile = new GameProfile(uuid, name);
+        this.npc = new ServerPlayer(
+                server,
+                level,
+                profile,
+                ClientInformation.createDefault()
+        );
+
+        npc.setPos(
+                location.getX(),
+                location.getY(),
+                location.getZ()
+        );
+
+        npc.setYRot(location.getYaw());
+        npc.setXRot(location.getPitch());
+    }
+
+    // -----------------------------
+    // Show / hide
+    // -----------------------------
+    public void showTo(Player viewer) {
+        if (!viewers.add(viewer.getUniqueId())) return;
+        npc.connection = new ServerGamePacketListenerImpl(
+                server,
+                new Connection(PacketFlow.SERVERBOUND),
+                npc,
+                CommonListenerCookie.createInitial(npc.getGameProfile(), false)
+        );
+
+        ServerGamePacketListenerImpl conn = ((CraftPlayer) viewer).getHandle().connection;
+        conn.send(new ClientboundPlayerInfoUpdatePacket(
+                ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, npc
+        ));
+
+        // Spawn the NPC entity
+        ServerEntity spawnData = new ServerEntity(
+                npc.level(), npc, 0, false, this.getDummySynchronizer(), Set.of()
+        );
+        conn.send(npc.getAddEntityPacket(spawnData));
+    }
+
+
+
+    public void hideFrom(org.bukkit.entity.Player player) {
+        if (!viewers.remove(player.getUniqueId())) return;
+
+        ((CraftPlayer) player).getHandle()
+                .connection
+                .send(
+                        new ClientboundPlayerInfoRemovePacket(List.of(npc.getUUID()))
+                );
+    }
+
+    public void delete() {
+        viewers.stream()
+                .map(Bukkit::getPlayer)
+                .filter(p -> p != null && p.isOnline())
+                .forEach(this::hideFrom);
+
+        viewers.clear();
+    }
+
+    // -----------------------------
+    // Getters
+    // -----------------------------
+    public int getEntityId() {
+        return npc.getId();
+    }
+
+    public Location getLocation() {
+        return new Location(
+                level.getWorld(),
+                npc.getX(),
+                npc.getY(),
+                npc.getZ(),
+                npc.getYRot(),
+                npc.getXRot()
+        );
+    }
+
+    private ServerEntity.Synchronizer getDummySynchronizer() {
+        return new ServerEntity.Synchronizer() {
+            @Override
+            public void sendToTrackingPlayers(Packet<? super ClientGamePacketListener> packet) {
+
+            }
+
+            @Override
+            public void sendToTrackingPlayersAndSelf(Packet<? super ClientGamePacketListener> packet) {
+
+            }
+
+            @Override
+            public void sendToTrackingPlayersFiltered(Packet<? super ClientGamePacketListener> packet, Predicate<ServerPlayer> predicate) {
+
+            }
+
+            @Override
+            public void sendToTrackingPlayersFilteredAndSelf(Packet<? super ClientGamePacketListener> packet, Predicate<ServerPlayer> predicate) {
+
+            }
+        };
+    }
+
+    @Override
+    public int getId() {
+        return npc.getId();
+    }
+
+    public String getName() {
+        return name;
+    }
+}

+ 23 - 0
src/main/resources/plugin.yml

@@ -0,0 +1,23 @@
+name: BazaarFlux
+version: '${project.version}'
+main: me.lethunderhawk.main.Main
+api-version: 1.21.10
+prefix: BazaarFlux
+commands:
+  eco:
+    description: Economy administration command
+    usage: /eco <set|add|remove|get> <player> <amount>
+    permission: currency.eco
+  trade:
+    description: Sende eine Handelsanfrage
+  tradeaccept:
+    description: Akzeptiere eine Handelsanfrage
+
+permissions:
+  currency.eco:
+    description: Allows economy administration
+    default: op
+  trade.trade:
+    description: Allows initiating a trade
+  trade.acceptTrade:
+    description: Allows accepting a trade