list = new ArrayList<>();
+
+ for (String string : strings) {
+ if (string.contains("-")) {
+ String[] range = string.split("-", 2);
+ int start = Integer.parseInt(range[0].trim());
+ int end = Integer.parseInt(range[1].trim());
+
+ for (int i = start; i <= end; i++) {
+ list.add(i);
+ }
+ } else {
+ list.add(Integer.parseInt(string.trim()));
+ }
+ }
+
+ return list;
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/games/negative/engine/util/OptionalBool.java b/common/src/main/java/games/negative/engine/util/OptionalBool.java
new file mode 100644
index 0000000..232c2fc
--- /dev/null
+++ b/common/src/main/java/games/negative/engine/util/OptionalBool.java
@@ -0,0 +1,221 @@
+package games.negative.engine.util;
+
+import java.util.Optional;
+
+/**
+ * A utility class that wraps a boolean value and provides functional-style operations
+ * for conditional execution and value mapping based on the boolean state.
+ *
+ * This class is similar to {@link Optional} but specifically designed for boolean values,
+ * offering convenient methods for executing actions conditionally and mapping values
+ * based on the true/false state.
+ *
+ * The class uses a singleton pattern for the two possible instances ({@code true} and {@code false})
+ * to ensure memory efficiency and reference equality for instances with the same boolean value.
+ *
+ * Usage Examples:
+ * {@code
+ * OptionalBool condition = OptionalBool.of(someCondition);
+ *
+ * // Conditional execution
+ * condition.ifTrue(() -> System.out.println("Condition is true"));
+ * condition.ifTrueOrElse(
+ * () -> performTrueAction(),
+ * () -> performFalseAction()
+ * );
+ *
+ * // Value mapping
+ * Optional result = condition.mapIfTrue(() -> "Success");
+ * String value = condition.mapIfTrueOrElse(
+ * () -> "True value",
+ * () -> "False value"
+ * );
+ * }
+ */
+public class OptionalBool {
+
+ private static final OptionalBool TRUE = new OptionalBool(true);
+ private static final OptionalBool FALSE = new OptionalBool(false);
+
+ private final boolean value;
+
+ /**
+ * Private constructor to enforce singleton pattern.
+ *
+ * @param value the boolean value to wrap
+ */
+ private OptionalBool(boolean value) {
+ this.value = value;
+ }
+
+ /**
+ * Creates an {@code OptionalBool} instance for the given boolean value.
+ *
+ * This method returns one of two singleton instances to ensure memory efficiency
+ * and allow reference equality comparisons.
+ *
+ * @param value the boolean value to wrap
+ * @return {@code OptionalBool.TRUE} if value is {@code true},
+ * {@code OptionalBool.FALSE} if value is {@code false}
+ */
+ public static OptionalBool of(boolean value) {
+ return value ? TRUE : FALSE;
+ }
+
+ /**
+ * Checks if the wrapped boolean value is {@code true}.
+ *
+ * @return {@code true} if the wrapped value is {@code true}, {@code false} otherwise
+ */
+ public boolean isTrue() {
+ return value;
+ }
+
+ /**
+ * Checks if the wrapped boolean value is {@code false}.
+ *
+ * @return {@code true} if the wrapped value is {@code false}, {@code false} otherwise
+ */
+ public boolean isFalse() {
+ return !value;
+ }
+
+ /**
+ * Executes the given action if the wrapped boolean value is {@code true}.
+ *
+ * If the value is {@code false}, no action is performed.
+ *
+ * @param action the action to execute if the value is {@code true}
+ * @throws NullPointerException if action is null and the value is {@code true}
+ */
+ public void ifTrue(Runnable action) {
+ if (value) action.run();
+ }
+
+ /**
+ * Executes the given action if the wrapped boolean value is {@code false}.
+ *
+ * If the value is {@code true}, no action is performed.
+ *
+ * @param action the action to execute if the value is {@code false}
+ * @throws NullPointerException if action is null and the value is {@code false}
+ */
+ public void ifFalse(Runnable action) {
+ if (!value) action.run();
+ }
+
+ /**
+ * Executes one of two actions based on the wrapped boolean value.
+ *
+ * If the value is {@code true}, the {@code trueAction} is executed.
+ * If the value is {@code false}, the {@code falseAction} is executed.
+ *
+ * @param trueAction the action to execute if the value is {@code true}
+ * @param falseAction the action to execute if the value is {@code false}
+ * @throws NullPointerException if the corresponding action is null
+ */
+ public void ifTrueOrElse(Runnable trueAction, Runnable falseAction) {
+ if (value) {
+ trueAction.run();
+ } else {
+ falseAction.run();
+ }
+ }
+
+ /**
+ * Maps the wrapped boolean to a value using the provided callback if the value is {@code true}.
+ *
+ * If the wrapped value is {@code true}, the callback is executed and its result
+ * is wrapped in an {@link Optional}. If the wrapped value is {@code false},
+ * an empty {@code Optional} is returned.
+ *
+ * @param the type of the value to be returned
+ * @param callback the callback to execute if the value is {@code true}
+ * @return an {@code Optional} containing the callback result if value is {@code true},
+ * or an empty {@code Optional} if value is {@code false}
+ * @throws NullPointerException if callback is null and the value is {@code true}
+ */
+ public Optional mapIfTrue(Callback callback) {
+ if (isFalse()) return Optional.empty();
+
+ return Optional.of(callback.apply());
+ }
+
+ /**
+ * Maps the wrapped boolean to a value using the provided callback if the value is {@code false}.
+ *
+ * If the wrapped value is {@code false}, the callback is executed and its result
+ * is wrapped in an {@link Optional}. If the wrapped value is {@code true},
+ * an empty {@code Optional} is returned.
+ *
+ * @param the type of the value to be returned
+ * @param callback the callback to execute if the value is {@code false}
+ * @return an {@code Optional} containing the callback result if value is {@code false},
+ * or an empty {@code Optional} if value is {@code true}
+ * @throws NullPointerException if callback is null and the value is {@code false}
+ */
+ public Optional mapIfFalse(Callback callback) {
+ if (isTrue()) return Optional.empty();
+
+ return Optional.of(callback.apply());
+ }
+
+ /**
+ * Maps the wrapped boolean to a value using one of two callbacks based on the boolean state.
+ *
+ * If the wrapped value is {@code true}, the {@code trueCallback} is executed and its result returned.
+ * If the wrapped value is {@code false}, the {@code falseCallback} is executed and its result returned.
+ *
+ * @param the type of the value to be returned
+ * @param trueCallback the callback to execute if the value is {@code true}
+ * @param falseCallback the callback to execute if the value is {@code false}
+ * @return the result of the appropriate callback
+ * @throws NullPointerException if the corresponding callback is null
+ */
+ public T mapIfTrueOrElse(Callback trueCallback, Callback falseCallback) {
+ if (isTrue()) {
+ return trueCallback.apply();
+ } else {
+ return falseCallback.apply();
+ }
+ }
+
+ /**
+ * Indicates whether some other object is "equal to" this {@code OptionalBool}.
+ *
+ * Two {@code OptionalBool} instances are considered equal if they wrap the same boolean value.
+ *
+ * @param obj the reference object with which to compare
+ * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise
+ */
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof OptionalBool && ((OptionalBool) obj).value == value;
+ }
+
+ /**
+ * Returns a hash code value for this {@code OptionalBool}.
+ *
+ * The hash code is based on the wrapped boolean value.
+ *
+ * @return a hash code value for this object
+ */
+ @Override
+ public int hashCode() {
+ return Boolean.hashCode(value);
+ }
+
+ /**
+ * Returns a string representation of this {@code OptionalBool}.
+ *
+ * The string representation consists of the class name followed by
+ * the wrapped boolean value in square brackets.
+ *
+ * @return a string representation of this {@code OptionalBool}
+ */
+ @Override
+ public String toString() {
+ return "OptionalBool[" + value + "]";
+ }
+}
+
diff --git a/paper/build.gradle.kts b/paper/build.gradle.kts
index 49a3616..e29b0e9 100644
--- a/paper/build.gradle.kts
+++ b/paper/build.gradle.kts
@@ -6,7 +6,7 @@ plugins {
var id = "plugin-engine-paper"
var domain = "games.negative.engine"
-var apiVersion = "1.0.0"
+var apiVersion = "1.1.0"
repositories {
mavenCentral()
diff --git a/paper/src/main/java/games/negative/engine/paper/PaperPlugin.java b/paper/src/main/java/games/negative/engine/paper/PaperPlugin.java
index 2554abd..775ba17 100644
--- a/paper/src/main/java/games/negative/engine/paper/PaperPlugin.java
+++ b/paper/src/main/java/games/negative/engine/paper/PaperPlugin.java
@@ -3,7 +3,10 @@
import games.negative.engine.Plugin;
import games.negative.engine.message.util.MiniMessageUtil;
import games.negative.engine.paper.scheduler.Scheduler;
+import games.negative.engine.state.Reloadable;
import games.negative.moss.paper.MossPaper;
+import lombok.extern.slf4j.Slf4j;
+import org.bukkit.event.Listener;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.nio.file.Path;
@@ -14,6 +17,7 @@
* An abstract base class for Paper plugins that integrates with the Moss framework.
* This class provides common functionality for managing plugin data directories and fetching beans from the Moss context.
*/
+@Slf4j
public abstract class PaperPlugin extends MossPaper implements Plugin {
@Override
@@ -24,6 +28,17 @@ public void loadInitialComponents(AnnotationConfigApplicationContext context) {
MiniMessageUtil.init();
}
+ @Override
+ public void onEnable() {
+ super.onEnable();
+
+ invokeBeans(
+ Listener.class,
+ listener -> getServer().getPluginManager().registerEvents(listener, this),
+ (listener, e) -> log.error("Failed to register listener: {}", listener.getClass().getSimpleName(), e)
+ );
+ }
+
@Override
public Path directory() {
return getDataPath().toAbsolutePath();
@@ -39,4 +54,12 @@ public void fetchBeans(Class clazz, Consumer consumer) {
invokeBeans(clazz, consumer);
}
+ @Override
+ public void reload() {
+ invokeBeans(
+ Reloadable.class,
+ Reloadable::reload,
+ (reloadable, e) -> log.error("Failed to reload {}", reloadable.getClass().getSimpleName(), e)
+ );
+ }
}
diff --git a/paper/src/main/java/games/negative/engine/paper/command/PaperCommand.java b/paper/src/main/java/games/negative/engine/paper/command/PaperCommand.java
new file mode 100644
index 0000000..c592fd3
--- /dev/null
+++ b/paper/src/main/java/games/negative/engine/paper/command/PaperCommand.java
@@ -0,0 +1,8 @@
+package games.negative.engine.paper.command;
+
+import games.negative.engine.command.CloudCommand;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
+
+public interface PaperCommand extends CloudCommand {
+
+}
diff --git a/paper/src/main/java/games/negative/engine/paper/command/PaperCommandRegistry.java b/paper/src/main/java/games/negative/engine/paper/command/PaperCommandRegistry.java
index cb4b5e3..af8ec1d 100644
--- a/paper/src/main/java/games/negative/engine/paper/command/PaperCommandRegistry.java
+++ b/paper/src/main/java/games/negative/engine/paper/command/PaperCommandRegistry.java
@@ -1,7 +1,6 @@
package games.negative.engine.paper.command;
import games.negative.engine.command.CloudArgument;
-import games.negative.engine.command.CloudCommand;
import games.negative.engine.paper.PaperPlugin;
import games.negative.moss.spring.Enableable;
import games.negative.moss.spring.SpringComponent;
@@ -23,6 +22,7 @@ public class PaperCommandRegistry implements Enableable {
@Override
public void onEnable() {
+ log.info("Registering commands");
PaperCommandManager commands = PaperCommandManager.builder()
.executionCoordinator(ExecutionCoordinator.asyncCoordinator())
.buildOnEnable(plugin);
@@ -40,7 +40,7 @@ public void onEnable() {
AnnotationParser annotationParser = new AnnotationParser<>(commands, CommandSourceStack.class);
- plugin.invokeBeans(CloudCommand.class, command -> {
+ plugin.invokeBeans(PaperCommand.class, command -> {
command.onRegister(commands);
annotationParser.parse(command);
log.info("Registered command: {}", command.getClass().getSimpleName());
diff --git a/paper/src/main/java/games/negative/engine/paper/cooldown/Cooldowns.java b/paper/src/main/java/games/negative/engine/paper/cooldown/Cooldowns.java
new file mode 100644
index 0000000..aa868ae
--- /dev/null
+++ b/paper/src/main/java/games/negative/engine/paper/cooldown/Cooldowns.java
@@ -0,0 +1,60 @@
+package games.negative.engine.paper.cooldown;
+
+import com.google.common.collect.Table;
+import com.google.common.collect.Tables;
+import games.negative.moss.spring.SpringComponent;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerQuitEvent;
+
+import java.time.Duration;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+@SpringComponent
+public class Cooldowns implements Listener {
+
+ private static final Table COOLDOWNS = Tables.newCustomTable(
+ new ConcurrentHashMap<>(),
+ ConcurrentHashMap::new
+ );
+
+ /**
+ * Adds a cooldown for the specified UUID and key.
+ * @param uuid UUID of the entity
+ * @param key Cooldown key
+ * @param millis Duration of the cooldown in milliseconds
+ */
+ public static void addCooldown(UUID uuid, String key, long millis) {
+ long expiryTime = System.currentTimeMillis() + millis;
+
+ COOLDOWNS.put(uuid, key, expiryTime);
+ }
+
+ /**
+ * Adds a cooldown for the specified UUID and key.
+ * @param uuid UUID of the entity
+ * @param key Cooldown key
+ * @param duration Duration of the cooldown
+ */
+ public static void addCooldown(UUID uuid, String key, Duration duration) {
+ addCooldown(uuid, key, duration.toMillis());
+ }
+
+ /**
+ * Checks if the specified UUID and key is on cooldown.
+ * @param uuid UUID of the entity
+ * @param key Cooldown key
+ * @return true if on cooldown, false otherwise
+ */
+ public static boolean isOnCooldown(UUID uuid, String key) {
+ return COOLDOWNS.contains(uuid, key) && COOLDOWNS.get(uuid, key) > System.currentTimeMillis();
+ }
+
+ @EventHandler
+ public void onQuit(PlayerQuitEvent event) {
+ COOLDOWNS.row(event.getPlayer().getUniqueId()).clear();
+ }
+
+}
+
diff --git a/paper/src/main/java/games/negative/engine/paper/util/JsonUtil.java b/paper/src/main/java/games/negative/engine/paper/util/JsonUtil.java
new file mode 100644
index 0000000..01628c3
--- /dev/null
+++ b/paper/src/main/java/games/negative/engine/paper/util/JsonUtil.java
@@ -0,0 +1,138 @@
+package games.negative.engine.paper.util;
+
+
+import com.google.gson.Gson;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.*;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+/**
+ * Utility class for JSON operations using Gson.
+ */
+@Slf4j
+public final class JsonUtil {
+
+ private JsonUtil() {
+ throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
+ }
+
+ /**
+ * Loads a JSON object from a file.
+ * @param file the file to load from
+ * @param clazz the class of the object to load
+ * @param gson the Gson instance to use for deserialization
+ * @param the type of the object to load
+ * @return an Optional containing the loaded object, or empty if loading failed
+ */
+ public static Optional loadFromFile(File file, Class clazz, Gson gson) {
+ if (file == null || !file.exists() || !file.isFile()) {
+ log.error("File {} does not exist or is not a valid file", file != null ? file.getAbsolutePath() : "null");
+ return Optional.empty();
+ }
+
+ try (Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
+ T object = gson.fromJson(reader, clazz);
+ return Optional.ofNullable(object);
+ } catch (IOException e) {
+ log.error("Failed to load json from file {}", file.getAbsolutePath(), e);
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Loads a JSON object from a file using a Type.
+ * @param file the file to load from
+ * @param type the Type of the object to load
+ * @param gson the Gson instance to use for deserialization
+ * @param the type of the object to load
+ * @return an Optional containing the loaded object, or empty if loading failed
+ */
+ public static Optional loadTypeFromFile(File file, Type type, Gson gson) {
+ if (file == null || !file.exists() || !file.isFile()) {
+ log.error("File {} does not exist or is not a valid file", file != null ? file.getAbsolutePath() : "null");
+ return Optional.empty();
+ }
+
+ try (Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
+ T object = gson.fromJson(reader, type);
+ return Optional.ofNullable(object);
+ } catch (IOException e) {
+ log.error("Failed to load json from file {}", file.getAbsolutePath(), e);
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Loads all JSON objects from a directory.
+ * @param directory the directory to load from
+ * @param clazz the class of the objects to load
+ * @param gson the Gson instance to use for deserialization
+ * @param the type of the objects to load
+ * @return a collection of loaded objects
+ */
+ public static Collection loadFromDirectory(File directory, Class clazz, Gson gson) {
+ if (directory == null || !directory.exists() || !directory.isDirectory()) {
+ log.error("Directory {} does not exist or is not a valid directory", directory != null ? directory.getAbsolutePath() : "null");
+ return Collections.emptyList();
+ }
+
+ List objects = new ArrayList<>();
+
+ File[] files = directory.listFiles((dir, name) -> name.toLowerCase().endsWith(".json"));
+ if (files == null) return objects;
+
+ for (File file : files) {
+ loadFromFile(file, clazz, gson).ifPresent(objects::add);
+ }
+
+ return objects;
+ }
+
+ /**
+ * Saves a JSON object to a file.
+ * @param file the file to save to
+ * @param object the object to save
+ * @param gson the Gson instance to use for serialization
+ * @param the type of the object to save
+ */
+ public static void saveToFile(File file, T object, Gson gson) {
+ if (file == null) {
+ log.error("File is null, cannot save object");
+ return;
+ }
+
+ if (!file.exists()) {
+ try {
+ file.createNewFile();
+ } catch (IOException e) {
+ log.error("Failed to save file {}", file.getAbsolutePath(), e);
+ return;
+ }
+ }
+
+ try (Writer writer = new FileWriter(file, StandardCharsets.UTF_8)) {
+ gson.toJson(object, writer);
+ } catch (IOException e) {
+ log.error("Failed to save to file {}", file.getAbsolutePath(), e);
+ }
+ }
+
+ /**
+ * Saves a JSON object to a file using a Type.
+ * @param file the file to save to
+ * @param type the Type of the object to save
+ * @param gson the Gson instance to use for serialization
+ */
+ public static void saveTypeToFile(File file, Type type, Gson gson) {
+ try (Writer writer = new FileWriter(file, StandardCharsets.UTF_8)) {
+ gson.toJson(type, writer);
+ } catch (IOException e) {
+ log.error("Failed to save to file {}", file.getAbsolutePath(), e);
+ }
+ }
+
+}
+