mirror of
https://github.com/PlaceholderAPI/PlaceholderAPI
synced 2026-02-05 11:57:14 +01:00
Use PlayerRef instead of Player, finalise expansion/replace api
This commit is contained in:
@@ -14,12 +14,16 @@ description = "An awesome placeholder provider!"
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
|
||||
maven {
|
||||
url = uri("https://repo.codemc.io/repository/hytale/")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.yaml:snakeyaml:2.5")
|
||||
|
||||
compileOnly(files("libs/HytaleServer.jar"))
|
||||
compileOnly("com.hypixel.hytale:Server:2026.01.17-4b0f30090")
|
||||
compileOnlyApi("org.jetbrains:annotations:23.0.0")
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,12 @@ import java.util.stream.Collectors;
|
||||
import at.helpch.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import at.helpch.placeholderapi.expansion.Relational;
|
||||
import at.helpch.placeholderapi.replacer.CharsReplacer;
|
||||
import at.helpch.placeholderapi.replacer.MessageReplacer;
|
||||
import at.helpch.placeholderapi.replacer.Replacer;
|
||||
import at.helpch.placeholderapi.replacer.Replacer.Closure;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class PlaceholderAPI {
|
||||
@@ -62,48 +64,26 @@ public final class PlaceholderAPI {
|
||||
* @return String containing all translated placeholders
|
||||
*/
|
||||
@NotNull
|
||||
public static String setPlaceholders(final Player player,
|
||||
public static String setPlaceholders(final PlayerRef player,
|
||||
@NotNull final String text) {
|
||||
return REPLACER_PERCENT.apply(text, player,
|
||||
PlaceholderAPIPlugin.instance().localExpansionManager()::getExpansion);
|
||||
}
|
||||
|
||||
public static Message setPlaceholders(final Player player,
|
||||
/**
|
||||
* Translates all placeholders into their corresponding values.
|
||||
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
|
||||
*
|
||||
* @param player Player to parse the placeholders against
|
||||
* @param original Message to set the placeholder values in
|
||||
* @return Message containing all translated placeholders
|
||||
*/
|
||||
@NotNull
|
||||
public static Message setPlaceholders(final PlayerRef player,
|
||||
@NotNull final Message original) {
|
||||
String replaced = setPlaceholders(player, original.getFormattedMessage().rawText);
|
||||
String link = original.getFormattedMessage().link == null ? null : PlaceholderAPI.setPlaceholders(player, original.getFormattedMessage().link);
|
||||
|
||||
List<Message> newChildren = original.getChildren().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(child -> setPlaceholders(player, child))
|
||||
.toList();
|
||||
|
||||
Message message = Message.raw(replaced)
|
||||
.color(original.getColor());
|
||||
|
||||
if (link != null) {
|
||||
message = message.link(link);
|
||||
}
|
||||
|
||||
int bold = original.getFormattedMessage().bold.getValue();
|
||||
if (bold != 0) {
|
||||
message = message.bold(bold != 1);
|
||||
}
|
||||
|
||||
int italic = original.getFormattedMessage().italic.getValue();
|
||||
if (italic != 0) {
|
||||
message = message.italic(italic != 1);
|
||||
}
|
||||
|
||||
return message.insertAll(newChildren);
|
||||
return MessageReplacer.replace(original, str -> setPlaceholders(player, str));
|
||||
}
|
||||
|
||||
// @NotNull
|
||||
// public static Message setPlaceholders(final Player player,
|
||||
// @NotNull final Message message) {
|
||||
//
|
||||
// }
|
||||
|
||||
/**
|
||||
* Translates all placeholders into their corresponding values.
|
||||
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
|
||||
@@ -113,11 +93,25 @@ public final class PlaceholderAPI {
|
||||
* @return String containing all translated placeholders
|
||||
*/
|
||||
@NotNull
|
||||
public static List<String> setPlaceholders(final Player player,
|
||||
public static List<String> setPlaceholders(final PlayerRef player,
|
||||
@NotNull final List<String> text) {
|
||||
return text.stream().map(line -> setPlaceholders(player, line)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Translates all placeholders into their corresponding values.
|
||||
// * <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
|
||||
// *
|
||||
// * @param player Player to parse the placeholders against
|
||||
// * @param original List of Messages to set the placeholder values in
|
||||
// * @return List of Message containing all translated placeholders
|
||||
// */
|
||||
// @NotNull
|
||||
// public static List<Message> setPlaceholders(final Player player,
|
||||
// @NotNull final List<Message> original) {
|
||||
// return original.stream().map(msg -> setPlaceholders(player, msg)).collect(Collectors.toList());
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Translates all placeholders into their corresponding values.
|
||||
// * <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
|
||||
@@ -153,12 +147,18 @@ public final class PlaceholderAPI {
|
||||
* @return String containing all translated placeholders
|
||||
*/
|
||||
@NotNull
|
||||
public static String setBracketPlaceholders(final Player player,
|
||||
public static String setBracketPlaceholders(final PlayerRef player,
|
||||
@NotNull final String text) {
|
||||
return REPLACER_BRACKET.apply(text, player,
|
||||
PlaceholderAPIPlugin.instance().localExpansionManager()::getExpansion);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Message setBracketPlaceholders(final PlayerRef player,
|
||||
@NotNull final Message original) {
|
||||
return MessageReplacer.replace(original, str -> setBracketPlaceholders(player, str));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates all placeholders into their corresponding values.
|
||||
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
|
||||
@@ -168,7 +168,7 @@ public final class PlaceholderAPI {
|
||||
* @return String containing all translated placeholders
|
||||
*/
|
||||
@NotNull
|
||||
public static List<@NotNull String> setBracketPlaceholders(final Player player,
|
||||
public static List<@NotNull String> setBracketPlaceholders(final PlayerRef player,
|
||||
@NotNull final List<@NotNull String> text) {
|
||||
return text.stream().map(line -> setBracketPlaceholders(player, line))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -25,19 +25,7 @@ import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public abstract class PlaceholderHook {
|
||||
public interface PlaceholderHook {
|
||||
@Nullable
|
||||
public String onRequest(final Player player, @NotNull final String params) {
|
||||
// Player player2;
|
||||
// if (player != null && player.isOnline()) {
|
||||
// return onPlaceholderRequest(player.getPlayer(), params);
|
||||
// }
|
||||
|
||||
return onPlaceholderRequest(player, params);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String onPlaceholderRequest(final Player player, @NotNull final String params) {
|
||||
return null;
|
||||
}
|
||||
String onPlaceholderRequest(final PlayerRef player, @NotNull final String params);
|
||||
}
|
||||
|
||||
@@ -20,17 +20,16 @@
|
||||
|
||||
package at.helpch.placeholderapi.expansion;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import at.helpch.placeholderapi.PlaceholderAPIPlugin;
|
||||
import at.helpch.placeholderapi.PlaceholderHook;
|
||||
import com.hypixel.hytale.server.core.HytaleServer;
|
||||
import com.hypixel.hytale.server.core.plugin.PluginBase;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -40,7 +39,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
* class extending this one is located under the {@code PlaceholderAPI/expansions}
|
||||
* directory or when the {@link #register()} method is called by said class.
|
||||
*/
|
||||
public abstract class PlaceholderExpansion extends PlaceholderHook {
|
||||
public abstract class PlaceholderExpansion implements PlaceholderHook {
|
||||
private static final Pattern PATH_DELIMITER = Pattern.compile(".");
|
||||
|
||||
/**
|
||||
* The type is {@link Type#INTERNAL} by default.
|
||||
@@ -224,113 +224,157 @@ public abstract class PlaceholderExpansion extends PlaceholderHook {
|
||||
// return section == null ? null : section.getConfigurationSection(path);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Gets the Object relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
// * by the expansion or the provided Default Object, when the default ConfigurationSection is null
|
||||
// *
|
||||
// * @param path The path to get the Object from. This is relative to the default section
|
||||
// * @param def The default Object to return when the ConfigurationSection returns null
|
||||
// * @return Object from the provided path or the default one provided
|
||||
// */
|
||||
// @Nullable
|
||||
// @Contract("_, !null -> !null")
|
||||
// public final Object get(@NotNull final String path, final Object def) {
|
||||
// final ConfigurationSection section = getConfigSection();
|
||||
// return section == null ? def : section.get(path, def);
|
||||
// }
|
||||
/**
|
||||
* Gets the Object relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
* by the expansion or the provided Default Object, when the default ConfigurationSection is null
|
||||
*
|
||||
* @param path The path to get the Object from. This is relative to the default section
|
||||
* @param def The default Object to return when the ConfigurationSection returns null
|
||||
* @return Object from the provided path or the default one provided
|
||||
*/
|
||||
@Nullable
|
||||
@Contract("_, !null -> !null")
|
||||
public final Object get(@NotNull final String path, final Object def) {
|
||||
return get(new ArrayDeque<>(Arrays.asList(PATH_DELIMITER.split(path))), def, getExpansionConfig());
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Gets the int relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
// * by the expansion or the provided Default int, when the default ConfigurationSection is null
|
||||
// *
|
||||
// * @param path The path to get the int from. This is relative to the default section
|
||||
// * @param def The default int to return when the ConfigurationSection returns null
|
||||
// * @return int from the provided path or the default one provided
|
||||
// */
|
||||
// public final int getInt(@NotNull final String path, final int def) {
|
||||
// final ConfigurationSection section = getConfigSection();
|
||||
// return section == null ? def : section.getInt(path, def);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Gets the long relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
// * by the expansion or the provided Default long, when the default ConfigurationSection is null
|
||||
// *
|
||||
// * @param path The path to get the long from. This is relative to the default section
|
||||
// * @param def The default long to return when the ConfigurationSection returns null
|
||||
// * @return long from the provided path or the default one provided
|
||||
// */
|
||||
// public final long getLong(@NotNull final String path, final long def) {
|
||||
// final ConfigurationSection section = getConfigSection();
|
||||
// return section == null ? def : section.getLong(path, def);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Gets the double relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
// * by the expansion or the provided Default double, when the default ConfigurationSection is null
|
||||
// *
|
||||
// * @param path The path to get the double from. This is relative to the default section
|
||||
// * @param def The default double to return when the ConfigurationSection returns null
|
||||
// * @return double from the provided path or the default one provided
|
||||
// */
|
||||
// public final double getDouble(@NotNull final String path, final double def) {
|
||||
// final ConfigurationSection section = getConfigSection();
|
||||
// return section == null ? def : section.getDouble(path, def);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Gets the String relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
// * by the expansion or the provided Default String, when the default ConfigurationSection is null
|
||||
// *
|
||||
// * @param path The path to get the String from. This is relative to the default section
|
||||
// * @param def The default String to return when the ConfigurationSection returns null. Can be null
|
||||
// * @return String from the provided path or the default one provided
|
||||
// */
|
||||
// @Nullable
|
||||
// @Contract("_, !null -> !null")
|
||||
// public final String getString(@NotNull final String path, @Nullable final String def) {
|
||||
// final ConfigurationSection section = getConfigSection();
|
||||
// return section == null ? def : section.getString(path, def);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Gets a String List relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
// * by the expansion or an empty List, when the default ConfigurationSection is null
|
||||
// *
|
||||
// * @param path The path to get the String list from. This is relative to the default section
|
||||
// * @return String list from the provided path or an empty list
|
||||
// */
|
||||
// @NotNull
|
||||
// public final List<String> getStringList(@NotNull final String path) {
|
||||
// final ConfigurationSection section = getConfigSection();
|
||||
// return section == null ? Collections.emptyList() : section.getStringList(path);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Gets the boolean relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
// * by the expansion or the default boolean, when the default ConfigurationSection is null
|
||||
// *
|
||||
// * @param path The path to get the boolean from. This is relative to the default section
|
||||
// * @param def The default boolean to return when the ConfigurationSection is null
|
||||
// * @return boolean from the provided path or the default one provided
|
||||
// */
|
||||
// public final boolean getBoolean(@NotNull final String path, final boolean def) {
|
||||
// final ConfigurationSection section = getConfigSection();
|
||||
// return section == null ? def : section.getBoolean(path, def);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Whether the {@link #getConfigSection() default ConfigurationSection} contains the provided path
|
||||
// * or not. This will return {@code false} when either the default section is null, or doesn't
|
||||
// * contain the provided path
|
||||
// *
|
||||
// * @param path The path to check
|
||||
// * @return true when the default ConfigurationSection is not null and contains the path, false otherwise
|
||||
// */
|
||||
// public final boolean configurationContains(@NotNull final String path) {
|
||||
// final ConfigurationSection section = getConfigSection();
|
||||
// return section != null && section.contains(path);
|
||||
// }
|
||||
private Object get(@NotNull final Queue<String> path, final Object def, @NotNull final Map<String, Object> map) {
|
||||
if (path.size() == 1) {
|
||||
return map.getOrDefault(path.poll(), def);
|
||||
}
|
||||
|
||||
Object obj = map.get(path.poll());
|
||||
|
||||
if (!(obj instanceof Map<?, ?>)) {
|
||||
return def;
|
||||
}
|
||||
|
||||
return get(path, def, (Map<String, Object>) obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the int relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
* by the expansion or the provided Default int, when the default ConfigurationSection is null
|
||||
*
|
||||
* @param path The path to get the int from. This is relative to the default section
|
||||
* @param def The default int to return when the ConfigurationSection returns null
|
||||
* @return int from the provided path or the default one provided
|
||||
*/
|
||||
public final int getInt(@NotNull final String path, final int def) {
|
||||
final Object obj = get(path, def);
|
||||
|
||||
if (!(obj instanceof Integer)) {
|
||||
return def;
|
||||
}
|
||||
|
||||
return (Integer) obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the long relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
* by the expansion or the provided Default long, when the default ConfigurationSection is null
|
||||
*
|
||||
* @param path The path to get the long from. This is relative to the default section
|
||||
* @param def The default long to return when the ConfigurationSection returns null
|
||||
* @return long from the provided path or the default one provided
|
||||
*/
|
||||
public final long getLong(@NotNull final String path, final long def) {
|
||||
final Object obj = get(path, def);
|
||||
|
||||
if (!(obj instanceof Long) ) {
|
||||
return def;
|
||||
}
|
||||
|
||||
return (Long) obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the double relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
* by the expansion or the provided Default double, when the default ConfigurationSection is null
|
||||
*
|
||||
* @param path The path to get the double from. This is relative to the default section
|
||||
* @param def The default double to return when the ConfigurationSection returns null
|
||||
* @return double from the provided path or the default one provided
|
||||
*/
|
||||
public final double getDouble(@NotNull final String path, final double def) {
|
||||
final Object obj = get(path, def);
|
||||
|
||||
if (!(obj instanceof Double) ) {
|
||||
return def;
|
||||
}
|
||||
|
||||
return (Double) obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the String relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
* by the expansion or the provided Default String, when the default ConfigurationSection is null
|
||||
*
|
||||
* @param path The path to get the String from. This is relative to the default section
|
||||
* @param def The default String to return when the ConfigurationSection returns null. Can be null
|
||||
* @return String from the provided path or the default one provided
|
||||
*/
|
||||
@Nullable
|
||||
@Contract("_, !null -> !null")
|
||||
public final String getString(@NotNull final String path, @Nullable final String def) {
|
||||
final Object obj = get(path, def);
|
||||
|
||||
if (!(obj instanceof String)) {
|
||||
return def;
|
||||
}
|
||||
|
||||
return (String) obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a String List relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
* by the expansion or an empty List, when the default ConfigurationSection is null
|
||||
*
|
||||
* @param path The path to get the String list from. This is relative to the default section
|
||||
* @return String list from the provided path or an empty list
|
||||
*/
|
||||
@NotNull
|
||||
public final List<String> getStringList(@NotNull final String path) {
|
||||
final Object obj = get(path, new ArrayList<>());
|
||||
|
||||
if (!(obj instanceof List<?>)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return (List<String>) obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the boolean relative to the {@link #getConfigSection() default ConfigurationSection} set
|
||||
* by the expansion or the default boolean, when the default ConfigurationSection is null
|
||||
*
|
||||
* @param path The path to get the boolean from. This is relative to the default section
|
||||
* @param def The default boolean to return when the ConfigurationSection is null
|
||||
* @return boolean from the provided path or the default one provided
|
||||
*/
|
||||
public final boolean getBoolean(@NotNull final String path, final boolean def) {
|
||||
final Object obj = get(path, def);
|
||||
|
||||
if (!(obj instanceof Boolean)) {
|
||||
return def;
|
||||
}
|
||||
|
||||
return (Boolean) obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the {@link #getConfigSection() default ConfigurationSection} contains the provided path
|
||||
* or not. This will return {@code false} when either the default section is null, or doesn't
|
||||
* contain the provided path
|
||||
*
|
||||
* @param path The path to check
|
||||
* @return true when the default ConfigurationSection is not null and contains the path, false otherwise
|
||||
*/
|
||||
public final boolean configurationContains(@NotNull final String path) {
|
||||
final Object obj = get(path, null);
|
||||
|
||||
return obj == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the provided message with the provided Level in the console.
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.function.Function;
|
||||
|
||||
import at.helpch.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -40,7 +41,7 @@ public final class CharsReplacer implements Replacer {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String apply(@NotNull final String text, @Nullable final Player player,
|
||||
public String apply(@NotNull final String text, @Nullable final PlayerRef player,
|
||||
@NotNull final Function<String, @Nullable PlaceholderExpansion> lookup) {
|
||||
final char[] chars = text.toCharArray();
|
||||
final StringBuilder builder = new StringBuilder(text.length());
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package at.helpch.placeholderapi.replacer;
|
||||
|
||||
import at.helpch.placeholderapi.PlaceholderAPI;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class MessageReplacer {
|
||||
@NotNull
|
||||
public static Message replace(@NotNull final Message original, @NotNull final Function<String, String> setPlaceholders) {
|
||||
if (original.getFormattedMessage().rawText == null) {
|
||||
return original;
|
||||
}
|
||||
|
||||
String replaced = setPlaceholders.apply(original.getFormattedMessage().rawText);
|
||||
String link = original.getFormattedMessage().link == null ? null : setPlaceholders.apply(original.getFormattedMessage().link);
|
||||
|
||||
List<Message> newChildren = original.getChildren().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(child -> replace(child, setPlaceholders))
|
||||
.toList();
|
||||
|
||||
Message message = Message.raw(replaced);
|
||||
|
||||
if (original.getColor() != null) {
|
||||
message = message.color(original.getColor());
|
||||
}
|
||||
|
||||
if (link != null) {
|
||||
message = message.link(link);
|
||||
}
|
||||
|
||||
int bold = original.getFormattedMessage().bold.getValue();
|
||||
if (bold != 0) {
|
||||
message = message.bold(bold != 1);
|
||||
}
|
||||
|
||||
int italic = original.getFormattedMessage().italic.getValue();
|
||||
if (italic != 0) {
|
||||
message = message.italic(italic != 1);
|
||||
}
|
||||
|
||||
return message.insertAll(newChildren);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
public interface Replacer {
|
||||
|
||||
@NotNull
|
||||
String apply(@NotNull final String text, @Nullable final Player player,
|
||||
String apply(@NotNull final String text, @Nullable final PlayerRef player,
|
||||
@NotNull final Function<String, @Nullable PlaceholderExpansion> lookup);
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user