diff --git a/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java b/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java
index 258cb92..6d8a87d 100644
--- a/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java
+++ b/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java
@@ -65,8 +65,8 @@ public final class PlaceholderAPI {
@NotNull
public static String setPlaceholders(final OfflinePlayer player,
@NotNull final String text) {
- return REPLACER_PERCENT.apply(text, player,
- PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion);
+ final LocalExpansionManager manager = PlaceholderAPIPlugin.getInstance().getLocalExpansionManager();
+ return REPLACER_PERCENT.apply(text, player, manager::getExpansion, manager::resolvePlaceholderValue);
}
/**
@@ -124,8 +124,8 @@ public final class PlaceholderAPI {
@NotNull
public static String setBracketPlaceholders(final OfflinePlayer player,
@NotNull final String text) {
- return REPLACER_BRACKET.apply(text, player,
- PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion);
+ final LocalExpansionManager manager = PlaceholderAPIPlugin.getInstance().getLocalExpansionManager();
+ return REPLACER_BRACKET.apply(text, player, manager::getExpansion, manager::resolvePlaceholderValue);
}
/**
diff --git a/src/main/java/me/clip/placeholderapi/PlaceholderHook.java b/src/main/java/me/clip/placeholderapi/PlaceholderHook.java
index 655553b..db7fed4 100644
--- a/src/main/java/me/clip/placeholderapi/PlaceholderHook.java
+++ b/src/main/java/me/clip/placeholderapi/PlaceholderHook.java
@@ -35,8 +35,40 @@ public abstract class PlaceholderHook {
return onPlaceholderRequest(null, params);
}
+ /**
+ * Override this method when a placeholder should return a non-string value for type handling.
+ *
The default delegates to {@link #onPlaceholderTypeHandledRequest(Player, String)} with an online
+ * {@link Player}, matching the behavior of {@link #onRequest(OfflinePlayer, String)}.
+ *
+ * @param player Player to parse the placeholder against
+ * @param params Placeholder parameters
+ * @return Object to resolve through type handlers, or null to leave the placeholder unchanged
+ */
+ @Nullable
+ public Object onTypeHandledRequest(final OfflinePlayer player, @NotNull final String params) {
+ if (player != null && player.isOnline()) {
+ return onPlaceholderTypeHandledRequest(player.getPlayer(), params);
+ }
+
+ return onPlaceholderTypeHandledRequest(null, params);
+ }
+
@Nullable
public String onPlaceholderRequest(final Player player, @NotNull final String params) {
return null;
}
+
+ /**
+ * Override this method when a player placeholder should return a non-string value for type handling.
+ *
The default returns null so legacy string placeholders are only resolved through
+ * {@link #onRequest(OfflinePlayer, String)}.
+ *
+ * @param player Online player to parse the placeholder against
+ * @param params Placeholder parameters
+ * @return Object to resolve through type handlers, or null to leave the placeholder unchanged
+ */
+ @Nullable
+ public Object onPlaceholderTypeHandledRequest(final Player player, @NotNull final String params) {
+ return null;
+ }
}
diff --git a/src/main/java/me/clip/placeholderapi/expansion/PlaceholderContext.java b/src/main/java/me/clip/placeholderapi/expansion/PlaceholderContext.java
new file mode 100644
index 0000000..45d413e
--- /dev/null
+++ b/src/main/java/me/clip/placeholderapi/expansion/PlaceholderContext.java
@@ -0,0 +1,33 @@
+package me.clip.placeholderapi.expansion;
+
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class PlaceholderContext {
+ @Nullable private final Player player;
+ @NotNull private final T type;
+ @NotNull private final String args;
+
+ public PlaceholderContext(@Nullable final Player player, @NotNull final T type,
+ @NotNull final String args) {
+ this.player = player;
+ this.type = type;
+ this.args = args;
+ }
+
+ @Nullable
+ public Player player() {
+ return player;
+ }
+
+ @NotNull
+ public T type() {
+ return type;
+ }
+
+ @NotNull
+ public String args() {
+ return args;
+ }
+}
diff --git a/src/main/java/me/clip/placeholderapi/expansion/PlaceholderExpansion.java b/src/main/java/me/clip/placeholderapi/expansion/PlaceholderExpansion.java
index 0d90b89..d0110cd 100644
--- a/src/main/java/me/clip/placeholderapi/expansion/PlaceholderExpansion.java
+++ b/src/main/java/me/clip/placeholderapi/expansion/PlaceholderExpansion.java
@@ -20,6 +20,7 @@
package me.clip.placeholderapi.expansion;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
@@ -140,6 +141,10 @@ public abstract class PlaceholderExpansion extends PlaceholderHook {
|| Bukkit.getPluginManager().getPlugin(getRequiredPlugin()) != null;
}
+ public Collection> provideTypeHandlers() {
+ return Collections.emptyList();
+ }
+
/**
* Attempt to register this PlaceholderExpansion
*
diff --git a/src/main/java/me/clip/placeholderapi/expansion/TypeHandler.java b/src/main/java/me/clip/placeholderapi/expansion/TypeHandler.java
new file mode 100644
index 0000000..5cc4d0d
--- /dev/null
+++ b/src/main/java/me/clip/placeholderapi/expansion/TypeHandler.java
@@ -0,0 +1,15 @@
+package me.clip.placeholderapi.expansion;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public interface TypeHandler {
+ Class typeClass();
+
+ default boolean includeDerivatives() {
+ return true;
+ }
+
+ @Nullable
+ String onRequest(@NotNull final PlaceholderContext context);
+}
diff --git a/src/main/java/me/clip/placeholderapi/expansion/manager/LocalExpansionManager.java b/src/main/java/me/clip/placeholderapi/expansion/manager/LocalExpansionManager.java
index 6f4d0f8..5fa74a1 100644
--- a/src/main/java/me/clip/placeholderapi/expansion/manager/LocalExpansionManager.java
+++ b/src/main/java/me/clip/placeholderapi/expansion/manager/LocalExpansionManager.java
@@ -44,19 +44,16 @@ import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.events.ExpansionRegisterEvent;
import me.clip.placeholderapi.events.ExpansionUnregisterEvent;
import me.clip.placeholderapi.events.ExpansionsLoadedEvent;
-import me.clip.placeholderapi.expansion.Cacheable;
-import me.clip.placeholderapi.expansion.Cleanable;
-import me.clip.placeholderapi.expansion.Configurable;
-import me.clip.placeholderapi.expansion.PlaceholderExpansion;
-import me.clip.placeholderapi.expansion.Taskable;
-import me.clip.placeholderapi.expansion.VersionSpecific;
+import me.clip.placeholderapi.expansion.*;
import me.clip.placeholderapi.expansion.cloud.CloudExpansion;
import me.clip.placeholderapi.util.FileUtil;
import me.clip.placeholderapi.util.Futures;
import me.clip.placeholderapi.util.Msg;
import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
@@ -86,6 +83,7 @@ public final class LocalExpansionManager implements Listener {
@NotNull
private final Map expansions = new ConcurrentHashMap<>();
+ private final Map, TypeHandler> typeHandlers = new ConcurrentHashMap<>();
private final ReentrantLock expansionsLock = new ReentrantLock();
@@ -208,6 +206,64 @@ public final class LocalExpansionManager implements Listener {
return Optional.empty();
}
+ @Nullable
+ public String resolvePlaceholderValue(@NotNull final PlaceholderExpansion expansion, @Nullable final OfflinePlayer player,
+ @NotNull final String args) {
+ Object value = expansion.onRequest(player, args);
+
+ if (value != null) {
+ return (String) value;
+ }
+
+ value = expansion.onTypeHandledRequest(player, args);
+
+ if (value instanceof String) {
+ return (String) value;
+ }
+
+ if (value != null) {
+ return resolveTypeHandler(value, player, args);
+ }
+
+ return null;
+ }
+
+ @Nullable
+ public String resolveTypeHandler(@NotNull final Object value, @Nullable final OfflinePlayer player,
+ @NotNull final String args) {
+ TypeHandler handler = null;
+
+ for (final TypeHandler> typeHandler : typeHandlers.values()) {
+ if (typeHandler.typeClass() == value.getClass()) {
+ handler = typeHandler;
+ }
+ }
+
+ if (handler == null) {
+ for (final TypeHandler> typeHandler : typeHandlers.values()) {
+ if (typeHandler.includeDerivatives() && typeHandler.typeClass().isAssignableFrom(value.getClass())) {
+ handler = typeHandler;
+ }
+ }
+ }
+
+ if (handler == null) {
+ Msg.warn("Failed to resolve type handler for placeholder %s and type %s", args, value.getClass().getSimpleName());
+ return null;
+ }
+
+ return handler.onRequest(new PlaceholderContext<>(player(player), value, args));
+ }
+
+ @Nullable
+ private Player player(@Nullable final OfflinePlayer player) {
+ if (player == null || !player.isOnline()) {
+ return null;
+ }
+
+ return player.getPlayer();
+ }
+
/**
* Attempt to register a {@link PlaceholderExpansion}
*
@@ -228,6 +284,16 @@ public final class LocalExpansionManager implements Listener {
return false;
}
+ for (final TypeHandler> typeHandler : expansion.provideTypeHandlers()) {
+ if (typeHandler.typeClass().isAssignableFrom(String.class)) {
+ Msg.warn("Failed to load type handler %s from expansion %s as there is already a type handler for this type.",
+ expansion.getIdentifier(), typeHandler.typeClass().getSimpleName());
+ continue;
+ }
+
+ typeHandlers.putIfAbsent(typeHandler.typeClass(), typeHandler);
+ }
+
if (expansion instanceof Configurable) {
Map defaults = ((Configurable) expansion).getDefaults();
String pre = "expansions." + identifier + ".";
diff --git a/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java b/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java
index 5edc145..8530e2b 100644
--- a/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java
+++ b/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java
@@ -47,15 +47,17 @@ public final class CharsReplacer implements Replacer {
* @param text The raw text containing potential placeholders to be replaced.
* @param player The {@link OfflinePlayer} to contextually parse the placeholders against.
* May be {@code null} if no player context is available.
- * @param lookup A function that maps a lowercase identifier string to a registered
- * {@link PlaceholderExpansion}.
- * @return A string with all valid placeholders replaced by their respective values.
- * Returns the original text if no placeholders are found.
- */
- @NotNull
- @Override
- public String apply(@NotNull final String text, @Nullable final OfflinePlayer player,
- @NotNull final Function lookup) {
+ * @param lookup A function that maps a lowercase identifier string to a registered
+ * {@link PlaceholderExpansion}.
+ * @param resolver Resolves a registered {@link PlaceholderExpansion} to its replacement value.
+ * @return A string with all valid placeholders replaced by their respective values.
+ * Returns the original text if no placeholders are found.
+ */
+ @NotNull
+ @Override
+ public String apply(@NotNull final String text, @Nullable final OfflinePlayer player,
+ @NotNull final Function lookup,
+ @NotNull final PlaceholderResolver resolver) {
final char head = closure.head;
int startPlaceholder = text.indexOf(head);
@@ -122,10 +124,10 @@ public final class CharsReplacer implements Replacer {
final PlaceholderExpansion expansion = lookup.apply(identifier.toLowerCase(Locale.ROOT));
String replacement = null;
-
- if (expansion != null) {
- replacement = expansion.onRequest(player, parameters);
- }
+
+ if (expansion != null) {
+ replacement = resolver.resolve(expansion, player, parameters);
+ }
if (replacement != null) {
builder.append(replacement);
@@ -148,4 +150,4 @@ public final class CharsReplacer implements Replacer {
return builder.toString();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/me/clip/placeholderapi/replacer/Replacer.java b/src/main/java/me/clip/placeholderapi/replacer/Replacer.java
index 417df30..510637c 100644
--- a/src/main/java/me/clip/placeholderapi/replacer/Replacer.java
+++ b/src/main/java/me/clip/placeholderapi/replacer/Replacer.java
@@ -30,9 +30,25 @@ import org.jetbrains.annotations.Nullable;
public interface Replacer {
@NotNull
- String apply(@NotNull final String text, @Nullable final OfflinePlayer player,
- @NotNull final Function lookup);
+ default String apply(@NotNull final String text, @Nullable final OfflinePlayer player,
+ @NotNull final Function lookup) {
+ return apply(text, player, lookup, PlaceholderExpansion::onRequest);
+ }
+ @NotNull
+ String apply(@NotNull final String text, @Nullable final OfflinePlayer player,
+ @NotNull final Function lookup,
+ @NotNull final PlaceholderResolver resolver);
+
+ @FunctionalInterface
+ interface PlaceholderResolver {
+
+ @Nullable
+ String resolve(@NotNull final PlaceholderExpansion expansion,
+ @Nullable final OfflinePlayer player,
+ @NotNull final String params);
+
+ }
enum Closure {
BRACKET('{', '}'),