فهرست منبع

Dynamically loading and saving SubProfiles; Paginated Inventory; fixed NPC bug where it threw an error on interacting with entities while no npcs have been registered yet

Jan 1 ماه پیش
والد
کامیت
aed10bed03
23فایلهای تغییر یافته به همراه779 افزوده شده و 85 حذف شده
  1. 1 1
      pom.xml
  2. 5 0
      src/main/java/me/lethunderhawk/fluxapi/FluxService.java
  3. 7 3
      src/main/java/me/lethunderhawk/fluxapi/main/FluxAPI.java
  4. 8 6
      src/main/java/me/lethunderhawk/fluxapi/npc/command/NPCCommand.java
  5. 3 0
      src/main/java/me/lethunderhawk/fluxapi/npc/listener/NPCListener.java
  6. 245 0
      src/main/java/me/lethunderhawk/fluxapi/profile/AnnotatedProfileCategory.java
  7. 3 0
      src/main/java/me/lethunderhawk/fluxapi/profile/ProfileAPIModule.java
  8. 1 2
      src/main/java/me/lethunderhawk/fluxapi/profile/ProfileManager.java
  9. 39 42
      src/main/java/me/lethunderhawk/fluxapi/profile/representation/FluxProfile.java
  10. 2 2
      src/main/java/me/lethunderhawk/fluxapi/profile/representation/SubProfileFactory.java
  11. 7 4
      src/main/java/me/lethunderhawk/fluxapi/util/command/CustomCommand.java
  12. 31 10
      src/main/java/me/lethunderhawk/fluxapi/util/config/ConfigLoader.java
  13. 134 7
      src/main/java/me/lethunderhawk/fluxapi/util/gui/InventoryGUI.java
  14. 2 2
      src/main/java/me/lethunderhawk/fluxapi/util/gui/InventoryManager.java
  15. 100 0
      src/main/java/me/lethunderhawk/fluxapi/util/gui/PaginatedInventoryGUI.java
  16. 4 0
      src/main/java/me/lethunderhawk/fluxapi/util/inventoryoverlay/LoreOverlayManager.java
  17. 4 0
      src/main/java/me/lethunderhawk/fluxapi/util/inventoryoverlay/OverlaySession.java
  18. 50 0
      src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/ItemData.java
  19. 49 0
      src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/ItemLoreRenderer.java
  20. 55 0
      src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/ItemLoreTemplate.java
  21. 17 0
      src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/ItemLoreUpdater.java
  22. 9 1
      src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/LoreDesigner.java
  23. 3 5
      src/main/resources/plugin.yml

+ 1 - 1
pom.xml

@@ -6,7 +6,7 @@
 
     <groupId>me.lethunderhawk</groupId>
     <artifactId>FluxAPI</artifactId>
-    <version>1.2.2</version>
+    <version>1.2.5</version>
     <packaging>jar</packaging>
 
     <name>FluxAPI</name>

+ 5 - 0
src/main/java/me/lethunderhawk/fluxapi/FluxService.java

@@ -1,5 +1,6 @@
 package me.lethunderhawk.fluxapi;
 
+import me.lethunderhawk.fluxapi.npc.manager.NPCManager;
 import me.lethunderhawk.fluxapi.util.interfaces.FluxAPIModule;
 
 import java.util.Map;
@@ -33,4 +34,8 @@ public final class FluxService {
     public static <T> void unregisterModule(Class<? extends FluxAPIModule> type) {
         services.remove(type);
     }
+
+    public static boolean isRegistered(Class<NPCManager> npcManagerClass) {
+        return services.containsKey(npcManagerClass);
+    }
 }

+ 7 - 3
src/main/java/me/lethunderhawk/fluxapi/main/FluxAPI.java

@@ -4,6 +4,7 @@ import me.lethunderhawk.fluxapi.FluxService;
 import me.lethunderhawk.fluxapi.npc.NPCAPIModule;
 import me.lethunderhawk.fluxapi.profile.ProfileAPIModule;
 import me.lethunderhawk.fluxapi.util.config.ConfigLoader;
