Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
67ca66c
Improve RTP world override handling and Yaml map loading
WhiteProject1 Nov 11, 2025
c240dfd
Add online and player validation to teleport requests
WhiteProject1 Nov 19, 2025
bda7f7f
Remove debug logging from TeleportationModule
WhiteProject1 Nov 19, 2025
1fb0769
Improve thread safety and error handling in core modules
WhiteProject1 Nov 19, 2025
79fdec9
Delay teleport on first join for chunk loading
WhiteProject1 Nov 20, 2025
b71e6ba
Enhance vote module and fix PlayerListener issues
WhiteProject1 Nov 20, 2025
ce78fa1
Add player trading system with commands and GUI (NOT TESTED)
WhiteProject1 Nov 20, 2025
dd98b65
Add support for custom model data in trade items
WhiteProject1 Nov 20, 2025
5ff92e2
Add soft expiration and async refresh to ExpiringCache
WhiteProject1 Nov 20, 2025
5440676
Add item caching and refactor item meta updates
WhiteProject1 Nov 20, 2025
6f5408d
Add cross-server teleportation support (Not Tested)
WhiteProject1 Nov 25, 2025
eb03851
Fix UnsupportedOperationException on Folia/Canvas region threading
WhiteProject1 Mar 13, 2026
41e0279
Add missing cross-server teleport message constants
WhiteProject1 Mar 13, 2026
290a4af
Upgrade zMenu compatibility to 1.1.1.0
WhiteProject1 Mar 13, 2026
16b5a78
Fix NullPointerException in HologramLoader for unsupported NMS versions
WhiteProject1 Mar 13, 2026
d238bf3
Delay spawn teleport on join to avoid chunk loader race condition
WhiteProject1 Mar 13, 2026
3cd6db0
Add NMS V1_21_11 support for holograms and waypoints
WhiteProject1 Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions API/src/main/java/fr/maxlego08/essentials/api/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,19 @@ public interface Configuration extends ConfigurationFile {
* @return a map of options and their default values
*/
Map<Option, Boolean> getDefaultOptionValues();

/**
* Retrieves the server name used for cross-server communication.
* This is used with BungeeCord/Velocity for cross-server teleportation.
*
* @return the server name, or "default" if not configured
*/
String getServerName();

/**
* Checks if cross-server teleportation is enabled.
*
* @return true if cross-server teleportation is enabled
*/
boolean isCrossServerTeleportEnabled();
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,52 @@ public ExpiringCache(long expiryDurationMillis) {
* Retrieves the value associated with the specified key from the cache. If the key is not found,
* or the entry has expired, the provided Loader is used to load the value, store it in the cache,
* and then return it.
* <p>
* Supports soft expiration: if the entry is soft-expired but not hard-expired, it returns the old value
* and refreshes the cache asynchronously.
*
* @param key the key whose associated value is to be returned
* @param loader the loader used to generate the value if it is not present or expired in the cache
* @return the value associated with the specified key or the newly loaded value if the key
* was not found in the cache or if the entry expired
*/
public V get(K key, Loader<V> loader) {
return cache.compute(key, (k, v) -> {
long currentTime = System.currentTimeMillis();
if (v == null || v.expiryTime < currentTime) {
V newValue = loader.load();
return new CacheEntry<>(newValue, currentTime + expiryDurationMillis);
}
return v;
}).value;
CacheEntry<V> entry = cache.get(key);
long currentTime = System.currentTimeMillis();

// 1. Hard Expiration or Not Present -> Synchronous Load
if (entry == null || entry.expiryTime < currentTime) {
return cache.compute(key, (k, v) -> {
// Double-check inside lock
long now = System.currentTimeMillis();
if (v == null || v.expiryTime < now) {
V newValue = loader.load();
long expiry = now + expiryDurationMillis;
long softExpiry = now + (long) (expiryDurationMillis * 0.8); // Refresh at 80% of ttl
return new CacheEntry<>(newValue, expiry, softExpiry);
}
return v;
}).value;
}

// 2. Soft Expiration -> Return Old Value & Asynchronous Refresh
if (entry.softExpiryTime < currentTime && entry.isUpdating.compareAndSet(false, true)) {
java.util.concurrent.CompletableFuture.runAsync(() -> {
try {
V newValue = loader.load();
long now = System.currentTimeMillis();
long expiry = now + expiryDurationMillis;
long softExpiry = now + (long) (expiryDurationMillis * 0.8);
cache.put(key, new CacheEntry<>(newValue, expiry, softExpiry));
} catch (Exception e) {
e.printStackTrace();
// Reset flag on failure so we can try again later
entry.isUpdating.set(false);
}
});
}

return entry.value;
}

/**
Expand Down Expand Up @@ -75,6 +106,16 @@ public interface Loader<V> {
/**
* A simple cache entry that holds the value and its expiry time.
*/
private record CacheEntry<V>(V value, long expiryTime) {
private static class CacheEntry<V> {
final V value;
final long expiryTime;
final long softExpiryTime;
final java.util.concurrent.atomic.AtomicBoolean isUpdating = new java.util.concurrent.atomic.AtomicBoolean(false);

CacheEntry(V value, long expiryTime, long softExpiryTime) {
this.value = value;
this.expiryTime = expiryTime;
this.softExpiryTime = softExpiryTime;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ public V get(K key, Loader<V> loader) {
}


/**
* Clears all entries from the cache.
*/
public void clear() {
cache.clear();
}

/**
* Functional interface for loading values into the cache. Implementations of this interface
* provide a method to load a value, typically involving an operation such as fetching data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public enum Permission {
ESSENTIALS_TPA_ACCEPT,
ESSENTIALS_TPA_DENY,
ESSENTIALS_TPA_CANCEL,
ESSENTIALS_TRADE_USE,
ESSENTIALS_TRADE_ACCEPT,
ESSENTIALS_TRADE_DENY,
ESSENTIALS_BYPASS_COOLDOWN("Allows not to have a cooldown on all commands"),
ESSENTIALS_MORE,
ESSENTIALS_TP_WORLD,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package fr.maxlego08.essentials.api.event.events.trade;

import fr.maxlego08.essentials.api.event.EssentialsEvent;
import org.bukkit.entity.Player;

public class TradeCancelEvent extends EssentialsEvent {

private final Player player1;
private final Player player2;
private final Player canceller;

public TradeCancelEvent(Player player1, Player player2, Player canceller) {
this.player1 = player1;
this.player2 = player2;
this.canceller = canceller;
}

public Player getPlayer1() {
return player1;
}

public Player getPlayer2() {
return player2;
}

public Player getCanceller() {
return canceller;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package fr.maxlego08.essentials.api.event.events.trade;

import fr.maxlego08.essentials.api.event.EssentialsEvent;
import org.bukkit.entity.Player;

public class TradeCompleteEvent extends EssentialsEvent {

private final Player player1;
private final Player player2;

public TradeCompleteEvent(Player player1, Player player2) {
this.player1 = player1;
this.player2 = player2;
}

public Player getPlayer1() {
return player1;
}

public Player getPlayer2() {
return player2;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package fr.maxlego08.essentials.api.event.events.trade;

import fr.maxlego08.essentials.api.event.CancellableEssentialsEvent;
import org.bukkit.entity.Player;

public class TradeStartEvent extends CancellableEssentialsEvent {

private final Player player1;
private final Player player2;

public TradeStartEvent(Player player1, Player player2) {
this.player1 = player1;
this.player2 = player2;
}

public Player getPlayer1() {
return player1;
}

public Player getPlayer2() {
return player2;
}
}

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package fr.maxlego08.essentials.api.mailbox;

import fr.maxlego08.essentials.api.dto.MailBoxDTO;
import fr.maxlego08.menu.zcore.utils.nms.ItemStackUtils;
import fr.maxlego08.menu.common.utils.nms.ItemStackUtils;
import org.bukkit.inventory.ItemStack;

import java.util.Date;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public enum Message {
COMMAND_NO_ARG("<error>Impossible to find the command with its arguments."),
COMMAND_RESTRICTED("<error>You cannot use this command here."),
COMMAND_SYNTAXE_HELP("&f%syntax% &7» &7%description%"),
DESCRIPTION_TRADE("Manage trade requests"),

COMMAND_RELOAD("<success>You have just reloaded the configuration files."),
COMMAND_RELOAD_MODULE("<success>You have just reloaded the configuration files of the module &f%module%<success>."),
Expand Down Expand Up @@ -418,6 +419,9 @@ public enum Message {

TELEPORT_DAMAGE("<error>You must not take damage during teleportation."),
TELEPORT_ERROR_LOCATION("<error>Unable to teleport you safely."),
TELEPORT_CROSS_SERVER_NOT_SUPPORTED("<error>Cross-server teleportation is not supported without Redis."),
TELEPORT_CROSS_SERVER_CONNECTING("<success>Connecting to server <white>%server%<success>..."),
TELEPORT_CROSS_SERVER_PLAYER_NOT_FOUND("<error>Player <white>%player% <error>was not found on any server."),

// RTP Queue System Messages
TELEPORT_ALREADY_IN_QUEUE("<error>You are already in the teleportation queue!"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import fr.maxlego08.essentials.api.commands.Permission;
import fr.maxlego08.essentials.api.messages.Message;
import fr.maxlego08.essentials.api.teleport.CrossServerLocation;
import fr.maxlego08.essentials.api.teleport.TeleportType;
import fr.maxlego08.essentials.api.user.Option;
import fr.maxlego08.essentials.api.user.PrivateMessage;
import fr.maxlego08.essentials.api.user.User;
Expand Down Expand Up @@ -142,4 +144,47 @@ public interface EssentialsServer {
* @param message The message to send.
*/
void pub(Player player, String message);

/**
* Sends a player to another server via BungeeCord/Velocity.
*
* @param player The player to send.
* @param serverName The target server name.
*/
void sendToServer(Player player, String serverName);

/**
* Requests a cross-server teleport. The player will be sent to the target server
* and then teleported to the destination location.
*
* @param player The player to teleport.
* @param teleportType The type of teleportation.
* @param destination The destination location including server name.
*/
void crossServerTeleport(Player player, TeleportType teleportType, CrossServerLocation destination);

/**
* Requests a cross-server teleport to another player.
*
* @param player The player requesting the teleport.
* @param teleportType The type of teleportation (TPA or TPA_HERE).
* @param targetPlayerName The name of the target player.
* @param targetServer The server where the target player is.
*/
void crossServerTeleportToPlayer(Player player, TeleportType teleportType, String targetPlayerName, String targetServer);

/**
* Gets the current server name from BungeeCord configuration.
*
* @return The server name or null if not configured.
*/
String getServerName();

/**
* Finds which server a player is on.
*
* @param playerName The player name to find.
* @return The server name or null if not found.
*/
String findPlayerServer(String playerName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package fr.maxlego08.essentials.api.server.messages;

import fr.maxlego08.essentials.api.teleport.CrossServerLocation;
import fr.maxlego08.essentials.api.teleport.TeleportType;

import java.util.UUID;

/**
* Redis message for cross-server teleportation requests.
*/
public class CrossServerTeleport {

private final UUID playerUuid;
private final String playerName;
private final TeleportType teleportType;
private final CrossServerLocation destination;
private final String targetName; // For TPA - the target player name
private final long timestamp;

public CrossServerTeleport(UUID playerUuid, String playerName, TeleportType teleportType, CrossServerLocation destination) {
this(playerUuid, playerName, teleportType, destination, null);
}

public CrossServerTeleport(UUID playerUuid, String playerName, TeleportType teleportType, CrossServerLocation destination, String targetName) {
this.playerUuid = playerUuid;
this.playerName = playerName;
this.teleportType = teleportType;
this.destination = destination;
this.targetName = targetName;
this.timestamp = System.currentTimeMillis();
}

public UUID getPlayerUuid() {
return playerUuid;
}

public String getPlayerName() {
return playerName;
}

public TeleportType getTeleportType() {
return teleportType;
}

public CrossServerLocation getDestination() {
return destination;
}

public String getTargetName() {
return targetName;
}

public long getTimestamp() {
return timestamp;
}

/**
* Check if this request is still valid (not expired).
* Default timeout is 30 seconds.
*/
public boolean isValid() {
return System.currentTimeMillis() - timestamp < 30000;
}
}

Loading