listener) {
+ EventBus.unregister(eventClass, listener);
+ }
+
+ // ========================================================================
+ // Manager Accessors (for advanced use)
+ // ========================================================================
+
+ /**
+ * Gets the HomeManager for direct access to home operations.
+ *
+ * @return the HomeManager, or null if the homes module is unavailable
+ */
+ @Nullable
+ public static HomeManager getHomeManager() {
+ return homeManager();
+ }
+
+ /**
+ * Gets the WarpManager for direct access to warp operations.
+ *
+ * @return the WarpManager, or null if the warps module is unavailable
+ */
+ @Nullable
+ public static WarpManager getWarpManager() {
+ return warpManager();
+ }
+
+ /**
+ * Gets the SpawnManager for direct access to spawn operations.
+ *
+ * @return the SpawnManager, or null if the spawns module is unavailable
+ */
+ @Nullable
+ public static SpawnManager getSpawnManager() {
+ return spawnManager();
+ }
+
+ /**
+ * Gets the KitManager for direct access to kit operations.
+ *
+ * @return the KitManager, or null if the kits module is unavailable
+ */
+ @Nullable
+ public static KitManager getKitManager() {
+ return kitManager();
+ }
+
+ /**
+ * Gets the ModerationManager for direct access to moderation operations.
+ *
+ * @return the ModerationManager, or null if the moderation module is unavailable
+ */
+ @Nullable
+ public static ModerationManager getModerationManager() {
+ return moderationManager();
+ }
+
+ /**
+ * Gets the UtilityManager for direct access to utility state operations.
+ *
+ * @return the UtilityManager, or null if the utility module is unavailable
+ */
+ @Nullable
+ public static UtilityManager getUtilityManager() {
+ return utilityManager();
+ }
+
+ /**
+ * Gets the TpaManager for direct access to TPA request operations.
+ *
+ * @return the TpaManager, or null if the teleport module is unavailable
+ */
+ @Nullable
+ public static TpaManager getTpaManager() {
+ return tpaManager();
+ }
+
+ /**
+ * Gets the BackManager for direct access to back history operations.
+ *
+ * @return the BackManager, or null if the teleport module is unavailable
+ */
+ @Nullable
+ public static BackManager getBackManager() {
+ return backManager();
+ }
+
+ // ========================================================================
+ // Private Helpers
+ // ========================================================================
+
+ @Nullable
+ private static HomeManager homeManager() {
+ if (!isAvailable()) return null;
+ HomesModule m = instance.getHomesModule();
+ return (m != null && m.isEnabled()) ? m.getHomeManager() : null;
+ }
+
+ @Nullable
+ private static WarpManager warpManager() {
+ if (!isAvailable()) return null;
+ WarpsModule m = instance.getWarpsModule();
+ return (m != null && m.isEnabled()) ? m.getWarpManager() : null;
+ }
+
+ @Nullable
+ private static SpawnManager spawnManager() {
+ if (!isAvailable()) return null;
+ SpawnsModule m = instance.getSpawnsModule();
+ return (m != null && m.isEnabled()) ? m.getSpawnManager() : null;
+ }
+
+ @Nullable
+ private static KitManager kitManager() {
+ if (!isAvailable()) return null;
+ KitsModule m = instance.getKitsModule();
+ return (m != null && m.isEnabled()) ? m.getKitManager() : null;
+ }
@Nullable
- private static WarpManager getWarpManager() {
+ private static ModerationManager moderationManager() {
if (!isAvailable()) return null;
- WarpsModule module = instance.getWarpsModule();
- return (module != null && module.isEnabled()) ? module.getWarpManager() : null;
+ ModerationModule m = instance.getModerationModule();
+ return (m != null && m.isEnabled()) ? m.getModerationManager() : null;
}
@Nullable
- private static SpawnManager getSpawnManager() {
+ private static UtilityManager utilityManager() {
if (!isAvailable()) return null;
- SpawnsModule module = instance.getSpawnsModule();
- return (module != null && module.isEnabled()) ? module.getSpawnManager() : null;
+ UtilityModule m = instance.getUtilityModule();
+ return (m != null && m.isEnabled()) ? m.getUtilityManager() : null;
}
@Nullable
- private static BackManager getBackManager() {
+ private static TpaManager tpaManager() {
if (!isAvailable()) return null;
- TeleportModule module = instance.getTeleportModule();
- return (module != null && module.isEnabled()) ? module.getBackManager() : null;
+ TeleportModule m = instance.getTeleportModule();
+ return (m != null && m.isEnabled()) ? m.getTpaManager() : null;
}
@Nullable
- private static TpaManager getTpaManager() {
+ private static BackManager backManager() {
if (!isAvailable()) return null;
- TeleportModule module = instance.getTeleportModule();
- return (module != null && module.isEnabled()) ? module.getTpaManager() : null;
+ TeleportModule m = instance.getTeleportModule();
+ return (m != null && m.isEnabled()) ? m.getBackManager() : null;
}
}
diff --git a/src/main/java/com/hyperessentials/api/events/Cancellable.java b/src/main/java/com/hyperessentials/api/events/Cancellable.java
new file mode 100644
index 0000000..9615c63
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/Cancellable.java
@@ -0,0 +1,23 @@
+package com.hyperessentials.api.events;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Interface for events that can be cancelled by listeners.
+ * When a pre-event is cancelled, the corresponding action is aborted.
+ *
+ * Listeners can optionally provide a cancellation reason via
+ * {@link #setCancelReason(String)}, which will be sent to the player
+ * instead of the default denial message.
+ */
+public interface Cancellable {
+
+ boolean isCancelled();
+
+ void setCancelled(boolean cancelled);
+
+ @Nullable
+ String getCancelReason();
+
+ void setCancelReason(@Nullable String reason);
+}
diff --git a/src/main/java/com/hyperessentials/api/events/EventBus.java b/src/main/java/com/hyperessentials/api/events/EventBus.java
index 98b7d16..fce3522 100644
--- a/src/main/java/com/hyperessentials/api/events/EventBus.java
+++ b/src/main/java/com/hyperessentials/api/events/EventBus.java
@@ -1,48 +1,81 @@
package com.hyperessentials.api.events;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.List;
-import java.util.Map;
+import com.hyperessentials.util.ErrorHandler;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
+import org.jetbrains.annotations.NotNull;
/**
* Simple event bus for HyperEssentials events.
*/
public final class EventBus {
- private static final Map, List>> handlers = new ConcurrentHashMap<>();
+ private static final Map, List>> listeners = new ConcurrentHashMap<>();
private EventBus() {}
- public static void register(@NotNull Class eventClass, @NotNull Consumer handler) {
- handlers.computeIfAbsent(eventClass, k -> new CopyOnWriteArrayList<>()).add(handler);
+ /**
+ * Registers a listener for an event type.
+ */
+ public static void register(@NotNull Class eventClass, @NotNull Consumer listener) {
+ listeners.computeIfAbsent(eventClass, k -> Collections.synchronizedList(new ArrayList<>()))
+ .add(listener);
+ }
+
+ /**
+ * Unregisters a listener for an event type.
+ */
+ public static void unregister(@NotNull Class eventClass, @NotNull Consumer listener) {
+ List> list = listeners.get(eventClass);
+ if (list != null) {
+ list.remove(listener);
+ }
}
- public static void unregister(@NotNull Class eventClass, @NotNull Consumer handler) {
- List> list = handlers.get(eventClass);
+ /**
+ * Publishes an event to all registered listeners.
+ */
+ @SuppressWarnings("unchecked")
+ public static void publish(@NotNull T event) {
+ List> list = listeners.get(event.getClass());
if (list != null) {
- list.remove(handler);
+ for (Consumer> listener : list) {
+ try {
+ ((Consumer) listener).accept(event);
+ } catch (Exception e) {
+ ErrorHandler.report("Event bus listener error", e);
+ }
+ }
}
}
+ /**
+ * Publishes a cancellable event to all registered listeners.
+ * Returns true if the event was cancelled by any listener.
+ */
@SuppressWarnings("unchecked")
- public static void fire(@NotNull T event) {
- List> list = handlers.get(event.getClass());
+ public static boolean publishCancellable(@NotNull T event) {
+ List> list = listeners.get(event.getClass());
if (list != null) {
- for (Consumer> handler : list) {
+ for (Consumer> listener : list) {
try {
- ((Consumer) handler).accept(event);
+ ((Consumer) listener).accept(event);
+ if (event.isCancelled()) {
+ return true;
+ }
} catch (Exception e) {
- e.printStackTrace();
+ ErrorHandler.report("Event bus listener error", e);
}
}
}
+ return event.isCancelled();
}
- public static void clear() {
- handlers.clear();
+ /**
+ * Clears all listeners.
+ */
+ public static void clearAll() {
+ listeners.clear();
}
}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeDefaultSetEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeDefaultSetEvent.java
new file mode 100644
index 0000000..c2ad1bf
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeDefaultSetEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.homes;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record HomeDefaultSetEvent(
+ @NotNull UUID playerUuid,
+ @NotNull String homeName
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeDefaultSetPreEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeDefaultSetPreEvent.java
new file mode 100644
index 0000000..5cc0a2a
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeDefaultSetPreEvent.java
@@ -0,0 +1,28 @@
+package com.hyperessentials.api.events.homes;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class HomeDefaultSetPreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final String homeName;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public HomeDefaultSetPreEvent(@NotNull UUID playerUuid, @NotNull String homeName) {
+ this.playerUuid = playerUuid;
+ this.homeName = homeName;
+ }
+
+ @NotNull public UUID playerUuid() { return playerUuid; }
+ @NotNull public String homeName() { return homeName; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeDeleteEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeDeleteEvent.java
new file mode 100644
index 0000000..2fee018
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeDeleteEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.homes;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record HomeDeleteEvent(
+ @NotNull UUID playerUuid,
+ @NotNull String homeName
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeDeletePreEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeDeletePreEvent.java
new file mode 100644
index 0000000..e12ee01
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeDeletePreEvent.java
@@ -0,0 +1,28 @@
+package com.hyperessentials.api.events.homes;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class HomeDeletePreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final String homeName;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public HomeDeletePreEvent(@NotNull UUID playerUuid, @NotNull String homeName) {
+ this.playerUuid = playerUuid;
+ this.homeName = homeName;
+ }
+
+ @NotNull public UUID playerUuid() { return playerUuid; }
+ @NotNull public String homeName() { return homeName; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeSetEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeSetEvent.java
new file mode 100644
index 0000000..9b6d6b1
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeSetEvent.java
@@ -0,0 +1,14 @@
+package com.hyperessentials.api.events.homes;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record HomeSetEvent(
+ @NotNull UUID playerUuid,
+ @NotNull String homeName,
+ @NotNull String world,
+ double x,
+ double y,
+ double z
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeSetPreEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeSetPreEvent.java
new file mode 100644
index 0000000..de4ba23
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeSetPreEvent.java
@@ -0,0 +1,41 @@
+package com.hyperessentials.api.events.homes;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class HomeSetPreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final String homeName;
+ private final String world;
+ private final double x;
+ private final double y;
+ private final double z;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public HomeSetPreEvent(@NotNull UUID playerUuid, @NotNull String homeName,
+ @NotNull String world, double x, double y, double z) {
+ this.playerUuid = playerUuid;
+ this.homeName = homeName;
+ this.world = world;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ @NotNull public UUID playerUuid() { return playerUuid; }
+ @NotNull public String homeName() { return homeName; }
+ @NotNull public String world() { return world; }
+ public double x() { return x; }
+ public double y() { return y; }
+ public double z() { return z; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeShareEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeShareEvent.java
new file mode 100644
index 0000000..2f41958
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeShareEvent.java
@@ -0,0 +1,11 @@
+package com.hyperessentials.api.events.homes;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record HomeShareEvent(
+ @NotNull UUID ownerUuid,
+ @NotNull String homeName,
+ @NotNull UUID targetUuid
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeSharePreEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeSharePreEvent.java
new file mode 100644
index 0000000..87658e6
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeSharePreEvent.java
@@ -0,0 +1,32 @@
+package com.hyperessentials.api.events.homes;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class HomeSharePreEvent implements Cancellable {
+
+ private final UUID ownerUuid;
+ private final String homeName;
+ private final UUID targetUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public HomeSharePreEvent(@NotNull UUID ownerUuid, @NotNull String homeName,
+ @NotNull UUID targetUuid) {
+ this.ownerUuid = ownerUuid;
+ this.homeName = homeName;
+ this.targetUuid = targetUuid;
+ }
+
+ @NotNull public UUID ownerUuid() { return ownerUuid; }
+ @NotNull public String homeName() { return homeName; }
+ @NotNull public UUID targetUuid() { return targetUuid; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeTeleportEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeTeleportEvent.java
new file mode 100644
index 0000000..a64abff
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeTeleportEvent.java
@@ -0,0 +1,11 @@
+package com.hyperessentials.api.events.homes;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record HomeTeleportEvent(
+ @NotNull UUID playerUuid,
+ @NotNull String homeName,
+ @NotNull String world
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeTeleportPreEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeTeleportPreEvent.java
new file mode 100644
index 0000000..41251b2
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeTeleportPreEvent.java
@@ -0,0 +1,32 @@
+package com.hyperessentials.api.events.homes;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class HomeTeleportPreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final String homeName;
+ private final String world;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public HomeTeleportPreEvent(@NotNull UUID playerUuid, @NotNull String homeName,
+ @NotNull String world) {
+ this.playerUuid = playerUuid;
+ this.homeName = homeName;
+ this.world = world;
+ }
+
+ @NotNull public UUID playerUuid() { return playerUuid; }
+ @NotNull public String homeName() { return homeName; }
+ @NotNull public String world() { return world; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeUnshareEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeUnshareEvent.java
new file mode 100644
index 0000000..bab61f2
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeUnshareEvent.java
@@ -0,0 +1,11 @@
+package com.hyperessentials.api.events.homes;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record HomeUnshareEvent(
+ @NotNull UUID ownerUuid,
+ @NotNull String homeName,
+ @NotNull UUID targetUuid
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/homes/HomeUnsharePreEvent.java b/src/main/java/com/hyperessentials/api/events/homes/HomeUnsharePreEvent.java
new file mode 100644
index 0000000..37d40b1
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/homes/HomeUnsharePreEvent.java
@@ -0,0 +1,32 @@
+package com.hyperessentials.api.events.homes;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class HomeUnsharePreEvent implements Cancellable {
+
+ private final UUID ownerUuid;
+ private final String homeName;
+ private final UUID targetUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public HomeUnsharePreEvent(@NotNull UUID ownerUuid, @NotNull String homeName,
+ @NotNull UUID targetUuid) {
+ this.ownerUuid = ownerUuid;
+ this.homeName = homeName;
+ this.targetUuid = targetUuid;
+ }
+
+ @NotNull public UUID ownerUuid() { return ownerUuid; }
+ @NotNull public String homeName() { return homeName; }
+ @NotNull public UUID targetUuid() { return targetUuid; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/kits/KitClaimEvent.java b/src/main/java/com/hyperessentials/api/events/kits/KitClaimEvent.java
new file mode 100644
index 0000000..0b054a0
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/kits/KitClaimEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.kits;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player successfully claims a kit.
+ */
+public record KitClaimEvent(@NotNull UUID playerUuid, @NotNull String kitName) {}
diff --git a/src/main/java/com/hyperessentials/api/events/kits/KitClaimPreEvent.java b/src/main/java/com/hyperessentials/api/events/kits/KitClaimPreEvent.java
new file mode 100644
index 0000000..37281d2
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/kits/KitClaimPreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.kits;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player claims a kit.
+ * Cancel to prevent the kit from being given.
+ */
+public final class KitClaimPreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final String kitName;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public KitClaimPreEvent(@NotNull UUID playerUuid, @NotNull String kitName) {
+ this.playerUuid = playerUuid;
+ this.kitName = kitName;
+ }
+
+ @NotNull
+ public UUID playerUuid() {
+ return playerUuid;
+ }
+
+ @NotNull
+ public String kitName() {
+ return kitName;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/kits/KitCreateEvent.java b/src/main/java/com/hyperessentials/api/events/kits/KitCreateEvent.java
new file mode 100644
index 0000000..c8ef6d2
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/kits/KitCreateEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.kits;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a new kit is successfully created.
+ */
+public record KitCreateEvent(@NotNull String kitName, @NotNull UUID actorUuid) {}
diff --git a/src/main/java/com/hyperessentials/api/events/kits/KitCreatePreEvent.java b/src/main/java/com/hyperessentials/api/events/kits/KitCreatePreEvent.java
new file mode 100644
index 0000000..046c63c
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/kits/KitCreatePreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.kits;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a new kit is created.
+ * Cancel to prevent the kit from being created.
+ */
+public final class KitCreatePreEvent implements Cancellable {
+
+ private final String kitName;
+ private final UUID actorUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public KitCreatePreEvent(@NotNull String kitName, @NotNull UUID actorUuid) {
+ this.kitName = kitName;
+ this.actorUuid = actorUuid;
+ }
+
+ @NotNull
+ public String kitName() {
+ return kitName;
+ }
+
+ @NotNull
+ public UUID actorUuid() {
+ return actorUuid;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/kits/KitDeleteEvent.java b/src/main/java/com/hyperessentials/api/events/kits/KitDeleteEvent.java
new file mode 100644
index 0000000..6f374f0
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/kits/KitDeleteEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.kits;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a kit is successfully deleted.
+ */
+public record KitDeleteEvent(@NotNull String kitName, @NotNull UUID actorUuid) {}
diff --git a/src/main/java/com/hyperessentials/api/events/kits/KitDeletePreEvent.java b/src/main/java/com/hyperessentials/api/events/kits/KitDeletePreEvent.java
new file mode 100644
index 0000000..ebc7bdb
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/kits/KitDeletePreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.kits;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a kit is deleted.
+ * Cancel to prevent the kit from being deleted.
+ */
+public final class KitDeletePreEvent implements Cancellable {
+
+ private final String kitName;
+ private final UUID actorUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public KitDeletePreEvent(@NotNull String kitName, @NotNull UUID actorUuid) {
+ this.kitName = kitName;
+ this.actorUuid = actorUuid;
+ }
+
+ @NotNull
+ public String kitName() {
+ return kitName;
+ }
+
+ @NotNull
+ public UUID actorUuid() {
+ return actorUuid;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerBanEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerBanEvent.java
new file mode 100644
index 0000000..998a3c1
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerBanEvent.java
@@ -0,0 +1,12 @@
+package com.hyperessentials.api.events.moderation;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player is successfully banned.
+ */
+public record PlayerBanEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid,
+ @NotNull String reason, @Nullable Long durationMs) {}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerBanPreEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerBanPreEvent.java
new file mode 100644
index 0000000..224e323
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerBanPreEvent.java
@@ -0,0 +1,73 @@
+package com.hyperessentials.api.events.moderation;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player is banned.
+ * Cancel to prevent the ban.
+ */
+public final class PlayerBanPreEvent implements Cancellable {
+
+ private final UUID targetUuid;
+ private final UUID actorUuid;
+ private final String reason;
+ private final Long durationMs;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public PlayerBanPreEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid,
+ @NotNull String reason, @Nullable Long durationMs) {
+ this.targetUuid = targetUuid;
+ this.actorUuid = actorUuid;
+ this.reason = reason;
+ this.durationMs = durationMs;
+ }
+
+ @NotNull
+ public UUID targetUuid() {
+ return targetUuid;
+ }
+
+ @NotNull
+ public UUID actorUuid() {
+ return actorUuid;
+ }
+
+ @NotNull
+ public String reason() {
+ return reason;
+ }
+
+ /**
+ * Duration in milliseconds, or {@code null} for a permanent ban.
+ */
+ @Nullable
+ public Long durationMs() {
+ return durationMs;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerKickEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerKickEvent.java
new file mode 100644
index 0000000..a7fd3d7
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerKickEvent.java
@@ -0,0 +1,11 @@
+package com.hyperessentials.api.events.moderation;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player is successfully kicked.
+ */
+public record PlayerKickEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid,
+ @NotNull String reason) {}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerKickPreEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerKickPreEvent.java
new file mode 100644
index 0000000..c32432e
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerKickPreEvent.java
@@ -0,0 +1,63 @@
+package com.hyperessentials.api.events.moderation;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player is kicked.
+ * Cancel to prevent the kick.
+ */
+public final class PlayerKickPreEvent implements Cancellable {
+
+ private final UUID targetUuid;
+ private final UUID actorUuid;
+ private final String reason;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public PlayerKickPreEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid,
+ @NotNull String reason) {
+ this.targetUuid = targetUuid;
+ this.actorUuid = actorUuid;
+ this.reason = reason;
+ }
+
+ @NotNull
+ public UUID targetUuid() {
+ return targetUuid;
+ }
+
+ @NotNull
+ public UUID actorUuid() {
+ return actorUuid;
+ }
+
+ @NotNull
+ public String reason() {
+ return reason;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerMuteEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerMuteEvent.java
new file mode 100644
index 0000000..0cb8627
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerMuteEvent.java
@@ -0,0 +1,12 @@
+package com.hyperessentials.api.events.moderation;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player is successfully muted.
+ */
+public record PlayerMuteEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid,
+ @NotNull String reason, @Nullable Long durationMs) {}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerMutePreEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerMutePreEvent.java
new file mode 100644
index 0000000..87a956a
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerMutePreEvent.java
@@ -0,0 +1,73 @@
+package com.hyperessentials.api.events.moderation;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player is muted.
+ * Cancel to prevent the mute.
+ */
+public final class PlayerMutePreEvent implements Cancellable {
+
+ private final UUID targetUuid;
+ private final UUID actorUuid;
+ private final String reason;
+ private final Long durationMs;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public PlayerMutePreEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid,
+ @NotNull String reason, @Nullable Long durationMs) {
+ this.targetUuid = targetUuid;
+ this.actorUuid = actorUuid;
+ this.reason = reason;
+ this.durationMs = durationMs;
+ }
+
+ @NotNull
+ public UUID targetUuid() {
+ return targetUuid;
+ }
+
+ @NotNull
+ public UUID actorUuid() {
+ return actorUuid;
+ }
+
+ @NotNull
+ public String reason() {
+ return reason;
+ }
+
+ /**
+ * Duration in milliseconds, or {@code null} for a permanent mute.
+ */
+ @Nullable
+ public Long durationMs() {
+ return durationMs;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnbanEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnbanEvent.java
new file mode 100644
index 0000000..8162352
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnbanEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.moderation;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player is successfully unbanned.
+ */
+public record PlayerUnbanEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid) {}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnbanPreEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnbanPreEvent.java
new file mode 100644
index 0000000..2ff2e0c
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnbanPreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.moderation;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player is unbanned.
+ * Cancel to prevent the unban.
+ */
+public final class PlayerUnbanPreEvent implements Cancellable {
+
+ private final UUID targetUuid;
+ private final UUID actorUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public PlayerUnbanPreEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid) {
+ this.targetUuid = targetUuid;
+ this.actorUuid = actorUuid;
+ }
+
+ @NotNull
+ public UUID targetUuid() {
+ return targetUuid;
+ }
+
+ @NotNull
+ public UUID actorUuid() {
+ return actorUuid;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnmuteEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnmuteEvent.java
new file mode 100644
index 0000000..676f44b
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnmuteEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.moderation;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player is successfully unmuted.
+ */
+public record PlayerUnmuteEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid) {}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnmutePreEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnmutePreEvent.java
new file mode 100644
index 0000000..fd57205
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerUnmutePreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.moderation;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player is unmuted.
+ * Cancel to prevent the unmute.
+ */
+public final class PlayerUnmutePreEvent implements Cancellable {
+
+ private final UUID targetUuid;
+ private final UUID actorUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public PlayerUnmutePreEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid) {
+ this.targetUuid = targetUuid;
+ this.actorUuid = actorUuid;
+ }
+
+ @NotNull
+ public UUID targetUuid() {
+ return targetUuid;
+ }
+
+ @NotNull
+ public UUID actorUuid() {
+ return actorUuid;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerWarnEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerWarnEvent.java
new file mode 100644
index 0000000..3df4489
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerWarnEvent.java
@@ -0,0 +1,11 @@
+package com.hyperessentials.api.events.moderation;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player is successfully warned.
+ */
+public record PlayerWarnEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid,
+ @NotNull String reason) {}
diff --git a/src/main/java/com/hyperessentials/api/events/moderation/PlayerWarnPreEvent.java b/src/main/java/com/hyperessentials/api/events/moderation/PlayerWarnPreEvent.java
new file mode 100644
index 0000000..2971e6e
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/moderation/PlayerWarnPreEvent.java
@@ -0,0 +1,63 @@
+package com.hyperessentials.api.events.moderation;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player is warned.
+ * Cancel to prevent the warning.
+ */
+public final class PlayerWarnPreEvent implements Cancellable {
+
+ private final UUID targetUuid;
+ private final UUID actorUuid;
+ private final String reason;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public PlayerWarnPreEvent(@NotNull UUID targetUuid, @NotNull UUID actorUuid,
+ @NotNull String reason) {
+ this.targetUuid = targetUuid;
+ this.actorUuid = actorUuid;
+ this.reason = reason;
+ }
+
+ @NotNull
+ public UUID targetUuid() {
+ return targetUuid;
+ }
+
+ @NotNull
+ public UUID actorUuid() {
+ return actorUuid;
+ }
+
+ @NotNull
+ public String reason() {
+ return reason;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/spawns/SpawnDeleteEvent.java b/src/main/java/com/hyperessentials/api/events/spawns/SpawnDeleteEvent.java
new file mode 100644
index 0000000..b0762f6
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/spawns/SpawnDeleteEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.spawns;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record SpawnDeleteEvent(
+ @NotNull String worldUuid,
+ @NotNull UUID actorUuid
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/spawns/SpawnDeletePreEvent.java b/src/main/java/com/hyperessentials/api/events/spawns/SpawnDeletePreEvent.java
new file mode 100644
index 0000000..c46a441
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/spawns/SpawnDeletePreEvent.java
@@ -0,0 +1,28 @@
+package com.hyperessentials.api.events.spawns;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class SpawnDeletePreEvent implements Cancellable {
+
+ private final String worldUuid;
+ private final UUID actorUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public SpawnDeletePreEvent(@NotNull String worldUuid, @NotNull UUID actorUuid) {
+ this.worldUuid = worldUuid;
+ this.actorUuid = actorUuid;
+ }
+
+ @NotNull public String worldUuid() { return worldUuid; }
+ @NotNull public UUID actorUuid() { return actorUuid; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/spawns/SpawnSetEvent.java b/src/main/java/com/hyperessentials/api/events/spawns/SpawnSetEvent.java
new file mode 100644
index 0000000..a1f8ca8
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/spawns/SpawnSetEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.spawns;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record SpawnSetEvent(
+ @NotNull String worldUuid,
+ @NotNull UUID actorUuid
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/spawns/SpawnSetPreEvent.java b/src/main/java/com/hyperessentials/api/events/spawns/SpawnSetPreEvent.java
new file mode 100644
index 0000000..b9e349d
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/spawns/SpawnSetPreEvent.java
@@ -0,0 +1,28 @@
+package com.hyperessentials.api.events.spawns;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class SpawnSetPreEvent implements Cancellable {
+
+ private final String worldUuid;
+ private final UUID actorUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public SpawnSetPreEvent(@NotNull String worldUuid, @NotNull UUID actorUuid) {
+ this.worldUuid = worldUuid;
+ this.actorUuid = actorUuid;
+ }
+
+ @NotNull public String worldUuid() { return worldUuid; }
+ @NotNull public UUID actorUuid() { return actorUuid; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/spawns/SpawnTeleportEvent.java b/src/main/java/com/hyperessentials/api/events/spawns/SpawnTeleportEvent.java
new file mode 100644
index 0000000..1534c02
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/spawns/SpawnTeleportEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.spawns;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record SpawnTeleportEvent(
+ @NotNull UUID playerUuid,
+ @NotNull String worldUuid
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/spawns/SpawnTeleportPreEvent.java b/src/main/java/com/hyperessentials/api/events/spawns/SpawnTeleportPreEvent.java
new file mode 100644
index 0000000..e882a24
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/spawns/SpawnTeleportPreEvent.java
@@ -0,0 +1,28 @@
+package com.hyperessentials.api.events.spawns;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class SpawnTeleportPreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final String worldUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public SpawnTeleportPreEvent(@NotNull UUID playerUuid, @NotNull String worldUuid) {
+ this.playerUuid = playerUuid;
+ this.worldUuid = worldUuid;
+ }
+
+ @NotNull public UUID playerUuid() { return playerUuid; }
+ @NotNull public String worldUuid() { return worldUuid; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/teleport/BackTeleportEvent.java b/src/main/java/com/hyperessentials/api/events/teleport/BackTeleportEvent.java
new file mode 100644
index 0000000..cf1f8aa
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/teleport/BackTeleportEvent.java
@@ -0,0 +1,11 @@
+package com.hyperessentials.api.events.teleport;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player has teleported back to their previous location.
+ */
+public record BackTeleportEvent(@NotNull UUID playerUuid, @NotNull String world,
+ double x, double y, double z, @NotNull String source) {}
diff --git a/src/main/java/com/hyperessentials/api/events/teleport/BackTeleportPreEvent.java b/src/main/java/com/hyperessentials/api/events/teleport/BackTeleportPreEvent.java
new file mode 100644
index 0000000..ad74ba9
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/teleport/BackTeleportPreEvent.java
@@ -0,0 +1,82 @@
+package com.hyperessentials.api.events.teleport;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player teleports back to their previous location.
+ * Cancel to prevent the back-teleport.
+ */
+public final class BackTeleportPreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final String world;
+ private final double x;
+ private final double y;
+ private final double z;
+ private final String source;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public BackTeleportPreEvent(@NotNull UUID playerUuid, @NotNull String world,
+ double x, double y, double z, @NotNull String source) {
+ this.playerUuid = playerUuid;
+ this.world = world;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.source = source;
+ }
+
+ @NotNull
+ public UUID playerUuid() {
+ return playerUuid;
+ }
+
+ @NotNull
+ public String world() {
+ return world;
+ }
+
+ public double x() {
+ return x;
+ }
+
+ public double y() {
+ return y;
+ }
+
+ public double z() {
+ return z;
+ }
+
+ /** The source that recorded this back location (e.g. "tpa", "warp", "home", "death"). */
+ @NotNull
+ public String source() {
+ return source;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/teleport/RtpEvent.java b/src/main/java/com/hyperessentials/api/events/teleport/RtpEvent.java
new file mode 100644
index 0000000..d197b26
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/teleport/RtpEvent.java
@@ -0,0 +1,11 @@
+package com.hyperessentials.api.events.teleport;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a random teleport has completed successfully.
+ */
+public record RtpEvent(@NotNull UUID playerUuid, @NotNull String world,
+ double x, double y, double z) {}
diff --git a/src/main/java/com/hyperessentials/api/events/teleport/RtpPreEvent.java b/src/main/java/com/hyperessentials/api/events/teleport/RtpPreEvent.java
new file mode 100644
index 0000000..3998fda
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/teleport/RtpPreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.teleport;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a random teleport is initiated.
+ * Cancel to prevent the random teleport.
+ */
+public final class RtpPreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final String world;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public RtpPreEvent(@NotNull UUID playerUuid, @NotNull String world) {
+ this.playerUuid = playerUuid;
+ this.world = world;
+ }
+
+ @NotNull
+ public UUID playerUuid() {
+ return playerUuid;
+ }
+
+ @NotNull
+ public String world() {
+ return world;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/teleport/TpaAcceptEvent.java b/src/main/java/com/hyperessentials/api/events/teleport/TpaAcceptEvent.java
new file mode 100644
index 0000000..dbb1b2b
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/teleport/TpaAcceptEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.teleport;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a TPA request has been accepted successfully.
+ */
+public record TpaAcceptEvent(@NotNull UUID accepterUuid, @NotNull UUID requesterUuid) {}
diff --git a/src/main/java/com/hyperessentials/api/events/teleport/TpaAcceptPreEvent.java b/src/main/java/com/hyperessentials/api/events/teleport/TpaAcceptPreEvent.java
new file mode 100644
index 0000000..afe20b5
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/teleport/TpaAcceptPreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.teleport;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a TPA request is accepted.
+ * Cancel to prevent the acceptance.
+ */
+public final class TpaAcceptPreEvent implements Cancellable {
+
+ private final UUID accepterUuid;
+ private final UUID requesterUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public TpaAcceptPreEvent(@NotNull UUID accepterUuid, @NotNull UUID requesterUuid) {
+ this.accepterUuid = accepterUuid;
+ this.requesterUuid = requesterUuid;
+ }
+
+ @NotNull
+ public UUID accepterUuid() {
+ return accepterUuid;
+ }
+
+ @NotNull
+ public UUID requesterUuid() {
+ return requesterUuid;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/teleport/TpaDenyEvent.java b/src/main/java/com/hyperessentials/api/events/teleport/TpaDenyEvent.java
new file mode 100644
index 0000000..b3a77c7
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/teleport/TpaDenyEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.teleport;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a TPA request has been denied.
+ */
+public record TpaDenyEvent(@NotNull UUID denierUuid, @NotNull UUID requesterUuid) {}
diff --git a/src/main/java/com/hyperessentials/api/events/teleport/TpaDenyPreEvent.java b/src/main/java/com/hyperessentials/api/events/teleport/TpaDenyPreEvent.java
new file mode 100644
index 0000000..c6b56e9
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/teleport/TpaDenyPreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.teleport;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a TPA request is denied.
+ * Cancel to prevent the denial.
+ */
+public final class TpaDenyPreEvent implements Cancellable {
+
+ private final UUID denierUuid;
+ private final UUID requesterUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public TpaDenyPreEvent(@NotNull UUID denierUuid, @NotNull UUID requesterUuid) {
+ this.denierUuid = denierUuid;
+ this.requesterUuid = requesterUuid;
+ }
+
+ @NotNull
+ public UUID denierUuid() {
+ return denierUuid;
+ }
+
+ @NotNull
+ public UUID requesterUuid() {
+ return requesterUuid;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/teleport/TpaSendEvent.java b/src/main/java/com/hyperessentials/api/events/teleport/TpaSendEvent.java
new file mode 100644
index 0000000..0c96a76
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/teleport/TpaSendEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.teleport;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a TPA or TPAHere request has been sent successfully.
+ */
+public record TpaSendEvent(@NotNull UUID senderUuid, @NotNull UUID targetUuid, boolean here) {}
diff --git a/src/main/java/com/hyperessentials/api/events/teleport/TpaSendPreEvent.java b/src/main/java/com/hyperessentials/api/events/teleport/TpaSendPreEvent.java
new file mode 100644
index 0000000..7154073
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/teleport/TpaSendPreEvent.java
@@ -0,0 +1,62 @@
+package com.hyperessentials.api.events.teleport;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a TPA or TPAHere request is sent.
+ * Cancel to prevent the request from being sent.
+ */
+public final class TpaSendPreEvent implements Cancellable {
+
+ private final UUID senderUuid;
+ private final UUID targetUuid;
+ private final boolean here;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public TpaSendPreEvent(@NotNull UUID senderUuid, @NotNull UUID targetUuid, boolean here) {
+ this.senderUuid = senderUuid;
+ this.targetUuid = targetUuid;
+ this.here = here;
+ }
+
+ @NotNull
+ public UUID senderUuid() {
+ return senderUuid;
+ }
+
+ @NotNull
+ public UUID targetUuid() {
+ return targetUuid;
+ }
+
+ /** Whether this is a TPAHere request (true) or a regular TPA request (false). */
+ public boolean here() {
+ return here;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/utility/AfkToggleEvent.java b/src/main/java/com/hyperessentials/api/events/utility/AfkToggleEvent.java
new file mode 100644
index 0000000..9e9fe1b
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/utility/AfkToggleEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.utility;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player's AFK state has been toggled.
+ */
+public record AfkToggleEvent(@NotNull UUID playerUuid, boolean newState) {}
diff --git a/src/main/java/com/hyperessentials/api/events/utility/AfkTogglePreEvent.java b/src/main/java/com/hyperessentials/api/events/utility/AfkTogglePreEvent.java
new file mode 100644
index 0000000..e457c0b
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/utility/AfkTogglePreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.utility;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player's AFK state is toggled.
+ * Cancel to prevent the toggle.
+ */
+public final class AfkTogglePreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final boolean newState;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public AfkTogglePreEvent(@NotNull UUID playerUuid, boolean newState) {
+ this.playerUuid = playerUuid;
+ this.newState = newState;
+ }
+
+ @NotNull
+ public UUID playerUuid() {
+ return playerUuid;
+ }
+
+ /** The AFK state that will be set (true = going AFK, false = returning from AFK). */
+ public boolean newState() {
+ return newState;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/utility/FlyToggleEvent.java b/src/main/java/com/hyperessentials/api/events/utility/FlyToggleEvent.java
new file mode 100644
index 0000000..714555b
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/utility/FlyToggleEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.utility;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player's fly mode has been toggled.
+ */
+public record FlyToggleEvent(@NotNull UUID playerUuid, boolean newState) {}
diff --git a/src/main/java/com/hyperessentials/api/events/utility/FlyTogglePreEvent.java b/src/main/java/com/hyperessentials/api/events/utility/FlyTogglePreEvent.java
new file mode 100644
index 0000000..6e2a2e9
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/utility/FlyTogglePreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.utility;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player's fly mode is toggled.
+ * Cancel to prevent the toggle.
+ */
+public final class FlyTogglePreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final boolean newState;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public FlyTogglePreEvent(@NotNull UUID playerUuid, boolean newState) {
+ this.playerUuid = playerUuid;
+ this.newState = newState;
+ }
+
+ @NotNull
+ public UUID playerUuid() {
+ return playerUuid;
+ }
+
+ /** The fly state that will be set (true = enabling, false = disabling). */
+ public boolean newState() {
+ return newState;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/utility/GodToggleEvent.java b/src/main/java/com/hyperessentials/api/events/utility/GodToggleEvent.java
new file mode 100644
index 0000000..35b4178
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/utility/GodToggleEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.utility;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Fired after a player's god mode has been toggled.
+ */
+public record GodToggleEvent(@NotNull UUID playerUuid, boolean newState) {}
diff --git a/src/main/java/com/hyperessentials/api/events/utility/GodTogglePreEvent.java b/src/main/java/com/hyperessentials/api/events/utility/GodTogglePreEvent.java
new file mode 100644
index 0000000..83ad5f5
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/utility/GodTogglePreEvent.java
@@ -0,0 +1,55 @@
+package com.hyperessentials.api.events.utility;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Fired before a player's god mode is toggled.
+ * Cancel to prevent the toggle.
+ */
+public final class GodTogglePreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final boolean newState;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public GodTogglePreEvent(@NotNull UUID playerUuid, boolean newState) {
+ this.playerUuid = playerUuid;
+ this.newState = newState;
+ }
+
+ @NotNull
+ public UUID playerUuid() {
+ return playerUuid;
+ }
+
+ /** The god mode state that will be set (true = enabling, false = disabling). */
+ public boolean newState() {
+ return newState;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ @Override
+ @Nullable
+ public String getCancelReason() {
+ return cancelReason;
+ }
+
+ @Override
+ public void setCancelReason(@Nullable String reason) {
+ this.cancelReason = reason;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/warps/WarpDeleteEvent.java b/src/main/java/com/hyperessentials/api/events/warps/WarpDeleteEvent.java
new file mode 100644
index 0000000..51e56c2
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/warps/WarpDeleteEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.warps;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record WarpDeleteEvent(
+ @NotNull String warpName,
+ @NotNull UUID actorUuid
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/warps/WarpDeletePreEvent.java b/src/main/java/com/hyperessentials/api/events/warps/WarpDeletePreEvent.java
new file mode 100644
index 0000000..b41c3f0
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/warps/WarpDeletePreEvent.java
@@ -0,0 +1,28 @@
+package com.hyperessentials.api.events.warps;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class WarpDeletePreEvent implements Cancellable {
+
+ private final String warpName;
+ private final UUID actorUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public WarpDeletePreEvent(@NotNull String warpName, @NotNull UUID actorUuid) {
+ this.warpName = warpName;
+ this.actorUuid = actorUuid;
+ }
+
+ @NotNull public String warpName() { return warpName; }
+ @NotNull public UUID actorUuid() { return actorUuid; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/warps/WarpSetEvent.java b/src/main/java/com/hyperessentials/api/events/warps/WarpSetEvent.java
new file mode 100644
index 0000000..50ee8a8
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/warps/WarpSetEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.warps;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record WarpSetEvent(
+ @NotNull String warpName,
+ @NotNull UUID actorUuid
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/warps/WarpSetPreEvent.java b/src/main/java/com/hyperessentials/api/events/warps/WarpSetPreEvent.java
new file mode 100644
index 0000000..1b5ca2c
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/warps/WarpSetPreEvent.java
@@ -0,0 +1,28 @@
+package com.hyperessentials.api.events.warps;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class WarpSetPreEvent implements Cancellable {
+
+ private final String warpName;
+ private final UUID actorUuid;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public WarpSetPreEvent(@NotNull String warpName, @NotNull UUID actorUuid) {
+ this.warpName = warpName;
+ this.actorUuid = actorUuid;
+ }
+
+ @NotNull public String warpName() { return warpName; }
+ @NotNull public UUID actorUuid() { return actorUuid; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/api/events/warps/WarpTeleportEvent.java b/src/main/java/com/hyperessentials/api/events/warps/WarpTeleportEvent.java
new file mode 100644
index 0000000..5aca213
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/warps/WarpTeleportEvent.java
@@ -0,0 +1,10 @@
+package com.hyperessentials.api.events.warps;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public record WarpTeleportEvent(
+ @NotNull UUID playerUuid,
+ @NotNull String warpName
+) {}
diff --git a/src/main/java/com/hyperessentials/api/events/warps/WarpTeleportPreEvent.java b/src/main/java/com/hyperessentials/api/events/warps/WarpTeleportPreEvent.java
new file mode 100644
index 0000000..bfaa614
--- /dev/null
+++ b/src/main/java/com/hyperessentials/api/events/warps/WarpTeleportPreEvent.java
@@ -0,0 +1,28 @@
+package com.hyperessentials.api.events.warps;
+
+import com.hyperessentials.api.events.Cancellable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public final class WarpTeleportPreEvent implements Cancellable {
+
+ private final UUID playerUuid;
+ private final String warpName;
+ private boolean cancelled;
+ private String cancelReason;
+
+ public WarpTeleportPreEvent(@NotNull UUID playerUuid, @NotNull String warpName) {
+ this.playerUuid = playerUuid;
+ this.warpName = warpName;
+ }
+
+ @NotNull public UUID playerUuid() { return playerUuid; }
+ @NotNull public String warpName() { return warpName; }
+
+ @Override public boolean isCancelled() { return cancelled; }
+ @Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
+ @Override @Nullable public String getCancelReason() { return cancelReason; }
+ @Override public void setCancelReason(@Nullable String reason) { this.cancelReason = reason; }
+}
diff --git a/src/main/java/com/hyperessentials/integration/placeholder/HyperEssentialsExpansion.java b/src/main/java/com/hyperessentials/integration/placeholder/HyperEssentialsExpansion.java
new file mode 100644
index 0000000..98270cd
--- /dev/null
+++ b/src/main/java/com/hyperessentials/integration/placeholder/HyperEssentialsExpansion.java
@@ -0,0 +1,481 @@
+package com.hyperessentials.integration.placeholder;
+
+import at.helpch.placeholderapi.expansion.PlaceholderExpansion;
+import com.hyperessentials.BuildInfo;
+import com.hyperessentials.HyperEssentials;
+import com.hyperessentials.api.HyperEssentialsAPI;
+import com.hyperessentials.data.PlayerData;
+import com.hyperessentials.module.homes.HomeManager;
+import com.hyperessentials.module.homes.HomesModule;
+import com.hyperessentials.module.kits.KitManager;
+import com.hyperessentials.module.kits.KitsModule;
+import com.hyperessentials.module.moderation.ModerationManager;
+import com.hyperessentials.module.moderation.ModerationModule;
+import com.hyperessentials.module.moderation.VanishManager;
+import com.hyperessentials.module.moderation.data.Punishment;
+import com.hyperessentials.module.moderation.data.PunishmentType;
+import com.hyperessentials.module.spawns.SpawnManager;
+import com.hyperessentials.module.spawns.SpawnsModule;
+import com.hyperessentials.module.teleport.TeleportModule;
+import com.hyperessentials.module.teleport.TpaManager;
+import com.hyperessentials.module.utility.UtilityManager;
+import com.hyperessentials.module.utility.UtilityModule;
+import com.hyperessentials.module.warps.WarpManager;
+import com.hyperessentials.module.warps.WarpsModule;
+import com.hypixel.hytale.server.core.universe.PlayerRef;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * PlaceholderAPI expansion for HyperEssentials.
+ *
+ * Exposes essentials data as PAPI placeholders for use by scoreboards,
+ * holograms, chat formatting, and other PAPI consumers.
+ *
+ *
Homes:
+ * %essentials_home_count% - Number of player's homes
+ * %essentials_home_limit% - Player's max homes allowed
+ * %essentials_home_default% - Name of player's default home
+ * %essentials_has_homes% - Whether player has any homes (true/false)
+ *
+ *
Kits:
+ * %essentials_kit_count% - Total number of kits defined
+ * %essentials_kit_available_count% - Number of kits available to the player
+ * %essentials_kit_cooldown_% - Remaining cooldown seconds for a kit
+ *
+ * Moderation:
+ * %essentials_is_banned% - Whether player is banned (true/false)
+ * %essentials_is_muted% - Whether player is muted (true/false)
+ * %essentials_ban_reason% - Reason for active ban
+ * %essentials_mute_reason% - Reason for active mute
+ * %essentials_ban_expires% - Ban expiry date or "permanent"
+ * %essentials_mute_expires% - Mute expiry date or "permanent"
+ * %essentials_punishment_count% - Total number of punishments
+ * %essentials_warn_count% - Number of warnings
+ *
+ *
Utility:
+ * %essentials_is_afk% - Whether player is AFK (true/false)
+ * %essentials_is_flying% - Whether player is flying (true/false)
+ * %essentials_is_god% - Whether player has god mode (true/false)
+ * %essentials_is_infinite_stamina% - Whether player has infinite stamina (true/false)
+ * %essentials_is_vanished% - Whether player is vanished (true/false)
+ * %essentials_playtime% - Total playtime in milliseconds
+ * %essentials_playtime_formatted% - Total playtime formatted (e.g., 3d 2h 15m)
+ * %essentials_session_time% - Current session time in milliseconds
+ * %essentials_session_time_formatted% - Current session time formatted
+ * %essentials_first_join% - First join timestamp (ISO)
+ * %essentials_first_join_formatted% - First join date (yyyy-MM-dd HH:mm)
+ * %essentials_last_join% - Last join timestamp (ISO)
+ * %essentials_last_join_formatted% - Last join date (yyyy-MM-dd HH:mm)
+ *
+ *
Warps/Spawns/TPA:
+ * %essentials_warp_count% - Total number of warps
+ * %essentials_warp_accessible_count% - Number of warps accessible to the player
+ * %essentials_spawn_count% - Total number of spawns
+ * %essentials_is_accepting_tpa% - Whether player is accepting TPA requests (true/false)
+ * %essentials_tpa_pending_count% - Number of incoming TPA requests
+ */
+public class HyperEssentialsExpansion extends PlaceholderExpansion {
+
+ private static final DateTimeFormatter DATE_FORMAT =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault());
+
+ private final HyperEssentials plugin;
+
+ /** Creates a new HyperEssentialsExpansion. */
+ public HyperEssentialsExpansion(@NotNull HyperEssentials plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public @NotNull String getIdentifier() {
+ return "essentials";
+ }
+
+ @Override
+ public @NotNull String getAuthor() {
+ return "HyperSystems-Development";
+ }
+
+ @Override
+ public @NotNull String getVersion() {
+ return BuildInfo.VERSION;
+ }
+
+ /** Persist across reloads. */
+ @Override
+ public boolean persist() {
+ return true;
+ }
+
+ /** Called when a placeholder is requested. */
+ @Override
+ @Nullable
+ public String onPlaceholderRequest(@Nullable PlayerRef player, @NotNull String params) {
+ if (player == null) {
+ return null;
+ }
+
+ UUID uuid = player.getUuid();
+ if (uuid == null) {
+ return null;
+ }
+
+ // Handle dynamic kit cooldown placeholders before the switch
+ String lower = params.toLowerCase();
+ if (lower.startsWith("kit_cooldown_")) {
+ String kitName = lower.substring("kit_cooldown_".length());
+ KitManager km = kitManager();
+ if (km == null) return "";
+ long remainingMs = km.getRemainingCooldown(uuid, kitName);
+ return String.valueOf(remainingMs / 1000);
+ }
+
+ return switch (lower) {
+ // Homes
+ case "home_count" -> homeCount(uuid);
+ case "home_limit" -> homeLimit(uuid);
+ case "home_default" -> homeDefault(uuid);
+ case "has_homes" -> hasHomes(uuid);
+
+ // Kits
+ case "kit_count" -> kitCount();
+ case "kit_available_count" -> kitAvailableCount(uuid);
+
+ // Moderation
+ case "is_banned" -> isBanned(uuid);
+ case "is_muted" -> isMuted(uuid);
+ case "ban_reason" -> banReason(uuid);
+ case "mute_reason" -> muteReason(uuid);
+ case "ban_expires" -> banExpires(uuid);
+ case "mute_expires" -> muteExpires(uuid);
+ case "punishment_count" -> punishmentCount(uuid);
+ case "warn_count" -> warnCount(uuid);
+
+ // Utility
+ case "is_afk" -> isAfk(uuid);
+ case "is_flying" -> isFlying(uuid);
+ case "is_god" -> isGod(uuid);
+ case "is_infinite_stamina" -> isInfiniteStamina(uuid);
+ case "is_vanished" -> isVanished(uuid);
+ case "playtime" -> playtime(uuid);
+ case "playtime_formatted" -> playtimeFormatted(uuid);
+ case "session_time" -> sessionTime(uuid);
+ case "session_time_formatted" -> sessionTimeFormatted(uuid);
+ case "first_join" -> firstJoin(uuid);
+ case "first_join_formatted" -> firstJoinFormatted(uuid);
+ case "last_join" -> lastJoin(uuid);
+ case "last_join_formatted" -> lastJoinFormatted(uuid);
+
+ // Warps / Spawns / TPA
+ case "warp_count" -> warpCount();
+ case "warp_accessible_count" -> warpAccessibleCount(uuid);
+ case "spawn_count" -> spawnCount();
+ case "is_accepting_tpa" -> isAcceptingTpa(uuid);
+ case "tpa_pending_count" -> tpaPendingCount(uuid);
+
+ default -> null;
+ };
+ }
+
+ // ========== Homes ==========
+
+ private String homeCount(@NotNull UUID uuid) {
+ HomeManager hm = homeManager();
+ return hm != null ? String.valueOf(hm.getHomeCount(uuid)) : "0";
+ }
+
+ private String homeLimit(@NotNull UUID uuid) {
+ HomeManager hm = homeManager();
+ return hm != null ? String.valueOf(hm.getHomeLimit(uuid)) : "0";
+ }
+
+ private String homeDefault(@NotNull UUID uuid) {
+ HomeManager hm = homeManager();
+ if (hm == null) return "";
+ String def = hm.getDefaultHome(uuid);
+ return def != null ? def : "";
+ }
+
+ private String hasHomes(@NotNull UUID uuid) {
+ HomeManager hm = homeManager();
+ return hm != null ? String.valueOf(hm.getHomeCount(uuid) > 0) : "false";
+ }
+
+ // ========== Kits ==========
+
+ private String kitCount() {
+ KitManager km = kitManager();
+ return km != null ? String.valueOf(km.getAllKits().size()) : "0";
+ }
+
+ private String kitAvailableCount(@NotNull UUID uuid) {
+ KitManager km = kitManager();
+ return km != null ? String.valueOf(km.getAvailableKits(uuid).size()) : "0";
+ }
+
+ // ========== Moderation ==========
+
+ private String isBanned(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ return mm != null ? String.valueOf(mm.isBanned(uuid)) : "false";
+ }
+
+ private String isMuted(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ return mm != null ? String.valueOf(mm.isMuted(uuid)) : "false";
+ }
+
+ private String banReason(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ if (mm == null) return "";
+ Punishment ban = mm.getActiveBan(uuid);
+ if (ban == null) return "";
+ return ban.reason() != null ? ban.reason() : "";
+ }
+
+ private String muteReason(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ if (tm == null) return "";
+ PlayerData data = tm.getPlayerData(uuid);
+ if (data == null) return "";
+ Punishment mute = data.getActiveMute();
+ if (mute == null) return "";
+ return mute.reason() != null ? mute.reason() : "";
+ }
+
+ private String banExpires(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ if (mm == null) return "";
+ Punishment ban = mm.getActiveBan(uuid);
+ if (ban == null) return "";
+ if (ban.isPermanent()) return "permanent";
+ return formatInstant(ban.expiresAt());
+ }
+
+ private String muteExpires(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ if (tm == null) return "";
+ PlayerData data = tm.getPlayerData(uuid);
+ if (data == null) return "";
+ Punishment mute = data.getActiveMute();
+ if (mute == null) return "";
+ if (mute.isPermanent()) return "permanent";
+ return formatInstant(mute.expiresAt());
+ }
+
+ private String punishmentCount(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ if (mm == null) return "0";
+ return String.valueOf(mm.getHistory(uuid).size());
+ }
+
+ private String warnCount(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ if (mm == null) return "0";
+ List history = mm.getHistory(uuid);
+ long count = history.stream()
+ .filter(p -> p.type() == PunishmentType.WARN)
+ .count();
+ return String.valueOf(count);
+ }
+
+ // ========== Utility ==========
+
+ private String isAfk(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ return um != null ? String.valueOf(um.isAfk(uuid)) : "false";
+ }
+
+ private String isFlying(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ return um != null ? String.valueOf(um.isFlying(uuid)) : "false";
+ }
+
+ private String isGod(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ return um != null ? String.valueOf(um.isGod(uuid)) : "false";
+ }
+
+ private String isInfiniteStamina(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ return um != null ? String.valueOf(um.isInfiniteStamina(uuid)) : "false";
+ }
+
+ private String isVanished(@NotNull UUID uuid) {
+ VanishManager vm = vanishManager();
+ return vm != null ? String.valueOf(vm.isVanished(uuid)) : "false";
+ }
+
+ private String playtime(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ return um != null ? String.valueOf(um.getTotalPlaytimeMs(uuid)) : "0";
+ }
+
+ private String playtimeFormatted(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ if (um == null) return "0m";
+ return formatDuration(um.getTotalPlaytimeMs(uuid));
+ }
+
+ private String sessionTime(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ if (um == null) return "0";
+ Instant sessionStart = um.getSessionStart(uuid);
+ if (sessionStart == null) return "0";
+ return String.valueOf(Duration.between(sessionStart, Instant.now()).toMillis());
+ }
+
+ private String sessionTimeFormatted(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ if (um == null) return "0m";
+ Instant sessionStart = um.getSessionStart(uuid);
+ if (sessionStart == null) return "0m";
+ return formatDuration(Duration.between(sessionStart, Instant.now()).toMillis());
+ }
+
+ private String firstJoin(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ if (um == null) return "";
+ Instant instant = um.getFirstJoin(uuid);
+ return instant != null ? instant.toString() : "";
+ }
+
+ private String firstJoinFormatted(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ if (um == null) return "";
+ Instant instant = um.getFirstJoin(uuid);
+ return instant != null ? formatInstant(instant) : "";
+ }
+
+ private String lastJoin(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ if (tm == null) return "";
+ PlayerData data = tm.getPlayerData(uuid);
+ if (data == null) return "";
+ return data.getLastJoin().toString();
+ }
+
+ private String lastJoinFormatted(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ if (tm == null) return "";
+ PlayerData data = tm.getPlayerData(uuid);
+ if (data == null) return "";
+ return formatInstant(data.getLastJoin());
+ }
+
+ // ========== Warps / Spawns / TPA ==========
+
+ private String warpCount() {
+ WarpManager wm = warpManager();
+ return wm != null ? String.valueOf(wm.getWarpCount()) : "0";
+ }
+
+ private String warpAccessibleCount(@NotNull UUID uuid) {
+ WarpManager wm = warpManager();
+ return wm != null ? String.valueOf(wm.getAccessibleWarps(uuid).size()) : "0";
+ }
+
+ private String spawnCount() {
+ SpawnManager sm = spawnManager();
+ return sm != null ? String.valueOf(sm.getSpawnCount()) : "0";
+ }
+
+ private String isAcceptingTpa(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ return tm != null ? String.valueOf(tm.isAcceptingRequests(uuid)) : "true";
+ }
+
+ private String tpaPendingCount(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ return tm != null ? String.valueOf(tm.getIncomingRequests(uuid).size()) : "0";
+ }
+
+ // ========== Manager Accessors (null-safe) ==========
+
+ @Nullable
+ private HomeManager homeManager() {
+ HomesModule m = plugin.getHomesModule();
+ return (m != null && m.isEnabled()) ? m.getHomeManager() : null;
+ }
+
+ @Nullable
+ private KitManager kitManager() {
+ KitsModule m = plugin.getKitsModule();
+ return (m != null && m.isEnabled()) ? m.getKitManager() : null;
+ }
+
+ @Nullable
+ private ModerationManager moderationManager() {
+ ModerationModule m = plugin.getModerationModule();
+ return (m != null && m.isEnabled()) ? m.getModerationManager() : null;
+ }
+
+ @Nullable
+ private VanishManager vanishManager() {
+ ModerationModule m = plugin.getModerationModule();
+ return (m != null && m.isEnabled()) ? m.getVanishManager() : null;
+ }
+
+ @Nullable
+ private UtilityManager utilityManager() {
+ UtilityModule m = plugin.getUtilityModule();
+ return (m != null && m.isEnabled()) ? m.getUtilityManager() : null;
+ }
+
+ @Nullable
+ private WarpManager warpManager() {
+ WarpsModule m = plugin.getWarpsModule();
+ return (m != null && m.isEnabled()) ? m.getWarpManager() : null;
+ }
+
+ @Nullable
+ private SpawnManager spawnManager() {
+ SpawnsModule m = plugin.getSpawnsModule();
+ return (m != null && m.isEnabled()) ? m.getSpawnManager() : null;
+ }
+
+ @Nullable
+ private TpaManager tpaManager() {
+ TeleportModule m = plugin.getTeleportModule();
+ return (m != null && m.isEnabled()) ? m.getTpaManager() : null;
+ }
+
+ // ========== Helpers ==========
+
+ /**
+ * Formats a duration in milliseconds to a human-readable string.
+ * Examples: "3d 2h 15m", "5h 30m", "12m", "0m"
+ */
+ @NotNull
+ private static String formatDuration(long ms) {
+ if (ms <= 0) return "0m";
+
+ long totalMinutes = ms / 60_000;
+ long days = totalMinutes / 1440;
+ long hours = (totalMinutes % 1440) / 60;
+ long minutes = totalMinutes % 60;
+
+ StringBuilder sb = new StringBuilder();
+ if (days > 0) sb.append(days).append("d ");
+ if (hours > 0) sb.append(hours).append("h ");
+ sb.append(minutes).append("m");
+ return sb.toString().trim();
+ }
+
+ /**
+ * Formats an Instant to a human-readable date string (yyyy-MM-dd HH:mm).
+ */
+ @NotNull
+ private static String formatInstant(@Nullable Instant instant) {
+ if (instant == null) return "";
+ return DATE_FORMAT.format(instant);
+ }
+}
diff --git a/src/main/java/com/hyperessentials/integration/placeholder/PlaceholderAPIIntegration.java b/src/main/java/com/hyperessentials/integration/placeholder/PlaceholderAPIIntegration.java
new file mode 100644
index 0000000..8d1a021
--- /dev/null
+++ b/src/main/java/com/hyperessentials/integration/placeholder/PlaceholderAPIIntegration.java
@@ -0,0 +1,83 @@
+package com.hyperessentials.integration.placeholder;
+
+import com.hyperessentials.HyperEssentials;
+import com.hyperessentials.util.Logger;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Soft dependency integration with PlaceholderAPI.
+ *
+ *
+ * Detects PlaceholderAPI at runtime and registers the HyperEssentials
+ * expansion to expose essentials data as placeholders.
+ *
+ *
+ * Internal expansions must be manually registered since they are not
+ * separate JARs in the expansions folder.
+ */
+public final class PlaceholderAPIIntegration {
+
+ private static boolean available = false;
+
+ @Nullable
+ private static HyperEssentialsExpansion expansion;
+
+ private PlaceholderAPIIntegration() {}
+
+ /**
+ * Initializes PlaceholderAPI integration.
+ * Detects PAPI at runtime and registers the expansion.
+ * Must be called after all managers are initialized.
+ *
+ * @param plugin the HyperEssentials instance
+ */
+ public static void init(HyperEssentials plugin) {
+ try {
+ Class.forName("at.helpch.placeholderapi.PlaceholderAPI");
+ } catch (ClassNotFoundException e) {
+ Logger.info("[Integration] PlaceholderAPI not found - placeholders disabled");
+ return;
+ }
+
+ try {
+ expansion = new HyperEssentialsExpansion(plugin);
+ // Internal expansions must be manually registered
+ boolean registered = expansion.register();
+ if (registered) {
+ available = true;
+ Logger.info("[Integration] PlaceholderAPI expansion registered (%%essentials_*%%)");
+ } else {
+ Logger.warn("PlaceholderAPI expansion registration failed");
+ expansion = null;
+ }
+ } catch (Exception e) {
+ Logger.severe("Failed to register PlaceholderAPI expansion: %s", e.getMessage());
+ expansion = null;
+ }
+ }
+
+ /**
+ * Shuts down PlaceholderAPI integration.
+ * Unregisters the expansion.
+ */
+ public static void shutdown() {
+ if (expansion != null) {
+ try {
+ expansion.unregister();
+ } catch (Exception e) {
+ Logger.debug("Failed to unregister PlaceholderAPI expansion: %s", e.getMessage());
+ }
+ expansion = null;
+ }
+ available = false;
+ }
+
+ /**
+ * Checks if PlaceholderAPI is available and the expansion is registered.
+ *
+ * @return true if available
+ */
+ public static boolean isAvailable() {
+ return available;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/integration/placeholder/WiFlowExpansion.java b/src/main/java/com/hyperessentials/integration/placeholder/WiFlowExpansion.java
new file mode 100644
index 0000000..bcd4757
--- /dev/null
+++ b/src/main/java/com/hyperessentials/integration/placeholder/WiFlowExpansion.java
@@ -0,0 +1,507 @@
+package com.hyperessentials.integration.placeholder;
+
+import com.hyperessentials.BuildInfo;
+import com.hyperessentials.HyperEssentials;
+import com.hyperessentials.data.PlayerData;
+import com.hyperessentials.module.homes.HomeManager;
+import com.hyperessentials.module.homes.HomesModule;
+import com.hyperessentials.module.kits.KitManager;
+import com.hyperessentials.module.kits.KitsModule;
+import com.hyperessentials.module.moderation.ModerationManager;
+import com.hyperessentials.module.moderation.ModerationModule;
+import com.hyperessentials.module.moderation.VanishManager;
+import com.hyperessentials.module.moderation.data.Punishment;
+import com.hyperessentials.module.moderation.data.PunishmentType;
+import com.hyperessentials.module.spawns.SpawnManager;
+import com.hyperessentials.module.spawns.SpawnsModule;
+import com.hyperessentials.module.teleport.TeleportModule;
+import com.hyperessentials.module.teleport.TpaManager;
+import com.hyperessentials.module.utility.UtilityManager;
+import com.hyperessentials.module.utility.UtilityModule;
+import com.hyperessentials.module.warps.WarpManager;
+import com.hyperessentials.module.warps.WarpsModule;
+import com.wiflow.placeholderapi.context.PlaceholderContext;
+import com.wiflow.placeholderapi.expansion.PlaceholderExpansion;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * WiFlow PlaceholderAPI expansion for HyperEssentials.
+ *
+ *
Exposes essentials data as WiFlow placeholders ({@code {essentials_xxx}}) for use
+ * by scoreboards, holograms, chat formatting, and other WiFlow consumers.
+ *
+ *
Homes:
+ * {essentials_home_count} - Number of player's homes
+ * {essentials_home_limit} - Player's max homes allowed
+ * {essentials_home_default} - Name of player's default home
+ * {essentials_has_homes} - Whether player has any homes (true/false)
+ *
+ *
Kits:
+ * {essentials_kit_count} - Total number of kits defined
+ * {essentials_kit_available_count} - Number of kits available to the player
+ * {essentials_kit_cooldown_} - Remaining cooldown seconds for a kit
+ *
+ * Moderation:
+ * {essentials_is_banned} - Whether player is banned (true/false)
+ * {essentials_is_muted} - Whether player is muted (true/false)
+ * {essentials_ban_reason} - Reason for active ban
+ * {essentials_mute_reason} - Reason for active mute
+ * {essentials_ban_expires} - Ban expiry date or "permanent"
+ * {essentials_mute_expires} - Mute expiry date or "permanent"
+ * {essentials_punishment_count} - Total number of punishments
+ * {essentials_warn_count} - Number of warnings
+ *
+ *
Utility:
+ * {essentials_is_afk} - Whether player is AFK (true/false)
+ * {essentials_is_flying} - Whether player is flying (true/false)
+ * {essentials_is_god} - Whether player has god mode (true/false)
+ * {essentials_is_infinite_stamina} - Whether player has infinite stamina (true/false)
+ * {essentials_is_vanished} - Whether player is vanished (true/false)
+ * {essentials_playtime} - Total playtime in milliseconds
+ * {essentials_playtime_formatted} - Total playtime formatted (e.g., 3d 2h 15m)
+ * {essentials_session_time} - Current session time in milliseconds
+ * {essentials_session_time_formatted} - Current session time formatted
+ * {essentials_first_join} - First join timestamp (ISO)
+ * {essentials_first_join_formatted} - First join date (yyyy-MM-dd HH:mm)
+ * {essentials_last_join} - Last join timestamp (ISO)
+ * {essentials_last_join_formatted} - Last join date (yyyy-MM-dd HH:mm)
+ *
+ *
Warps/Spawns/TPA:
+ * {essentials_warp_count} - Total number of warps
+ * {essentials_warp_accessible_count} - Number of warps accessible to the player
+ * {essentials_spawn_count} - Total number of spawns
+ * {essentials_is_accepting_tpa} - Whether player is accepting TPA requests (true/false)
+ * {essentials_tpa_pending_count} - Number of incoming TPA requests
+ */
+public class WiFlowExpansion extends PlaceholderExpansion {
+
+ private static final DateTimeFormatter DATE_FORMAT =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault());
+
+ private final HyperEssentials plugin;
+
+ /** Creates a new WiFlowExpansion. */
+ public WiFlowExpansion(@NotNull HyperEssentials plugin) {
+ this.plugin = plugin;
+ }
+
+ /** Returns the identifier. */
+ @Override
+ public String getIdentifier() {
+ return "essentials";
+ }
+
+ /** Returns the author. */
+ @Override
+ public String getAuthor() {
+ return "HyperSystems-Development";
+ }
+
+ /** Returns the version. */
+ @Override
+ public String getVersion() {
+ return BuildInfo.VERSION;
+ }
+
+ /** Returns the name. */
+ @Override
+ public String getName() {
+ return "HyperEssentials";
+ }
+
+ @Override
+ public String getDescription() {
+ return "HyperEssentials data placeholders: homes, kits, moderation, utility, warps, spawns, TPA";
+ }
+
+ /** Persist across reloads. */
+ @Override
+ public boolean persist() {
+ return true;
+ }
+
+ /** Returns the placeholders. */
+ @Override
+ public List getPlaceholders() {
+ return List.of(
+ "home_count", "home_limit", "home_default", "has_homes",
+ "kit_count", "kit_available_count",
+ "is_banned", "is_muted", "ban_reason", "mute_reason",
+ "ban_expires", "mute_expires", "punishment_count", "warn_count",
+ "is_afk", "is_flying", "is_god", "is_infinite_stamina", "is_vanished",
+ "playtime", "playtime_formatted", "session_time", "session_time_formatted",
+ "first_join", "first_join_formatted", "last_join", "last_join_formatted",
+ "warp_count", "warp_accessible_count", "spawn_count",
+ "is_accepting_tpa", "tpa_pending_count"
+ );
+ }
+
+ /** Called when a placeholder is requested. */
+ @Override
+ @Nullable
+ public String onPlaceholderRequest(@NotNull PlaceholderContext context, @NotNull String params) {
+ UUID uuid = context.getPlayerUuid();
+ if (uuid == null) {
+ return null;
+ }
+
+ // Handle dynamic kit cooldown placeholders before the switch
+ String lower = params.toLowerCase();
+ if (lower.startsWith("kit_cooldown_")) {
+ String kitName = lower.substring("kit_cooldown_".length());
+ KitManager km = kitManager();
+ if (km == null) return "";
+ long remainingMs = km.getRemainingCooldown(uuid, kitName);
+ return String.valueOf(remainingMs / 1000);
+ }
+
+ return switch (lower) {
+ // Homes
+ case "home_count" -> homeCount(uuid);
+ case "home_limit" -> homeLimit(uuid);
+ case "home_default" -> homeDefault(uuid);
+ case "has_homes" -> hasHomes(uuid);
+
+ // Kits
+ case "kit_count" -> kitCount();
+ case "kit_available_count" -> kitAvailableCount(uuid);
+
+ // Moderation
+ case "is_banned" -> isBanned(uuid);
+ case "is_muted" -> isMuted(uuid);
+ case "ban_reason" -> banReason(uuid);
+ case "mute_reason" -> muteReason(uuid);
+ case "ban_expires" -> banExpires(uuid);
+ case "mute_expires" -> muteExpires(uuid);
+ case "punishment_count" -> punishmentCount(uuid);
+ case "warn_count" -> warnCount(uuid);
+
+ // Utility
+ case "is_afk" -> isAfk(uuid);
+ case "is_flying" -> isFlying(uuid);
+ case "is_god" -> isGod(uuid);
+ case "is_infinite_stamina" -> isInfiniteStamina(uuid);
+ case "is_vanished" -> isVanished(uuid);
+ case "playtime" -> playtime(uuid);
+ case "playtime_formatted" -> playtimeFormatted(uuid);
+ case "session_time" -> sessionTime(uuid);
+ case "session_time_formatted" -> sessionTimeFormatted(uuid);
+ case "first_join" -> firstJoin(uuid);
+ case "first_join_formatted" -> firstJoinFormatted(uuid);
+ case "last_join" -> lastJoin(uuid);
+ case "last_join_formatted" -> lastJoinFormatted(uuid);
+
+ // Warps / Spawns / TPA
+ case "warp_count" -> warpCount();
+ case "warp_accessible_count" -> warpAccessibleCount(uuid);
+ case "spawn_count" -> spawnCount();
+ case "is_accepting_tpa" -> isAcceptingTpa(uuid);
+ case "tpa_pending_count" -> tpaPendingCount(uuid);
+
+ default -> null; // Unknown placeholder - preserve original text
+ };
+ }
+
+ // ========== Homes ==========
+
+ private String homeCount(@NotNull UUID uuid) {
+ HomeManager hm = homeManager();
+ return hm != null ? String.valueOf(hm.getHomeCount(uuid)) : "0";
+ }
+
+ private String homeLimit(@NotNull UUID uuid) {
+ HomeManager hm = homeManager();
+ return hm != null ? String.valueOf(hm.getHomeLimit(uuid)) : "0";
+ }
+
+ private String homeDefault(@NotNull UUID uuid) {
+ HomeManager hm = homeManager();
+ if (hm == null) return "";
+ String def = hm.getDefaultHome(uuid);
+ return def != null ? def : "";
+ }
+
+ private String hasHomes(@NotNull UUID uuid) {
+ HomeManager hm = homeManager();
+ return hm != null ? String.valueOf(hm.getHomeCount(uuid) > 0) : "false";
+ }
+
+ // ========== Kits ==========
+
+ private String kitCount() {
+ KitManager km = kitManager();
+ return km != null ? String.valueOf(km.getAllKits().size()) : "0";
+ }
+
+ private String kitAvailableCount(@NotNull UUID uuid) {
+ KitManager km = kitManager();
+ return km != null ? String.valueOf(km.getAvailableKits(uuid).size()) : "0";
+ }
+
+ // ========== Moderation ==========
+
+ private String isBanned(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ return mm != null ? String.valueOf(mm.isBanned(uuid)) : "false";
+ }
+
+ private String isMuted(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ return mm != null ? String.valueOf(mm.isMuted(uuid)) : "false";
+ }
+
+ private String banReason(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ if (mm == null) return "";
+ Punishment ban = mm.getActiveBan(uuid);
+ if (ban == null) return "";
+ return ban.reason() != null ? ban.reason() : "";
+ }
+
+ private String muteReason(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ if (tm == null) return "";
+ PlayerData data = tm.getPlayerData(uuid);
+ if (data == null) return "";
+ Punishment mute = data.getActiveMute();
+ if (mute == null) return "";
+ return mute.reason() != null ? mute.reason() : "";
+ }
+
+ private String banExpires(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ if (mm == null) return "";
+ Punishment ban = mm.getActiveBan(uuid);
+ if (ban == null) return "";
+ if (ban.isPermanent()) return "permanent";
+ return formatInstant(ban.expiresAt());
+ }
+
+ private String muteExpires(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ if (tm == null) return "";
+ PlayerData data = tm.getPlayerData(uuid);
+ if (data == null) return "";
+ Punishment mute = data.getActiveMute();
+ if (mute == null) return "";
+ if (mute.isPermanent()) return "permanent";
+ return formatInstant(mute.expiresAt());
+ }
+
+ private String punishmentCount(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ if (mm == null) return "0";
+ return String.valueOf(mm.getHistory(uuid).size());
+ }
+
+ private String warnCount(@NotNull UUID uuid) {
+ ModerationManager mm = moderationManager();
+ if (mm == null) return "0";
+ List history = mm.getHistory(uuid);
+ long count = history.stream()
+ .filter(p -> p.type() == PunishmentType.WARN)
+ .count();
+ return String.valueOf(count);
+ }
+
+ // ========== Utility ==========
+
+ private String isAfk(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ return um != null ? String.valueOf(um.isAfk(uuid)) : "false";
+ }
+
+ private String isFlying(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ return um != null ? String.valueOf(um.isFlying(uuid)) : "false";
+ }
+
+ private String isGod(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ return um != null ? String.valueOf(um.isGod(uuid)) : "false";
+ }
+
+ private String isInfiniteStamina(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ return um != null ? String.valueOf(um.isInfiniteStamina(uuid)) : "false";
+ }
+
+ private String isVanished(@NotNull UUID uuid) {
+ VanishManager vm = vanishManager();
+ return vm != null ? String.valueOf(vm.isVanished(uuid)) : "false";
+ }
+
+ private String playtime(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ return um != null ? String.valueOf(um.getTotalPlaytimeMs(uuid)) : "0";
+ }
+
+ private String playtimeFormatted(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ if (um == null) return "0m";
+ return formatDuration(um.getTotalPlaytimeMs(uuid));
+ }
+
+ private String sessionTime(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ if (um == null) return "0";
+ Instant sessionStart = um.getSessionStart(uuid);
+ if (sessionStart == null) return "0";
+ return String.valueOf(Duration.between(sessionStart, Instant.now()).toMillis());
+ }
+
+ private String sessionTimeFormatted(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ if (um == null) return "0m";
+ Instant sessionStart = um.getSessionStart(uuid);
+ if (sessionStart == null) return "0m";
+ return formatDuration(Duration.between(sessionStart, Instant.now()).toMillis());
+ }
+
+ private String firstJoin(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ if (um == null) return "";
+ Instant instant = um.getFirstJoin(uuid);
+ return instant != null ? instant.toString() : "";
+ }
+
+ private String firstJoinFormatted(@NotNull UUID uuid) {
+ UtilityManager um = utilityManager();
+ if (um == null) return "";
+ Instant instant = um.getFirstJoin(uuid);
+ return instant != null ? formatInstant(instant) : "";
+ }
+
+ private String lastJoin(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ if (tm == null) return "";
+ PlayerData data = tm.getPlayerData(uuid);
+ if (data == null) return "";
+ return data.getLastJoin().toString();
+ }
+
+ private String lastJoinFormatted(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ if (tm == null) return "";
+ PlayerData data = tm.getPlayerData(uuid);
+ if (data == null) return "";
+ return formatInstant(data.getLastJoin());
+ }
+
+ // ========== Warps / Spawns / TPA ==========
+
+ private String warpCount() {
+ WarpManager wm = warpManager();
+ return wm != null ? String.valueOf(wm.getWarpCount()) : "0";
+ }
+
+ private String warpAccessibleCount(@NotNull UUID uuid) {
+ WarpManager wm = warpManager();
+ return wm != null ? String.valueOf(wm.getAccessibleWarps(uuid).size()) : "0";
+ }
+
+ private String spawnCount() {
+ SpawnManager sm = spawnManager();
+ return sm != null ? String.valueOf(sm.getSpawnCount()) : "0";
+ }
+
+ private String isAcceptingTpa(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ return tm != null ? String.valueOf(tm.isAcceptingRequests(uuid)) : "true";
+ }
+
+ private String tpaPendingCount(@NotNull UUID uuid) {
+ TpaManager tm = tpaManager();
+ return tm != null ? String.valueOf(tm.getIncomingRequests(uuid).size()) : "0";
+ }
+
+ // ========== Manager Accessors (null-safe) ==========
+
+ @Nullable
+ private HomeManager homeManager() {
+ HomesModule m = plugin.getHomesModule();
+ return (m != null && m.isEnabled()) ? m.getHomeManager() : null;
+ }
+
+ @Nullable
+ private KitManager kitManager() {
+ KitsModule m = plugin.getKitsModule();
+ return (m != null && m.isEnabled()) ? m.getKitManager() : null;
+ }
+
+ @Nullable
+ private ModerationManager moderationManager() {
+ ModerationModule m = plugin.getModerationModule();
+ return (m != null && m.isEnabled()) ? m.getModerationManager() : null;
+ }
+
+ @Nullable
+ private VanishManager vanishManager() {
+ ModerationModule m = plugin.getModerationModule();
+ return (m != null && m.isEnabled()) ? m.getVanishManager() : null;
+ }
+
+ @Nullable
+ private UtilityManager utilityManager() {
+ UtilityModule m = plugin.getUtilityModule();
+ return (m != null && m.isEnabled()) ? m.getUtilityManager() : null;
+ }
+
+ @Nullable
+ private WarpManager warpManager() {
+ WarpsModule m = plugin.getWarpsModule();
+ return (m != null && m.isEnabled()) ? m.getWarpManager() : null;
+ }
+
+ @Nullable
+ private SpawnManager spawnManager() {
+ SpawnsModule m = plugin.getSpawnsModule();
+ return (m != null && m.isEnabled()) ? m.getSpawnManager() : null;
+ }
+
+ @Nullable
+ private TpaManager tpaManager() {
+ TeleportModule m = plugin.getTeleportModule();
+ return (m != null && m.isEnabled()) ? m.getTpaManager() : null;
+ }
+
+ // ========== Helpers ==========
+
+ /**
+ * Formats a duration in milliseconds to a human-readable string.
+ * Examples: "3d 2h 15m", "5h 30m", "12m", "0m"
+ */
+ @NotNull
+ private static String formatDuration(long ms) {
+ if (ms <= 0) return "0m";
+
+ long totalMinutes = ms / 60_000;
+ long days = totalMinutes / 1440;
+ long hours = (totalMinutes % 1440) / 60;
+ long minutes = totalMinutes % 60;
+
+ StringBuilder sb = new StringBuilder();
+ if (days > 0) sb.append(days).append("d ");
+ if (hours > 0) sb.append(hours).append("h ");
+ sb.append(minutes).append("m");
+ return sb.toString().trim();
+ }
+
+ /**
+ * Formats an Instant to a human-readable date string (yyyy-MM-dd HH:mm).
+ */
+ @NotNull
+ private static String formatInstant(@Nullable Instant instant) {
+ if (instant == null) return "";
+ return DATE_FORMAT.format(instant);
+ }
+}
diff --git a/src/main/java/com/hyperessentials/integration/placeholder/WiFlowPlaceholderIntegration.java b/src/main/java/com/hyperessentials/integration/placeholder/WiFlowPlaceholderIntegration.java
new file mode 100644
index 0000000..e37e7d1
--- /dev/null
+++ b/src/main/java/com/hyperessentials/integration/placeholder/WiFlowPlaceholderIntegration.java
@@ -0,0 +1,106 @@
+package com.hyperessentials.integration.placeholder;
+
+import com.hyperessentials.HyperEssentials;
+import com.hyperessentials.util.ErrorHandler;
+import com.hyperessentials.util.Logger;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.Method;
+
+/**
+ * Soft dependency integration with WiFlow PlaceholderAPI.
+ *
+ * All WiFlow classes are accessed via reflection so HyperEssentials compiles
+ * and runs without the WiFlowPlaceholderAPI JAR present.
+ *
+ *
The actual expansion implementation lives in {@code WiFlowExpansion} which
+ * extends PlaceholderExpansion directly — that class is conditionally compiled
+ * only when the WiFlow API is available on the classpath.
+ */
+public final class WiFlowPlaceholderIntegration {
+
+ private static boolean available = false;
+
+ @Nullable
+ private static Object expansion;
+
+ // Cached reflection handles for shutdown
+ private static Method unregisterMethod;
+
+ private static Class> baseExpansionClass;
+
+ private WiFlowPlaceholderIntegration() {}
+
+ /**
+ * Initializes WiFlow PlaceholderAPI integration.
+ * Detects WiFlow at runtime and registers the expansion.
+ * Must be called after all managers are initialized.
+ *
+ * @param plugin the HyperEssentials instance
+ */
+ public static void init(HyperEssentials plugin) {
+ try {
+ Class.forName("com.wiflow.placeholderapi.WiFlowPlaceholderAPI");
+ } catch (ClassNotFoundException e) {
+ Logger.info("[Integration] WiFlow PlaceholderAPI not found - WiFlow placeholders disabled");
+ return;
+ }
+
+ try {
+ // WiFlowExpansion extends PlaceholderExpansion — only compiled when WiFlow is available.
+ // Load it reflectively to avoid a compile-time dependency in this class.
+ Class> expansionClass = Class.forName(
+ "com.hyperessentials.integration.placeholder.WiFlowExpansion");
+ expansion = expansionClass.getConstructor(HyperEssentials.class).newInstance(plugin);
+
+ // WiFlowPlaceholderAPI.registerExpansion(PlaceholderExpansion)
+ Class> apiClass = Class.forName("com.wiflow.placeholderapi.WiFlowPlaceholderAPI");
+ baseExpansionClass = Class.forName("com.wiflow.placeholderapi.expansion.PlaceholderExpansion");
+ Method registerMethod = apiClass.getMethod("registerExpansion", baseExpansionClass);
+ boolean registered = (boolean) registerMethod.invoke(null, expansion);
+
+ if (registered) {
+ // Cache unregister method for shutdown
+ unregisterMethod = apiClass.getMethod("unregisterExpansion", baseExpansionClass);
+ available = true;
+ Logger.info("[Integration] WiFlow PlaceholderAPI expansion registered ({essentials_*})");
+ } else {
+ Logger.warn("WiFlow PlaceholderAPI expansion registration failed");
+ expansion = null;
+ }
+ } catch (Exception e) {
+ ErrorHandler.report("Failed to register WiFlow PlaceholderAPI expansion", e);
+ expansion = null;
+ }
+ }
+
+ /**
+ * Shuts down WiFlow PlaceholderAPI integration.
+ * Unregisters the expansion.
+ */
+ public static void shutdown() {
+ if (expansion != null) {
+ try {
+ if (unregisterMethod != null) {
+ unregisterMethod.invoke(null, expansion);
+ }
+ } catch (Exception e) {
+ Logger.debug("Failed to unregister WiFlow PlaceholderAPI expansion: %s", e.getMessage());
+ }
+ expansion = null;
+ unregisterMethod = null;
+ baseExpansionClass = null;
+ }
+ available = false;
+ }
+
+ /**
+ * Checks if WiFlow PlaceholderAPI is available and the expansion is registered.
+ *
+ * @return true if available
+ */
+ public static boolean isAvailable() {
+ return available;
+ }
+}
diff --git a/src/main/java/com/hyperessentials/module/homes/HomeManager.java b/src/main/java/com/hyperessentials/module/homes/HomeManager.java
index a39c12b..7d634ff 100644
--- a/src/main/java/com/hyperessentials/module/homes/HomeManager.java
+++ b/src/main/java/com/hyperessentials/module/homes/HomeManager.java
@@ -1,6 +1,8 @@
package com.hyperessentials.module.homes;
import com.hyperessentials.Permissions;
+import com.hyperessentials.api.events.EventBus;
+import com.hyperessentials.api.events.homes.*;
import com.hyperessentials.command.util.CommandUtil;
import com.hyperessentials.config.ConfigManager;
import com.hyperessentials.config.modules.HomesConfig;
@@ -144,10 +146,16 @@ public boolean setHome(@NotNull UUID uuid, @NotNull Home home) {
if (isAtLimit(uuid)) return false;
}
+ if (EventBus.publishCancellable(new HomeSetPreEvent(uuid, home.name(), home.world(), home.x(), home.y(), home.z()))) {
+ return false;
+ }
+
homes.setHome(home);
savePlayer(uuid);
fireHomeChanged(uuid);
Logger.debug("[Homes] Set home '%s' for %s", home.name(), uuid);
+
+ EventBus.publish(new HomeSetEvent(uuid, home.name(), home.world(), home.x(), home.y(), home.z()));
return true;
}
@@ -155,6 +163,10 @@ public boolean deleteHome(@NotNull UUID uuid, @NotNull String name) {
PlayerHomes homes = cache.get(uuid);
if (homes == null) return false;
+ if (EventBus.publishCancellable(new HomeDeletePreEvent(uuid, name))) {
+ return false;
+ }
+
// Get shares before removing (for reverse index cleanup)
Set sharedWith = homes.getShares(name);
@@ -177,6 +189,8 @@ public boolean deleteHome(@NotNull UUID uuid, @NotNull String name) {
savePlayer(uuid);
fireHomeChanged(uuid);
Logger.debug("[Homes] Deleted home '%s' for %s", name, uuid);
+
+ EventBus.publish(new HomeDeleteEvent(uuid, name));
}
return removed;
}
@@ -228,10 +242,19 @@ public String getDefaultHome(@NotNull UUID uuid) {
public void setDefaultHome(@NotNull UUID uuid, @Nullable String name) {
PlayerHomes homes = cache.get(uuid);
if (homes == null) return;
+
+ if (name != null && EventBus.publishCancellable(new HomeDefaultSetPreEvent(uuid, name))) {
+ return;
+ }
+
homes.setDefaultHome(name);
savePlayer(uuid);
fireHomeChanged(uuid);
Logger.debug("[Homes] Set default home to '%s' for %s", name, uuid);
+
+ if (name != null) {
+ EventBus.publish(new HomeDefaultSetEvent(uuid, name));
+ }
}
@Nullable
@@ -275,6 +298,10 @@ public void shareHome(@NotNull UUID ownerUuid, @NotNull String homeName, @NotNul
PlayerHomes homes = cache.get(ownerUuid);
if (homes == null) return;
+ if (EventBus.publishCancellable(new HomeSharePreEvent(ownerUuid, homeName, targetUuid))) {
+ return;
+ }
+
homes.addShare(homeName, targetUuid);
reverseShareIndex.computeIfAbsent(targetUuid, k -> new ArrayList<>())
.add(new SharedHomeRef(ownerUuid, homeName.toLowerCase()));
@@ -283,12 +310,18 @@ public void shareHome(@NotNull UUID ownerUuid, @NotNull String homeName, @NotNul
fireHomeChanged(ownerUuid);
fireHomeChanged(targetUuid);
Logger.debug("[Homes] %s shared home '%s' with %s", ownerUuid, homeName, targetUuid);
+
+ EventBus.publish(new HomeShareEvent(ownerUuid, homeName, targetUuid));
}
public void unshareHome(@NotNull UUID ownerUuid, @NotNull String homeName, @NotNull UUID targetUuid) {
PlayerHomes homes = cache.get(ownerUuid);
if (homes == null) return;
+ if (EventBus.publishCancellable(new HomeUnsharePreEvent(ownerUuid, homeName, targetUuid))) {
+ return;
+ }
+
homes.removeShare(homeName, targetUuid);
List refs = reverseShareIndex.get(targetUuid);
@@ -301,6 +334,8 @@ public void unshareHome(@NotNull UUID ownerUuid, @NotNull String homeName, @NotN
fireHomeChanged(ownerUuid);
fireHomeChanged(targetUuid);
Logger.debug("[Homes] %s unshared home '%s' from %s", ownerUuid, homeName, targetUuid);
+
+ EventBus.publish(new HomeUnshareEvent(ownerUuid, homeName, targetUuid));
}
@NotNull
diff --git a/src/main/java/com/hyperessentials/module/kits/KitManager.java b/src/main/java/com/hyperessentials/module/kits/KitManager.java
index d031688..c385c7b 100644
--- a/src/main/java/com/hyperessentials/module/kits/KitManager.java
+++ b/src/main/java/com/hyperessentials/module/kits/KitManager.java
@@ -1,6 +1,8 @@
package com.hyperessentials.module.kits;
import com.hyperessentials.Permissions;
+import com.hyperessentials.api.events.EventBus;
+import com.hyperessentials.api.events.kits.*;
import com.hyperessentials.command.util.CommandUtil;
import com.hyperessentials.config.ConfigManager;
import com.hyperessentials.module.kits.data.Kit;
@@ -106,6 +108,10 @@ public ClaimResult claimKit(@NotNull UUID playerUuid, @NotNull PlayerRef playerR
@NotNull Kit kit) {
if (!canUseKit(playerUuid, kit)) return ClaimResult.NO_PERMISSION;
+ if (EventBus.publishCancellable(new KitClaimPreEvent(playerUuid, kit.name()))) {
+ return ClaimResult.NO_PERMISSION;
+ }
+
if (kit.oneTime()) {
String key = playerUuid + ":" + kit.name();
if (oneTimeClaims.contains(key)) return ClaimResult.ALREADY_CLAIMED;
@@ -135,6 +141,8 @@ public ClaimResult claimKit(@NotNull UUID playerUuid, @NotNull PlayerRef playerR
}
fireKitClaimed(playerUuid);
+
+ EventBus.publish(new KitClaimEvent(playerUuid, kit.name()));
return ClaimResult.SUCCESS;
}
@@ -209,9 +217,24 @@ public void updateKit(@NotNull Kit kit) {
}
public boolean deleteKit(@NotNull String name) {
+ return deleteKit(name, null);
+ }
+
+ public boolean deleteKit(@NotNull String name, @Nullable UUID actorUuid) {
+ Kit kit = kits.get(name.toLowerCase());
+ if (kit == null) return false;
+
+ if (actorUuid != null && EventBus.publishCancellable(new KitDeletePreEvent(kit.name(), actorUuid))) {
+ return false;
+ }
+
Kit removed = kits.remove(name.toLowerCase());
if (removed != null) {
storage.deleteKit(removed.uuid());
+
+ if (actorUuid != null) {
+ EventBus.publish(new KitDeleteEvent(removed.name(), actorUuid));
+ }
return true;
}
return false;
diff --git a/src/main/java/com/hyperessentials/module/moderation/ModerationManager.java b/src/main/java/com/hyperessentials/module/moderation/ModerationManager.java
index b46bb44..e0059fd 100644
--- a/src/main/java/com/hyperessentials/module/moderation/ModerationManager.java
+++ b/src/main/java/com/hyperessentials/module/moderation/ModerationManager.java
@@ -1,6 +1,8 @@
package com.hyperessentials.module.moderation;
import com.hyperessentials.Permissions;
+import com.hyperessentials.api.events.EventBus;
+import com.hyperessentials.api.events.moderation.*;
import com.hyperessentials.command.util.CommandUtil;
import com.hyperessentials.config.ConfigManager;
import com.hyperessentials.data.PlayerData;
@@ -194,10 +196,18 @@ public void kickPlayersWithIp(@NotNull String ip, @NotNull String reason) {
// === Ban Operations ===
- @NotNull
+ @Nullable
public Punishment ban(@NotNull UUID playerUuid, @NotNull String playerName,
@Nullable UUID issuerUuid, @NotNull String issuerName,
@Nullable String reason, @Nullable Long durationMs) {
+ String effectiveReason = reason != null ? reason
+ : ConfigManager.get().moderation().getDefaultBanReason();
+
+ if (issuerUuid != null && EventBus.publishCancellable(
+ new PlayerBanPreEvent(playerUuid, issuerUuid, effectiveReason, durationMs))) {
+ return null;
+ }
+
PlayerData data = getOrCreatePlayerData(playerUuid, playerName);
// Revoke any existing active ban first
@@ -206,9 +216,6 @@ public Punishment ban(@NotNull UUID playerUuid, @NotNull String playerName,
data.revokePunishment(existing.id(), issuerUuid, issuerName);
}
- String effectiveReason = reason != null ? reason
- : ConfigManager.get().moderation().getDefaultBanReason();
-
Instant expiresAt = durationMs != null ? Instant.now().plusMillis(durationMs) : null;
Punishment punishment = new Punishment(
@@ -231,6 +238,9 @@ public Punishment ban(@NotNull UUID playerUuid, @NotNull String playerName,
notifyStaff(Permissions.NOTIFY_BAN,
issuerName + " banned " + playerName + (punishment.isPermanent() ? " permanently" : " for " + DurationParser.formatHuman(durationMs)));
+ if (issuerUuid != null) {
+ EventBus.publish(new PlayerBanEvent(playerUuid, issuerUuid, effectiveReason, durationMs));
+ }
return punishment;
}
@@ -241,10 +251,18 @@ public boolean unban(@NotNull UUID playerUuid, @Nullable UUID revokerUuid, @NotN
Punishment ban = data.getActiveBan();
if (ban == null) return false;
+ if (revokerUuid != null && EventBus.publishCancellable(new PlayerUnbanPreEvent(playerUuid, revokerUuid))) {
+ return false;
+ }
+
data.revokePunishment(ban.id(), revokerUuid, revokerName);
savePlayerData(playerUuid, data);
notifyStaff(Permissions.NOTIFY_BAN, revokerName + " unbanned " + ban.playerName());
+
+ if (revokerUuid != null) {
+ EventBus.publish(new PlayerUnbanEvent(playerUuid, revokerUuid));
+ }
return true;
}
@@ -269,10 +287,18 @@ public Punishment getActiveBan(@NotNull UUID playerUuid) {
// === Mute Operations ===
- @NotNull
+ @Nullable
public Punishment mute(@NotNull UUID playerUuid, @NotNull String playerName,
@Nullable UUID issuerUuid, @NotNull String issuerName,
@Nullable String reason, @Nullable Long durationMs) {
+ String effectiveReason = reason != null ? reason
+ : ConfigManager.get().moderation().getDefaultMuteReason();
+
+ if (issuerUuid != null && EventBus.publishCancellable(
+ new PlayerMutePreEvent(playerUuid, issuerUuid, effectiveReason, durationMs))) {
+ return null;
+ }
+
PlayerData data = getOrCreatePlayerData(playerUuid, playerName);
Punishment existing = data.getActiveMute();
@@ -280,9 +306,6 @@ public Punishment mute(@NotNull UUID playerUuid, @NotNull String playerName,
data.revokePunishment(existing.id(), issuerUuid, issuerName);
}
- String effectiveReason = reason != null ? reason
- : ConfigManager.get().moderation().getDefaultMuteReason();
-
Instant expiresAt = durationMs != null ? Instant.now().plusMillis(durationMs) : null;
Punishment punishment = new Punishment(
@@ -308,6 +331,9 @@ public Punishment mute(@NotNull UUID playerUuid, @NotNull String playerName,
notifyStaff(Permissions.NOTIFY_MUTE,
issuerName + " muted " + playerName + (punishment.isPermanent() ? " permanently" : " for " + DurationParser.formatHuman(durationMs)));
+ if (issuerUuid != null) {
+ EventBus.publish(new PlayerMuteEvent(playerUuid, issuerUuid, effectiveReason, durationMs));
+ }
return punishment;
}
@@ -318,6 +344,10 @@ public boolean unmute(@NotNull UUID playerUuid, @Nullable UUID revokerUuid, @Not
Punishment mute = data.getActiveMute();
if (mute == null) return false;
+ if (revokerUuid != null && EventBus.publishCancellable(new PlayerUnmutePreEvent(playerUuid, revokerUuid))) {
+ return false;
+ }
+
data.revokePunishment(mute.id(), revokerUuid, revokerName);
savePlayerData(playerUuid, data);
@@ -327,6 +357,10 @@ public boolean unmute(@NotNull UUID playerUuid, @Nullable UUID revokerUuid, @Not
}
notifyStaff(Permissions.NOTIFY_MUTE, revokerName + " unmuted " + mute.playerName());
+
+ if (revokerUuid != null) {
+ EventBus.publish(new PlayerUnmuteEvent(playerUuid, revokerUuid));
+ }
return true;
}
@@ -345,13 +379,18 @@ public boolean isMuted(@NotNull UUID playerUuid) {
// === Kick Operations ===
- @NotNull
+ @Nullable
public Punishment kick(@NotNull UUID playerUuid, @NotNull String playerName,
@Nullable UUID issuerUuid, @NotNull String issuerName,
@Nullable String reason) {
String effectiveReason = reason != null ? reason
: ConfigManager.get().moderation().getDefaultKickReason();
+ if (issuerUuid != null && EventBus.publishCancellable(
+ new PlayerKickPreEvent(playerUuid, issuerUuid, effectiveReason))) {
+ return null;
+ }
+
Punishment punishment = new Punishment(
UUID.randomUUID(), PunishmentType.KICK, playerUuid, playerName,
issuerUuid, issuerName, effectiveReason, Instant.now(),
@@ -370,18 +409,26 @@ public Punishment kick(@NotNull UUID playerUuid, @NotNull String playerName,
}
notifyStaff(Permissions.NOTIFY_KICK, issuerName + " kicked " + playerName);
+ if (issuerUuid != null) {
+ EventBus.publish(new PlayerKickEvent(playerUuid, issuerUuid, effectiveReason));
+ }
return punishment;
}
// === Warn Operations ===
- @NotNull
+ @Nullable
public Punishment warn(@NotNull UUID playerUuid, @NotNull String playerName,
@Nullable UUID issuerUuid, @NotNull String issuerName,
@Nullable String reason) {
String effectiveReason = reason != null ? reason
: ConfigManager.get().moderation().getDefaultWarnReason();
+ if (issuerUuid != null && EventBus.publishCancellable(
+ new PlayerWarnPreEvent(playerUuid, issuerUuid, effectiveReason))) {
+ return null;
+ }
+
Punishment punishment = new Punishment(
UUID.randomUUID(), PunishmentType.WARN, playerUuid, playerName,
issuerUuid, issuerName, effectiveReason, Instant.now(),
@@ -415,6 +462,9 @@ public Punishment warn(@NotNull UUID playerUuid, @NotNull String playerName,
}
notifyStaff(Permissions.NOTIFY_WARN, issuerName + " warned " + playerName);
+ if (issuerUuid != null) {
+ EventBus.publish(new PlayerWarnEvent(playerUuid, issuerUuid, effectiveReason));
+ }
return punishment;
}
diff --git a/src/main/java/com/hyperessentials/module/spawns/SpawnManager.java b/src/main/java/com/hyperessentials/module/spawns/SpawnManager.java
index e74205d..6965add 100644
--- a/src/main/java/com/hyperessentials/module/spawns/SpawnManager.java
+++ b/src/main/java/com/hyperessentials/module/spawns/SpawnManager.java
@@ -1,5 +1,7 @@
package com.hyperessentials.module.spawns;
+import com.hyperessentials.api.events.EventBus;
+import com.hyperessentials.api.events.spawns.*;
import com.hyperessentials.data.Spawn;
import com.hyperessentials.storage.SpawnStorage;
import com.hyperessentials.util.ErrorHandler;
@@ -195,6 +197,15 @@ public int importWorldSpawns() {
* Sets/updates a spawn for a world.
*/
public void setSpawn(@NotNull Spawn spawn) {
+ if (spawn.createdBy() != null) {
+ try {
+ UUID actorUuid = UUID.fromString(spawn.createdBy());
+ if (EventBus.publishCancellable(new SpawnSetPreEvent(spawn.worldUuid(), actorUuid))) {
+ return;
+ }
+ } catch (IllegalArgumentException ignored) {}
+ }
+
if (spawn.isGlobal()) {
// Clear old global
for (Map.Entry entry : spawns.entrySet()) {
@@ -209,16 +220,42 @@ public void setSpawn(@NotNull Spawn spawn) {
spawns.put(spawn.worldUuid(), spawn);
storage.saveSpawn(spawn);
Logger.info("[Spawns] Spawn set for world '%s'%s", spawn.worldName(), spawn.isGlobal() ? " (global)" : "");
+
+ if (spawn.createdBy() != null) {
+ try {
+ UUID actorUuid = UUID.fromString(spawn.createdBy());
+ EventBus.publish(new SpawnSetEvent(spawn.worldUuid(), actorUuid));
+ } catch (IllegalArgumentException ignored) {}
+ }
}
/**
* Deletes the spawn for a world.
*/
public boolean deleteSpawn(@NotNull String worldUuid) {
+ Spawn spawn = spawns.get(worldUuid);
+ if (spawn == null) return false;
+
+ if (spawn.createdBy() != null) {
+ try {
+ UUID actorUuid = UUID.fromString(spawn.createdBy());
+ if (EventBus.publishCancellable(new SpawnDeletePreEvent(worldUuid, actorUuid))) {
+ return false;
+ }
+ } catch (IllegalArgumentException ignored) {}
+ }
+
Spawn removed = spawns.remove(worldUuid);
if (removed != null) {
storage.deleteSpawn(worldUuid);
Logger.info("[Spawns] Spawn deleted for world '%s'", removed.worldName());
+
+ if (removed.createdBy() != null) {
+ try {
+ UUID actorUuid = UUID.fromString(removed.createdBy());
+ EventBus.publish(new SpawnDeleteEvent(worldUuid, actorUuid));
+ } catch (IllegalArgumentException ignored) {}
+ }
return true;
}
return false;
diff --git a/src/main/java/com/hyperessentials/module/teleport/TpaManager.java b/src/main/java/com/hyperessentials/module/teleport/TpaManager.java
index caebb4c..31eea1a 100644
--- a/src/main/java/com/hyperessentials/module/teleport/TpaManager.java
+++ b/src/main/java/com/hyperessentials/module/teleport/TpaManager.java
@@ -1,6 +1,8 @@
package com.hyperessentials.module.teleport;
import com.hyperessentials.Permissions;
+import com.hyperessentials.api.events.EventBus;
+import com.hyperessentials.api.events.teleport.*;
import com.hyperessentials.config.modules.TeleportConfig;
import com.hyperessentials.data.PlayerData;
import com.hyperessentials.data.TeleportRequest;
@@ -127,6 +129,10 @@ public TeleportRequest createRequest(@NotNull UUID requesterUuid, @NotNull UUID
}
}
+ if (EventBus.publishCancellable(new TpaSendPreEvent(requesterUuid, targetUuid, type == TeleportRequest.Type.TPAHERE))) {
+ return null;
+ }
+
cancelOutgoingRequest(requesterUuid);
List targetIncoming = incomingRequests.computeIfAbsent(targetUuid, k -> new ArrayList<>());
@@ -158,6 +164,8 @@ public TeleportRequest createRequest(@NotNull UUID requesterUuid, @NotNull UUID
fireTpaChanged(requesterUuid);
fireTpaChanged(targetUuid);
Logger.debug("TPA request created: %s -> %s (%s)", requesterUuid, targetUuid, type);
+
+ EventBus.publish(new TpaSendEvent(requesterUuid, targetUuid, type == TeleportRequest.Type.TPAHERE));
return request;
}
@@ -209,18 +217,32 @@ public TeleportRequest getOutgoingRequest(@NotNull UUID uuid) {
return request;
}
- public void acceptRequest(@NotNull TeleportRequest request) {
+ public boolean acceptRequest(@NotNull TeleportRequest request) {
+ if (EventBus.publishCancellable(new TpaAcceptPreEvent(request.target(), request.requester()))) {
+ return false;
+ }
+
removeRequest(request);
fireTpaChanged(request.requester());
fireTpaChanged(request.target());
Logger.debug("TPA request accepted: %s -> %s", request.requester(), request.target());
+
+ EventBus.publish(new TpaAcceptEvent(request.target(), request.requester()));
+ return true;
}
- public void denyRequest(@NotNull TeleportRequest request) {
+ public boolean denyRequest(@NotNull TeleportRequest request) {
+ if (EventBus.publishCancellable(new TpaDenyPreEvent(request.target(), request.requester()))) {
+ return false;
+ }
+
removeRequest(request);
fireTpaChanged(request.requester());
fireTpaChanged(request.target());
Logger.debug("TPA request denied: %s -> %s", request.requester(), request.target());
+
+ EventBus.publish(new TpaDenyEvent(request.target(), request.requester()));
+ return true;
}
@Nullable
diff --git a/src/main/java/com/hyperessentials/module/utility/UtilityManager.java b/src/main/java/com/hyperessentials/module/utility/UtilityManager.java
index bde2c36..952a589 100644
--- a/src/main/java/com/hyperessentials/module/utility/UtilityManager.java
+++ b/src/main/java/com/hyperessentials/module/utility/UtilityManager.java
@@ -1,5 +1,7 @@
package com.hyperessentials.module.utility;
+import com.hyperessentials.api.events.EventBus;
+import com.hyperessentials.api.events.utility.*;
import com.hyperessentials.command.util.CommandUtil;
import com.hyperessentials.config.ConfigManager;
import com.hyperessentials.config.modules.UtilityConfig;
@@ -118,13 +120,20 @@ public boolean isFlying(@NotNull UUID uuid) {
}
public boolean toggleFly(@NotNull UUID uuid) {
- if (flyingPlayers.contains(uuid)) {
- flyingPlayers.remove(uuid);
- return false;
- } else {
+ boolean newState = !flyingPlayers.contains(uuid);
+
+ if (EventBus.publishCancellable(new FlyTogglePreEvent(uuid, newState))) {
+ return !newState; // return current state unchanged
+ }
+
+ if (newState) {
flyingPlayers.add(uuid);
- return true;
+ } else {
+ flyingPlayers.remove(uuid);
}
+
+ EventBus.publish(new FlyToggleEvent(uuid, newState));
+ return newState;
}
public void setFlying(@NotNull UUID uuid, boolean flying) {
@@ -139,13 +148,20 @@ public boolean isGod(@NotNull UUID uuid) {
}
public boolean toggleGod(@NotNull UUID uuid) {
- if (godPlayers.contains(uuid)) {
- godPlayers.remove(uuid);
- return false;
- } else {
+ boolean newState = !godPlayers.contains(uuid);
+
+ if (EventBus.publishCancellable(new GodTogglePreEvent(uuid, newState))) {
+ return !newState; // return current state unchanged
+ }
+
+ if (newState) {
godPlayers.add(uuid);
- return true;
+ } else {
+ godPlayers.remove(uuid);
}
+
+ EventBus.publish(new GodToggleEvent(uuid, newState));
+ return newState;
}
public void setGod(@NotNull UUID uuid, boolean god) {
@@ -160,13 +176,20 @@ public boolean isAfk(@NotNull UUID uuid) {
}
public boolean toggleAfk(@NotNull UUID uuid) {
- if (afkPlayers.contains(uuid)) {
- afkPlayers.remove(uuid);
- return false;
- } else {
+ boolean newState = !afkPlayers.contains(uuid);
+
+ if (EventBus.publishCancellable(new AfkTogglePreEvent(uuid, newState))) {
+ return !newState; // return current state unchanged
+ }
+
+ if (newState) {
afkPlayers.add(uuid);
- return true;
+ } else {
+ afkPlayers.remove(uuid);
}
+
+ EventBus.publish(new AfkToggleEvent(uuid, newState));
+ return newState;
}
/**
diff --git a/src/main/java/com/hyperessentials/module/warps/WarpManager.java b/src/main/java/com/hyperessentials/module/warps/WarpManager.java
index 0fa0e09..da1c8a8 100644
--- a/src/main/java/com/hyperessentials/module/warps/WarpManager.java
+++ b/src/main/java/com/hyperessentials/module/warps/WarpManager.java
@@ -1,5 +1,7 @@
package com.hyperessentials.module.warps;
+import com.hyperessentials.api.events.EventBus;
+import com.hyperessentials.api.events.warps.*;
import com.hyperessentials.data.Warp;
import com.hyperessentials.integration.PermissionManager;
import com.hyperessentials.storage.WarpStorage;
@@ -51,6 +53,15 @@ public CompletableFuture loadWarps() {
public boolean setWarp(@NotNull Warp warp) {
boolean isNew = !warps.containsKey(warp.name());
+ if (warp.createdBy() != null) {
+ try {
+ UUID actorUuid = UUID.fromString(warp.createdBy());
+ if (EventBus.publishCancellable(new WarpSetPreEvent(warp.name(), actorUuid))) {
+ return false;
+ }
+ } catch (IllegalArgumentException ignored) {}
+ }
+
// If updating an existing warp with a different name (shouldn't happen), remove the old
Warp existing = warps.get(warp.name());
if (existing != null && !existing.uuid().equals(warp.uuid())) {
@@ -62,6 +73,13 @@ public boolean setWarp(@NotNull Warp warp) {
storage.saveWarp(warp);
fireWarpChanged();
Logger.info("[Warps] Warp '%s' %s", warp.name(), isNew ? "created" : "updated");
+
+ if (warp.createdBy() != null) {
+ try {
+ UUID actorUuid = UUID.fromString(warp.createdBy());
+ EventBus.publish(new WarpSetEvent(warp.name(), actorUuid));
+ } catch (IllegalArgumentException ignored) {}
+ }
return isNew;
}
@@ -71,11 +89,30 @@ public Warp getWarp(@NotNull String name) {
}
public boolean deleteWarp(@NotNull String name) {
+ Warp warp = warps.get(name.toLowerCase());
+ if (warp == null) return false;
+
+ if (warp.createdBy() != null) {
+ try {
+ UUID actorUuid = UUID.fromString(warp.createdBy());
+ if (EventBus.publishCancellable(new WarpDeletePreEvent(warp.name(), actorUuid))) {
+ return false;
+ }
+ } catch (IllegalArgumentException ignored) {}
+ }
+
Warp removed = warps.remove(name.toLowerCase());
if (removed != null) {
storage.deleteWarp(removed.uuid());
fireWarpChanged();
Logger.info("[Warps] Warp '%s' deleted", name);
+
+ if (removed.createdBy() != null) {
+ try {
+ UUID actorUuid = UUID.fromString(removed.createdBy());
+ EventBus.publish(new WarpDeleteEvent(removed.name(), actorUuid));
+ } catch (IllegalArgumentException ignored) {}
+ }
return true;
}
return false;
diff --git a/src/main/resources/manifest.json b/src/main/resources/manifest.json
index 12bef33..5c70d52 100644
--- a/src/main/resources/manifest.json
+++ b/src/main/resources/manifest.json
@@ -7,5 +7,9 @@
"Main": "com.hyperessentials.platform.HyperEssentialsPlugin",
"ServerVersion": "${serverVersion}",
"SoftDependencies": ["HyperPerms", "VaultUnlocked", "LuckPerms", "HyperFactions"],
+ "OptionalDependencies": {
+ "HelpChat:PlaceholderAPI": ">= 1.0.2",
+ "com.wiflow:WiFlowPlaceholderAPI": ">= 1.0.3"
+ },
"IncludesAssetPack": true
}