+import me.lethunderhawk.fluxapi.util.gui.InventoryManager;
 import org.bukkit.plugin.java.JavaPlugin;
 
 public class FluxAPI extends JavaPlugin {
@@ -11,17 +12,20 @@ public class FluxAPI extends JavaPlugin {
     @Override
     public void onEnable() {
         FluxService.register(FluxAPI.class, this);
+        InventoryManager.register(this);
 
         ConfigLoader configLoader = new ConfigLoader(this);
         FluxService.register(ConfigLoader.class, configLoader);
 
-        ProfileAPIModule profileModule = new ProfileAPIModule(this);
-        profileModule.onEnable();
-        FluxService.registerModule(ProfileAPIModule.class, profileModule);
+        ProfileAPIModule profileAPIModule = new ProfileAPIModule(this);
+        profileAPIModule.onEnable();
+        FluxService.registerModule(ProfileAPIModule.class, profileAPIModule);
 
         NPCAPIModule npcModule = new NPCAPIModule(this);
         npcModule.onEnable();
         FluxService.registerModule(NPCAPIModule.class, npcModule);
+
+
     }
 
     @Override

+ 8 - 6
src/main/java/me/lethunderhawk/fluxapi/npc/command/NPCCommand.java

@@ -22,7 +22,9 @@ public class NPCCommand extends CustomCommand {
 
     public NPCCommand(FluxAPIModule module) {
         super(module);
-        setRootCommand(new CommandNode("npc", "Base npc command", this::sendHelp));
+    }
+    public void setRootCommand() {
+        this.rootCommand = new CommandNode("npc", "Base npc command", this::sendHelp);
     }
 
     @Override
@@ -37,22 +39,22 @@ public class NPCCommand extends CustomCommand {
     }
 
     private void reload(CommandSender sender, String[] strings) {
-        if(!sender.hasPermission("npc.reload")) return;
+        if(!sender.hasPermission("fluxapi.npc")) return;
         module.reload(sender, strings);
     }
 
     private List<String> npcDeletionCompleter(CommandSender sender, String[] args) {
-        if(!sender.hasPermission("npc.delete")) return null;
+        if(!sender.hasPermission("fluxapi.npc")) return null;
         return npcUUIDCompleter(args);
     }
 
     private List<String> createCompleter(CommandSender sender, String[] args) {
-        if(!sender.hasPermission("npc.create")) return null;
+        if(!sender.hasPermission("fluxapi.npc")) return null;
         return List.of("1. <name>", "2. <Player name for texture>");
     }
 
     private List<String> npcOptionsCompleter(CommandSender sender, String[] args) {
-        if(!sender.hasPermission("npc.edit")) return null;
+        if(!sender.hasPermission("fluxapi.npc")) return null;
         return npcUUIDCompleter(args);
     }
 
@@ -65,7 +67,7 @@ public class NPCCommand extends CustomCommand {
     }
 
     private void showOptionsMenu(CommandSender sender, String[] args) {
-        if(!sender.hasPermission("npc.options")) return;
+        if(!sender.hasPermission("fluxapi.npc")) return;
         if(!(sender instanceof Player player)) return;
         NPC npc = FluxService.get(NPCManager.class).findByUUID(args[0]);
         if(npc != null) {

+ 3 - 0
src/main/java/me/lethunderhawk/fluxapi/npc/listener/NPCListener.java

@@ -15,6 +15,7 @@ import org.bukkit.inventory.EquipmentSlot;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.Collection;
+import java.util.Collections;
 
 public final class NPCListener implements Listener {
     private final int FOCUS_RANGE_SQUARED = 250;
@@ -94,6 +95,7 @@ public final class NPCListener implements Listener {
         }
     }
     private NPC getNPC(@NotNull Entity entity) {
+        if(!FluxService.isRegistered(NPCManager.class)) return null;
         for(NPC npc : FluxService.get(NPCManager.class).getRegisteredNPCs()){
             if(npc.getEntityUUID() == null) continue;
             if(npc.getEntityUUID().equals(entity.getUniqueId())){
@@ -103,6 +105,7 @@ public final class NPCListener implements Listener {
         return null;
     }
     private Collection<NPC> getRegisteredNPCs(){
+        if(!FluxService.isRegistered(NPCManager.class)) return Collections.emptySet();
         return FluxService.get(NPCManager.class).getRegisteredNPCs();
     }
 }

+ 245 - 0
src/main/java/me/lethunderhawk/fluxapi/profile/AnnotatedProfileCategory.java

@@ -0,0 +1,245 @@
+package me.lethunderhawk.fluxapi.profile;
+
+import org.bukkit.configuration.serialization.ConfigurationSerializable;
+import org.bukkit.configuration.serialization.SerializableAs;
+
+import java.lang.reflect.*;
+import java.util.*;
+/**
+ * Every subclass must register via:
+ * ConfigurationSerialization.registerClass(MyClass.class, "MyClass");
+ *
+ * And provide:
+ * public static MyClass deserialize(Map<String, Object> map) {
+ *     return AnnotatedProfileCategory.deserialize(MyClass.class, map);
+ * }
+ */
+@SerializableAs("AnnotatedProfileCategory")
+public abstract class AnnotatedProfileCategory implements ConfigurationSerializable {
+
+    protected AnnotatedProfileCategory() {
+        // Required for Bukkit
+    }
+
+    protected AnnotatedProfileCategory(Map<String, Object> map) {
+        deserializeInto(this, map);
+    }
+
+    /* ------------------------------------------------ */
+    /*                   SERIALIZATION                  */
+    /* ------------------------------------------------ */
+    protected static <T extends AnnotatedProfileCategory> T deserializeFields(
+            Class<T> clazz, Map<String, Object> map) {
+        try {
+            Constructor<T> constructor = clazz.getDeclaredConstructor();
+            constructor.setAccessible(true);
+            T instance = constructor.newInstance();
+            deserializeInto(instance, map);
+            return instance;
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to deserialize " + clazz.getSimpleName(), e);
+        }
+    }
+    @Override
+    public Map<String, Object> serialize() {
+        Map<String, Object> result = new LinkedHashMap<>();
+
+        for (Field field : getAllFields(this.getClass())) {
+            if (field.isAnnotationPresent(AnnotatedSerializable.class)) {
+
+                AnnotatedSerializable annotation = field.getAnnotation(AnnotatedSerializable.class);
+                String key = annotation.prefix();
+
+                try {
+                    field.setAccessible(true);
+                    Object value = field.get(this);
+
+                    if (value == null) continue;
+
+                    // 🔥 ENUM FIELD
+                    if (value instanceof Enum<?>) {
+                        result.put(key, ((Enum<?>) value).name());
+                    }
+
+                    // 🔥 MAP SUPPORT (including enum keys)
+                    else if (value instanceof Map<?, ?> map) {
+
+                        Map<Object, Object> serializedMap = new LinkedHashMap<>();
+
+                        for (Map.Entry<?, ?> entry : map.entrySet()) {
+
+                            Object mapKey = entry.getKey();
+                            Object mapValue = entry.getValue();
+
+                            // Convert enum keys to String
+                            if (mapKey instanceof Enum<?>) {
+                                mapKey = ((Enum<?>) mapKey).name();
+                            }
+
+                            serializedMap.put(mapKey, mapValue);
+                        }
+
+                        result.put(key, serializedMap);
+                    }
+
+                    else {
+                        result.put(key, value);
+                    }
+
+                } catch (IllegalAccessException e) {
+                    throw new RuntimeException("Failed to serialize field: " + field.getName(), e);
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /* ------------------------------------------------ */
+    /*                 DESERIALIZATION                  */
+    /* ------------------------------------------------ */
+
+    @SuppressWarnings("unchecked")
+    public static <T extends AnnotatedProfileCategory> T deserialize(Class<T> clazz, Map<String, Object> map) {
+        try {
+
+            // 1️⃣ Try static deserialize(Map) method
+            try {
+                Method method = clazz.getDeclaredMethod("deserialize", Map.class);
+
+                if (Modifier.isStatic(method.getModifiers())
+                        && clazz.isAssignableFrom(method.getReturnType())) {
+
+                    method.setAccessible(true);
+                    return (T) method.invoke(null, map);
+                }
+            } catch (NoSuchMethodException ignored) {}
+
+            // 2️⃣ Try constructor(Map)
+            try {
+                Constructor<T> constructor = clazz.getDeclaredConstructor(Map.class);
+                constructor.setAccessible(true);
+                return constructor.newInstance(map);
+            } catch (NoSuchMethodException ignored) {}
+
+            // 3️⃣ Fallback: no-args constructor + reflective injection
+            Constructor<T> constructor = clazz.getDeclaredConstructor();
+            constructor.setAccessible(true);
+            T instance = constructor.newInstance();
+
+            deserializeInto(instance, map);
+            return instance;
+
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to deserialize " + clazz.getSimpleName(), e);
+        }
+    }
+
+    private static void deserializeInto(Object instance, Map<String, Object> map) {
+        Class<?> clazz = instance.getClass();
+
+        for (Field field : getAllFields(clazz)) {
+            if (!field.isAnnotationPresent(AnnotatedSerializable.class)) continue;
+
+            AnnotatedSerializable annotation = field.getAnnotation(AnnotatedSerializable.class);
+            String key = annotation.prefix();
+
+            if (!map.containsKey(key)) continue;
+
+            Object value = map.get(key);
+
+            try {
+                field.setAccessible(true);
+
+                if (value != null) {
+
+                    Class<?> fieldType = field.getType();
+
+                    // =============================
+                    // 🔥 ENUM FIELD SUPPORT
+                    // =============================
+                    if (fieldType.isEnum() && value instanceof String) {
+                        @SuppressWarnings({"unchecked", "rawtypes"})
+                        Enum<?> enumValue = Enum.valueOf(
+                                (Class<? extends Enum>) fieldType,
+                                (String) value
+                        );
+                        field.set(instance, enumValue);
+                        continue;
+                    }
+
+                    // =============================
+                    // 🔥 MAP SUPPORT (including enum keys)
+                    // =============================
+                    if (Map.class.isAssignableFrom(fieldType) && value instanceof Map<?, ?> rawMap) {
+
+                        Map<Object, Object> convertedMap = new LinkedHashMap<>();
+
+                        // Try to detect key enum type via generics
+                        Type genericType = field.getGenericType();
+                        Class<?> keyType = null;
+
+                        if (genericType instanceof ParameterizedType parameterizedType) {
+                            Type[] args = parameterizedType.getActualTypeArguments();
+                            if (args.length > 0 && args[0] instanceof Class<?>) {
+                                keyType = (Class<?>) args[0];
+                            }
+                        }
+
+                        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
+
+                            Object mapKey = entry.getKey();
+                            Object mapValue = entry.getValue();
+
+                            // Convert String keys back to Enum if needed
+                            if (keyType != null && keyType.isEnum() && mapKey instanceof String) {
+                                @SuppressWarnings({"unchecked", "rawtypes"})
+                                Enum<?> enumKey = Enum.valueOf(
+                                        (Class<? extends Enum>) keyType,
+                                        (String) mapKey
+                                );
+                                mapKey = enumKey;
+                            }
+
+                            convertedMap.put(mapKey, mapValue);
+                        }
+
+                        field.set(instance, convertedMap);
+                        continue;
+                    }
+
+                    // =============================
+                    // 🔥 NORMAL TYPE CHECK
+                    // =============================
+                    if (!fieldType.isAssignableFrom(value.getClass())) {
+                        throw new IllegalArgumentException(
+                                "Type mismatch for field '" + field.getName() +
+                                        "'. Expected: " + fieldType.getSimpleName() +
+                                        ", got: " + value.getClass().getSimpleName()
+                        );
+                    }
+
+                    field.set(instance, value);
+                }
+
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException("Failed to set field: " + field.getName(), e);
+            }
+        }
+    }
+
+    /* ------------------------------------------------ */
+    /*                    UTILITIES                     */
+    /* ------------------------------------------------ */
+
+    private static List<Field> getAllFields(Class<?> type) {
+        List<Field> fields = new ArrayList<>();
+
+        while (type != null && type != Object.class) {
+            fields.addAll(Arrays.asList(type.getDeclaredFields()));
+            type = type.getSuperclass();
+        }
+
+        return fields;
+    }
+}

+ 3 - 0
src/main/java/me/lethunderhawk/fluxapi/profile/ProfileAPIModule.java

@@ -2,8 +2,10 @@ package me.lethunderhawk.fluxapi.profile;
 
 import me.lethunderhawk.fluxapi.FluxService;
 import me.lethunderhawk.fluxapi.profile.listener.JoinListener;
+import me.lethunderhawk.fluxapi.profile.representation.FluxProfile;
 import me.lethunderhawk.fluxapi.util.config.ConfigLoader;
 import me.lethunderhawk.fluxapi.util.interfaces.FluxAPIModule;
+import org.bukkit.configuration.serialization.ConfigurationSerialization;
 import org.bukkit.event.HandlerList;
 import org.bukkit.plugin.java.JavaPlugin;
 
@@ -22,6 +24,7 @@ public class ProfileAPIModule extends FluxAPIModule {
 
     @Override
     public void onEnable() {
+        ConfigurationSerialization.registerClass(FluxProfile.class, "FluxProfile");
         profileManager = new ProfileManager(FluxService.get(ConfigLoader.class));
         FluxService.register(ProfileManager.class, profileManager);
 

+ 1 - 2
src/main/java/me/lethunderhawk/fluxapi/profile/ProfileManager.java

@@ -2,7 +2,6 @@ package me.lethunderhawk.fluxapi.profile;
 
 import me.lethunderhawk.fluxapi.profile.representation.FluxProfile;
 import me.lethunderhawk.fluxapi.util.config.ConfigLoader;
-import org.bukkit.configuration.serialization.ConfigurationSerializable;
 
 import java.util.HashSet;
 import java.util.Map;
@@ -110,7 +109,7 @@ public final class ProfileManager {
         }
     }
 
-    public <T extends ConfigurationSerializable> T getSubProfile(
+    public <T extends AnnotatedProfileCategory> T getSubProfile(
             UUID uuid,
             String id,
             Class<T> type

+ 39 - 42
src/main/java/me/lethunderhawk/fluxapi/profile/representation/FluxProfile.java

@@ -1,13 +1,16 @@
 package me.lethunderhawk.fluxapi.profile.representation;
 
+import me.lethunderhawk.fluxapi.profile.AnnotatedProfileCategory;
 import me.lethunderhawk.fluxapi.profile.event.FluxProfileLoadEvent;
 import me.lethunderhawk.fluxapi.profile.event.FluxProfileUpdateEvent;
 import org.bukkit.Bukkit;
 import org.bukkit.configuration.serialization.ConfigurationSerializable;
 import org.bukkit.configuration.serialization.ConfigurationSerialization;
 import org.jetbrains.annotations.NotNull;
+import org.jspecify.annotations.NonNull;
 
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -17,7 +20,7 @@ public class FluxProfile implements ConfigurationSerializable {
     /* Static Registry                                  */
     /* ------------------------------------------------ */
 
-    private static final Map<String, Class<? extends ConfigurationSerializable>> REGISTERED_TYPES = new ConcurrentHashMap<>();
+    private static final Map<String, Class<? extends AnnotatedProfileCategory>> REGISTERED_TYPES = new ConcurrentHashMap<>();
     private static final Map<String, SubProfileFactory<?>> DEFAULT_FACTORIES = new HashMap<>();
     /**
      * Register a sub profile type.
@@ -27,11 +30,10 @@ public class FluxProfile implements ConfigurationSerializable {
      */
     public static void registerSubProfile(
             @NotNull String id,
-            @NotNull Class<? extends ConfigurationSerializable> clazz
+            @NotNull Class<? extends AnnotatedProfileCategory> clazz
     ) {
         REGISTERED_TYPES.put(id, clazz);
         ConfigurationSerialization.registerClass(clazz);
-
     }
     /**
      * Register a sub profile type with a default Object.
@@ -40,11 +42,22 @@ public class FluxProfile implements ConfigurationSerializable {
      * @param clazz The serializable class
      * @param factory The default factory used to create a subConfig if there is nothing saved yet
      */
-    public static <T extends ConfigurationSerializable> void registerSubProfile(
+    public static <T extends AnnotatedProfileCategory> void registerSubProfile(
             @NotNull String id,
             @NotNull Class<T> clazz,
             @NotNull SubProfileFactory<T> factory
     ) {
+        try {
+            clazz.getDeclaredMethod("deserialize", Map.class);
+        } catch (NoSuchMethodException e) {
+            throw new IllegalArgumentException(
+                    clazz.getSimpleName() + " must declare a static deserialize(Map) method. " +
+                            "Add: public static " + clazz.getSimpleName() +
+                            " deserialize(Map<String, Object> map) { " +
+                            "return AnnotatedProfileCategory.deserialize(" + clazz.getSimpleName() +
+                            ".class, map); }"
+            );
+        }
         REGISTERED_TYPES.put(id, clazz);
         DEFAULT_FACTORIES.put(id, factory);
         ConfigurationSerialization.registerClass(clazz);
@@ -53,12 +66,10 @@ public class FluxProfile implements ConfigurationSerializable {
     /* ------------------------------------------------ */
     /* Instance Data                                    */
     /* ------------------------------------------------ */
-
     private final String playerUUID;
-    private boolean dirty = false;
 
     // Fully deserialized objects
-    private final Map<String, ConfigurationSerializable> subProfiles = new HashMap<>();
+    private final Map<String, AnnotatedProfileCategory> subProfiles = new HashMap<>();
 
     // Raw maps (used if plugin not yet registered)
     private final Map<String, Map<String, Object>> unresolvedData = new HashMap<>();
@@ -85,19 +96,15 @@ public class FluxProfile implements ConfigurationSerializable {
         for (Map.Entry<String, Object> entry : map.entrySet()) {
 
             String key = entry.getKey();
-
             if (key.equals("uuid")) continue;
 
             Object value = entry.getValue();
 
-            // CASE 1: Already deserialized by Bukkit
-            if (value instanceof ConfigurationSerializable serializable) {
-
+            if (value instanceof AnnotatedProfileCategory serializable) {
                 subProfiles.put(key, serializable);
                 continue;
             }
 
-            // CASE 2: Raw map (class not registered yet)
             if (value instanceof Map<?, ?> rawMap) {
 
                 @SuppressWarnings("unchecked")
@@ -111,13 +118,14 @@ public class FluxProfile implements ConfigurationSerializable {
                     try {
                         Class<?> clazz = Class.forName(className);
 
-                        if (ConfigurationSerializable.class.isAssignableFrom(clazz)) {
+                        if (AnnotatedProfileCategory.class.isAssignableFrom(clazz)) {
 
-                            ConfigurationSerializable obj =
-                                    ConfigurationSerialization.deserializeObject(
-                                            section,
-                                            clazz.asSubclass(ConfigurationSerializable.class)
-                                    );
+                            AnnotatedProfileCategory obj =
+                                    (AnnotatedProfileCategory)
+                                            ConfigurationSerialization.deserializeObject(
+                                                    section,
+                                                    clazz.asSubclass(AnnotatedProfileCategory.class)
+                                            );
 
                             subProfiles.put(key, obj);
                             continue;
@@ -128,8 +136,6 @@ public class FluxProfile implements ConfigurationSerializable {
 
                 unresolvedData.put(key, section);
             }
-
-            System.out.println("Loaded key: " + key + " type: " + value.getClass().getName());
         }
 
         Bukkit.getPluginManager().callEvent(new FluxProfileLoadEvent(this));
@@ -150,7 +156,7 @@ public class FluxProfile implements ConfigurationSerializable {
      */
     public void addSubProfile(
             @NotNull String id,
-            @NotNull ConfigurationSerializable profile
+            @NotNull AnnotatedProfileCategory profile
     ) {
         subProfiles.put(id, profile);
 
@@ -164,12 +170,12 @@ public class FluxProfile implements ConfigurationSerializable {
      * Automatically resolves unresolved raw data if class gets registered later.
      */
     @SuppressWarnings("unchecked")
-    public synchronized <T extends ConfigurationSerializable> T getSubProfile(
+    public synchronized <T extends AnnotatedProfileCategory> T getSubProfile(
             @NotNull String id,
             @NotNull Class<T> expectedType
     ) {
         // Already deserialized
-        ConfigurationSerializable existing = subProfiles.get(id);
+        AnnotatedProfileCategory existing = subProfiles.get(id);
         if (existing != null) {
             return expectedType.cast(existing);
         }
@@ -178,11 +184,11 @@ public class FluxProfile implements ConfigurationSerializable {
         Map<String, Object> raw = unresolvedData.remove(id);
         if (raw != null) {
 
-            Class<? extends ConfigurationSerializable> clazz = REGISTERED_TYPES.get(id);
+            Class<? extends AnnotatedProfileCategory> clazz = REGISTERED_TYPES.get(id);
 
             if (clazz != null) {
-                ConfigurationSerializable obj =
-                        ConfigurationSerialization.deserializeObject(raw, clazz);
+                AnnotatedProfileCategory obj =
+                        (AnnotatedProfileCategory) ConfigurationSerialization.deserializeObject(raw, clazz);
 
                 subProfiles.put(id, obj);
                 return expectedType.cast(obj);
@@ -204,27 +210,18 @@ public class FluxProfile implements ConfigurationSerializable {
 
         return null;
     }
-
-    /* ------------------------------------------------ */
-    /* Serialization                                     */
-    /* ------------------------------------------------ */
-
     @Override
-    public @NotNull Map<String, Object> serialize() {
+    public @NonNull Map<String, Object> serialize() {
 
-        Map<String, Object> data = new HashMap<>();
-        data.put("uuid", playerUUID);
+        Map<String, Object> result = new LinkedHashMap<>();
 
-        // Store resolved profiles
-        data.putAll(subProfiles);
+        result.put("uuid", playerUUID);
 
-        // Preserve unresolved raw data
-        data.putAll(unresolvedData);
+        result.putAll(subProfiles);
 
-        return data;
-    }
+        // Preserve unresolved data
+        result.putAll(unresolvedData);
 
-    public static FluxProfile deserialize(Map<String, Object> map) {
-        return new FluxProfile(map);
+        return result;
     }
 }

+ 2 - 2
src/main/java/me/lethunderhawk/fluxapi/profile/representation/SubProfileFactory.java

@@ -1,8 +1,8 @@
 package me.lethunderhawk.fluxapi.profile.representation;
 
-import org.bukkit.configuration.serialization.ConfigurationSerializable;
+import me.lethunderhawk.fluxapi.profile.AnnotatedProfileCategory;
 
 @FunctionalInterface
-public interface SubProfileFactory<T extends ConfigurationSerializable> {
+public interface SubProfileFactory<T extends AnnotatedProfileCategory> {
     T create(String playerUUID);
 }

+ 7 - 4
src/main/java/me/lethunderhawk/fluxapi/util/command/CustomCommand.java

@@ -20,13 +20,12 @@ public abstract class CustomCommand implements CommandExecutor, TabCompleter {
     protected final FluxAPIModule module;
     public CustomCommand(FluxAPIModule module) {
         this.module = module;
+        setRootCommand();
         createHelpCommand();
         createCommands();
     }
 
-    public void setRootCommand(CommandNode rootCommand) {
-        this.rootCommand = rootCommand;
-    }
+    public abstract void setRootCommand();
 
     private void createHelpCommand() {
         rootCommand.addSubCommand(new CommandNode("help", "Displays this help menu", this::sendHelp));
@@ -37,7 +36,11 @@ public abstract class CustomCommand implements CommandExecutor, TabCompleter {
     @Override
     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
         if (args.length == 0) {
-            sendHelp(sender);
+            if(rootCommand.getExecutor() != null) {
+                rootCommand.getExecutor().accept(sender, args);
+            }else{
+                sendHelp(sender);
+            }
             return true;
         }
 

+ 31 - 10
src/main/java/me/lethunderhawk/fluxapi/util/config/ConfigLoader.java

@@ -38,10 +38,28 @@ public final class ConfigLoader {
         return file;
     }
 
+    /**
+     * @Deprecated since 1.2.5
+     * @param path
+     * @return
+     */
     public YamlConfiguration loadConfig(String path) {
         File file = getFile(path);
         return YamlConfiguration.loadConfiguration(file);
     }
+    public YamlConfiguration safeLoadConfig(String path) {
+        File file = getFile(path);
+
+        if(!file.exists())
+            return new YamlConfiguration();
+
+        try {
+            return YamlConfiguration.loadConfiguration(file);
+        } catch(Exception e) {
+            plugin.getLogger().warning("Failed to load config " + file.getName() + ", creating new one.");
+            return new YamlConfiguration();
+        }
+    }
 
     public void saveConfig(YamlConfiguration cfg, String path) {
         File file = getFile(path);
@@ -61,7 +79,7 @@ public final class ConfigLoader {
             String node,
             T object
     ) {
-        YamlConfiguration cfg = loadConfig(path);
+        YamlConfiguration cfg = safeLoadConfig(path);
         cfg.set(node, object);
         saveConfig(cfg, path);
     }
@@ -71,10 +89,14 @@ public final class ConfigLoader {
             String node,
             Class<T> type
     ) {
-        YamlConfiguration cfg = loadConfig(path);
+        YamlConfiguration cfg = safeLoadConfig(path);
+
         Object obj = cfg.get(node);
 
-        if (obj == null) return null;
+        if(obj == null){
+            System.out.println("Node '" + node + "' not found.");
+            return null;
+        }
 
         if (!type.isInstance(obj)) {
             throw new IllegalStateException(
@@ -95,10 +117,9 @@ public final class ConfigLoader {
             String node,
             Collection<T> collection
     ) {
-        YamlConfiguration cfg = loadConfig(path);
-        for (T object : collection) {
-            cfg.set(node, object);
-        }
+        YamlConfiguration cfg = safeLoadConfig(path);
+        List<T> list = new ArrayList<>(collection);
+        cfg.set(node, list);
         saveConfig(cfg, path);
     }
 
@@ -107,7 +128,7 @@ public final class ConfigLoader {
             String node,
             Class<T> type
     ) {
-        YamlConfiguration cfg = loadConfig(path);
+        YamlConfiguration cfg = safeLoadConfig(path);
 
         List<?> rawList = cfg.getList(node);
         if (rawList == null) return new ArrayList<>();
@@ -133,7 +154,7 @@ public final class ConfigLoader {
             Map<K, V> map,
             Function<K, String> keySerializer
     ) {
-        YamlConfiguration cfg = loadConfig(path);
+        YamlConfiguration cfg = safeLoadConfig(path);
 
         ConfigurationSection section = cfg.createSection(node);
 
@@ -151,7 +172,7 @@ public final class ConfigLoader {
             Function<String, K> keyDeserializer,
             Class<V> type
     ) {
-        YamlConfiguration cfg = loadConfig(path);
+        YamlConfiguration cfg = safeLoadConfig(path);
 
         ConfigurationSection section = cfg.getConfigurationSection(node);
         if (section == null) return new HashMap<>();

+ 134 - 7
src/main/java/me/lethunderhawk/fluxapi/util/gui/InventoryGUI.java

@@ -7,15 +7,14 @@ import org.bukkit.Bukkit;
 import org.bukkit.Material;
 import org.bukkit.entity.Player;
 import org.bukkit.event.inventory.ClickType;
+import org.bukkit.event.inventory.InventoryClickEvent;
 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.*;
 import java.util.function.BiConsumer;
 
 /**
@@ -29,18 +28,68 @@ public abstract class InventoryGUI implements InventoryHolder {
 
     private final Map<Integer, BiConsumer<Player, ClickType>> slotActions = new HashMap<>();
     private final Stack<InventoryGUI> previousGuis = new Stack<>();
+    /** Slots that the player may edit (drag‑and‑drop, shift‑click, etc.) */
+    private final Set<Integer> editableSlots = new HashSet<>();
+
+    /** Register a slot as editable – call this from the subclass before the GUI opens */
+    protected final void setEditable(int... slots) {
+        for (int s : slots) editableSlots.add(s);
+    }
+
+    /** Helper to check whether a slot is editable */
+    protected final boolean isEditable(int slot) {
+        return editableSlots.contains(slot);
+    }
+
+    public InventoryGUI(String title, int size, Player p) {
+        this.title = title;
+        this.size = size;
+        this.inventory = Bukkit.createInventory(this, size, title);
+        performAdditionalComputationOnPlayer(p);
+    }
 
     public InventoryGUI(String title, int size) {
         this.title = title;
         this.size = size;
         this.inventory = Bukkit.createInventory(this, size, title);
-        buildContent();
     }
 
-    public void handleClick(Player player, int slot, ClickType type) {
-        BiConsumer<Player, ClickType> action = slotActions.get(slot);
+    /**
+     * Can be overwritten by subclasses
+     * @param p The Player for whom the GUI is for. May not exist if the super didn't specify a player
+     */
+    public void performAdditionalComputationOnPlayer(Player p) {
+    }
+
+    public void handleClick(Player player, InventoryClickEvent event) {
+        int rawSlot = event.getRawSlot();
+            // If the click is inside the top inventory (our GUI)
+            if (rawSlot < inventory.getSize()) {
+                // Editable slots: let Minecraft handle the action (no cancel)
+                if (isEditable(rawSlot)) {
+                    // Do NOT cancel – the player can move items freely.
+                    // However we still want to run any custom click‑action that
+                    // might be attached (e.g. a “clear slot” button).  The
+                    // default implementation in your base class usually stores
+                    // a map of slot → Consumer<Player,ClickType>.  If you have
+                    // that, call it here *after* the normal behaviour.
+                    runClickActionIfPresent(player, rawSlot, event.getClick());
+                    return;
+                }
+
+                // Non‑editable slots – keep the GUI read‑only
+                event.setCancelled(true);
+                runClickActionIfPresent(player, rawSlot, event.getClick());
+            } else {
+                onPlayerInventoryClick(player, event);
+            }
+    }
+    public void onPlayerInventoryClick(Player player, InventoryClickEvent event){}
+
+    private void runClickActionIfPresent(Player player, int rawSlot, ClickType click) {
+        BiConsumer<Player, ClickType> action = slotActions.get(rawSlot);
         if (action != null) {
-            action.accept(player, type);
+            action.accept(player, click);
         }
     }
     /**
@@ -50,6 +99,83 @@ public abstract class InventoryGUI implements InventoryHolder {
         inventory.setItem(slot, item);
     }
 
+    protected int getRow(int slot) {
+        return slot / 9;
+    }
+
+    protected int getColumn(int slot) {
+        return slot % 9;
+    }
+
+    protected int getSlot(int row, int column) {
+        return row * 9 + column;
+    }
+
+    protected List<Integer> computeRectangleSlots(int startSlot, int endSlot) {
+
+        List<Integer> slots = new ArrayList<>();
+
+        int startRow = startSlot / 9;
+        int startCol = startSlot % 9;
+
+        int endRow = endSlot / 9;
+        int endCol = endSlot % 9;
+
+        for (int row = startRow; row <= endRow; row++) {
+            for (int col = startCol; col <= endCol; col++) {
+
+                slots.add(row * 9 + col);
+
+            }
+        }
+
+        return slots;
+    }
+
+    public void fillRectangleWithClickAction(int startSlot, int endSlot, ItemStack item, BiConsumer<Player, ClickType> action) {
+        int startRow = getRow(startSlot);
+        int startCol = getColumn(startSlot);
+
+        int endRow = getRow(endSlot);
+        int endCol = getColumn(endSlot);
+
+        for (int row = startRow; row <= endRow; row++) {
+            for (int col = startCol; col <= endCol; col++) {
+                int slot = getSlot(row, col);
+                setItemWithClickAction(slot, item, action);
+            }
+        }
+    }
+
+    public void fillRectangle(int startSlot, int endSlot, ItemStack item) {
+        int startRow = getRow(startSlot);
+        int startCol = getColumn(startSlot);
+
+        int endRow = getRow(endSlot);
+        int endCol = getColumn(endSlot);
+
+        for (int row = startRow; row <= endRow; row++) {
+            for (int col = startCol; col <= endCol; col++) {
+                int slot = getSlot(row, col);
+                setItem(slot, item);
+            }
+        }
+    }
+    public void setRectangleEditable(int startSlot, int endSlot) {
+        int startRow = getRow(startSlot);
+        int startCol = getColumn(startSlot);
+
+        int endRow = getRow(endSlot);
+        int endCol = getColumn(endSlot);
+
+        for (int row = startRow; row <= endRow; row++) {
+            for (int col = startCol; col <= endCol; col++) {
+                int slot = getSlot(row, col);
+                setEditable(slot);
+            }
+        }
+    }
+
     /**
      * Sets an item and assigns a click action for that slot.
      */
@@ -92,6 +218,7 @@ public abstract class InventoryGUI implements InventoryHolder {
      * Opens this GUI for a player.
      */
     public void open(Player player) {
+        buildContent();
         player.openInventory(inventory);
     }
 

+ 2 - 2
src/main/java/me/lethunderhawk/fluxapi/util/gui/InventoryManager.java

@@ -30,6 +30,7 @@ public final class InventoryManager implements Listener {
     }
 
     public static void openFor(Player player, InventoryGUI gui) {
+
         // ensure main thread
         if (!Bukkit.isPrimaryThread()) {
             Bukkit.getScheduler().runTask(providerPlugin, () -> openFor(player, gui));
@@ -70,8 +71,7 @@ public final class InventoryManager implements Listener {
         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());
+        gui.handleClick(player, event);
     }
 
     @EventHandler

+ 100 - 0
src/main/java/me/lethunderhawk/fluxapi/util/gui/PaginatedInventoryGUI.java

@@ -0,0 +1,100 @@
+package me.lethunderhawk.fluxapi.util.gui;
+
+import me.lethunderhawk.fluxapi.util.itemdesign.ItemOptions;
+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.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+public abstract class PaginatedInventoryGUI<T> extends InventoryGUI {
+
+    protected int currentPage = 0;
+    protected int maxPage;
+
+    protected List<T> elements = new ArrayList<>();
+    protected List<Integer> contentSlots = new ArrayList<>();
+
+    public PaginatedInventoryGUI(String title, int size, Player player) {
+        super(title, size, player);
+    }
+
+    protected void setupPagination(Collection<T> elements, List<Integer> slots) {
+
+        this.elements = new ArrayList<>(elements);
+        this.contentSlots = slots;
+
+        int itemsPerPage = slots.size();
+        this.maxPage = (this.elements.size() - 1) / itemsPerPage;
+    }
+
+    protected void sortElements(Comparator<T> comparator) {
+        elements.sort(comparator);
+    }
+
+    protected void renderPage() {
+
+        int itemsPerPage = contentSlots.size();
+        int startIndex = currentPage * itemsPerPage;
+
+        for (int i = 0; i < itemsPerPage; i++) {
+
+            int elementIndex = startIndex + i;
+
+            if (elementIndex >= elements.size()) return;
+
+            T element = elements.get(elementIndex);
+
+            int slot = contentSlots.get(i);
+
+            setItemWithClickAction(slot, createItem(element), (p, click) -> onClick(element, p, click));
+        }
+    }
+
+    protected void nextPage() {
+        if (currentPage < maxPage) {
+            currentPage++;
+            update();
+        }
+    }
+
+    protected void previousPage() {
+        if (currentPage > 0) {
+            currentPage--;
+            update();
+        }
+    }
+
+    protected void addNavigationButtons() {
+
+        int size = getInventory().getSize();
+
+        if (currentPage > 0) {
+            setItemWithClickAction(size - 9,
+                    new ItemOptions(Material.ARROW)
+                            .setName(Component.text("Previous Page", NamedTextColor.YELLOW))
+                            .buildItemStack(),
+                    (p, t) -> previousPage());
+        }
+
+        if (currentPage < maxPage) {
+            setItemWithClickAction(size - 1,
+                    new ItemOptions(Material.ARROW)
+                            .setName(Component.text("Next Page", NamedTextColor.YELLOW))
+                            .buildItemStack(),
+                    (p, t) -> nextPage());
+        }
+    }
+
+    protected abstract ItemStack createItem(T element);
+
+    protected void onClick(T element, Player player, ClickType type) {
+    }
+
+}

+ 4 - 0
src/main/java/me/lethunderhawk/fluxapi/util/inventoryoverlay/LoreOverlayManager.java

@@ -0,0 +1,4 @@
+package me.lethunderhawk.fluxapi.util.inventoryoverlay;
+
+public class LoreOverlayManager {
+}

+ 4 - 0
src/main/java/me/lethunderhawk/fluxapi/util/inventoryoverlay/OverlaySession.java

@@ -0,0 +1,4 @@
+package me.lethunderhawk.fluxapi.util.inventoryoverlay;
+
+public class OverlaySession {
+}

+ 50 - 0
src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/ItemData.java

@@ -0,0 +1,50 @@
+package me.lethunderhawk.fluxapi.util.itemdesign;
+
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataType;
+import org.bukkit.plugin.Plugin;
+
+public class ItemData {
+
+    private static Plugin plugin;
+
+    public static void init(Plugin pl) {
+        plugin = pl;
+    }
+
+    private static NamespacedKey key(String name) {
+        return new NamespacedKey(plugin, name);
+    }
+
+    public static void setInt(ItemStack item, String id, int value) {
+        ItemMeta meta = item.getItemMeta();
+        if (meta == null) return;
+
+        meta.getPersistentDataContainer()
+                .set(key(id), PersistentDataType.INTEGER, value);
+
+        item.setItemMeta(meta);
+
+        ItemLoreRenderer.render(item); // auto refresh
+    }
+
+    public static int getInt(ItemStack item, String id, int def) {
+        ItemMeta meta = item.getItemMeta();
+        if (meta == null) return def;
+
+        Integer value = meta.getPersistentDataContainer()
+                .get(key(id), PersistentDataType.INTEGER);
+
+        return value == null ? def : value;
+    }
+
+    public static boolean has(ItemStack item, String id) {
+        ItemMeta meta = item.getItemMeta();
+        if (meta == null) return false;
+
+        return meta.getPersistentDataContainer()
+                .has(key(id), PersistentDataType.INTEGER);
+    }
+}

+ 49 - 0
src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/ItemLoreRenderer.java

@@ -0,0 +1,49 @@
+package me.lethunderhawk.fluxapi.util.itemdesign;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ItemLoreRenderer {
+
+    private static final Pattern PLACEHOLDER =
+            Pattern.compile("\\{([a-zA-Z0-9_]+)}");
+
+    public static void render(ItemStack item) {
+
+        List<String> template = ItemLoreTemplate.getTemplate(item);
+        if (template == null) return;
+
+        ItemMeta meta = item.getItemMeta();
+        if (meta == null) return;
+
+        List<Component> newLore = new ArrayList<>();
+
+        for (String line : template) {
+
+            Matcher matcher = PLACEHOLDER.matcher(line);
+            StringBuffer buffer = new StringBuffer();
+
+            while (matcher.find()) {
+
+                String key = matcher.group(1);
+
+                int value = ItemData.getInt(item, key, 0);
+
+                matcher.appendReplacement(buffer, String.valueOf(value));
+            }
+
+            matcher.appendTail(buffer);
+
+            newLore.add(Component.text(buffer.toString()));
+        }
+
+        meta.lore(newLore);
+        item.setItemMeta(meta);
+    }
+}

+ 55 - 0
src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/ItemLoreTemplate.java

@@ -0,0 +1,55 @@
+package me.lethunderhawk.fluxapi.util.itemdesign;
+
+import me.lethunderhawk.fluxapi.FluxService;
+import me.lethunderhawk.fluxapi.main.FluxAPI;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataType;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class ItemLoreTemplate {
+
+    private static final String KEY = "lore_template";
+
+    public static void storeTemplate(ItemStack item) {
+
+        ItemMeta meta = item.getItemMeta();
+        if (meta == null || !meta.hasLore()) return;
+
+        List<String> template = new ArrayList<>();
+
+        PlainTextComponentSerializer plain = PlainTextComponentSerializer.plainText();
+        for (Component c : meta.lore()) {
+            template.add(plain.serialize(c));
+        }
+
+        meta.getPersistentDataContainer().set(
+                new NamespacedKey(FluxService.get(FluxAPI.class), KEY),
+                PersistentDataType.STRING,
+                String.join("\n", template)
+        );
+
+        item.setItemMeta(meta);
+    }
+
+    public static List<String> getTemplate(ItemStack item) {
+
+        ItemMeta meta = item.getItemMeta();
+        if (meta == null) return null;
+
+        String raw = meta.getPersistentDataContainer().get(
+                new NamespacedKey(FluxService.get(FluxAPI.class), KEY),
+                PersistentDataType.STRING
+        );
+
+        if (raw == null) return null;
+
+        return Arrays.asList(raw.split("\n"));
+    }
+}

+ 17 - 0
src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/ItemLoreUpdater.java

@@ -0,0 +1,17 @@
+package me.lethunderhawk.fluxapi.util.itemdesign;
+
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+public class ItemLoreUpdater {
+    private ItemLoreUpdater() {}
+    public static void updateIntegersInLore(ItemStack item, String unformatted, String... fields) {
+        for(String field : fields) {
+            unformatted = unformatted.replace("{" + field + "}", String.valueOf(ItemData.getInt(item, field, 0)));
+        }
+
+        ItemMeta meta = item.getItemMeta();
+        meta.lore(LoreDesigner.createLore(unformatted));
+        item.setItemMeta(meta);
+    }
+}

+ 9 - 1
src/main/java/me/lethunderhawk/fluxapi/util/itemdesign/LoreDesigner.java

@@ -12,8 +12,9 @@ import java.util.List;
 import java.util.Map;
 
 public final class LoreDesigner {
-
+    public static String defaultWidthReference = "The default width reference";
     private LoreDesigner() {}
+
     public static Component createSingle(String text) {
         if (text == null || text.isBlank()) {
             return Component.empty();
@@ -48,6 +49,13 @@ public final class LoreDesigner {
         }
         return lore;
     }
+    public static List<Component> createLore(String text, NamedTextColor color) {
+        return createLore(text, defaultWidthReference, color);
+    }
+
+    public static List<Component> createLore(String text){
+        return createLore(text, defaultWidthReference);
+    }
 
     public static List<Component> createLore(String text, String widthReference) {
         int maxLength = widthReference.length();

+ 3 - 5
src/main/resources/plugin.yml

@@ -9,11 +9,9 @@ commands:
   npc:
     description: Create a NPC
     usage: /npc <next>
+    permission: fluxapi.npc
 
 permissions:
-  fluxapi.commands:
-    description: Allows usage of the base NPC command
-    default: op
-  fluxapi.delete:
-    description: Allows usage of deleting NPC command
+  fluxapi.npc:
+    description: Allows usage of the base NPC commands
     default: op