Add PlaceholderAPI (Bukkit) support

This commit is contained in:
Vankka 2022-04-26 17:00:09 +03:00
parent 5ccf8f7cb7
commit 3f030dfada
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
11 changed files with 239 additions and 46 deletions

View File

@ -52,10 +52,11 @@ public class PlaceholderLookupEvent implements Event, Processable {
return contexts;
}
public Optional<Object> getContext(Class<?> type) {
@SuppressWarnings("unchecked")
public <T> Optional<T> getContext(Class<T> type) {
for (Object o : contexts) {
if (type.isAssignableFrom(o.getClass())) {
return Optional.of(o);
return Optional.of((T) o);
}
}
return Optional.empty();

View File

@ -25,6 +25,7 @@ package com.discordsrv.api.placeholder;
import com.discordsrv.api.placeholder.mapper.PlaceholderResultMapper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import java.util.regex.Matcher;
@ -52,6 +53,9 @@ public interface PlaceholderService {
PlaceholderLookupResult lookupPlaceholder(@NotNull String placeholder, @NotNull Object... context);
Object getResult(@NotNull Matcher matcher, @NotNull Set<Object> context);
@NotNull
CharSequence getResultAsPlain(@NotNull Matcher matcher, @NotNull Set<Object> context);
@NotNull
CharSequence getResultAsPlain(@Nullable Object result);
}

View File

@ -23,7 +23,6 @@
package com.discordsrv.api.player;
import com.discordsrv.api.placeholder.annotation.Placeholder;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@ -37,7 +36,6 @@ public interface DiscordSRVPlayer {
* The username of the player.
* @return the player's username
*/
@Placeholder("player_name")
@NotNull
String username();
@ -45,7 +43,6 @@ public interface DiscordSRVPlayer {
* The {@link UUID} of the player.
* @return the player's unique id
*/
@Placeholder("player_uuid")
@NotNull
UUID uniqueId();

View File

@ -44,6 +44,7 @@ allprojects {
}
filter {
includeGroup 'net.milkbowl.vault'
includeGroup 'me.clip'
}
}
}
@ -79,6 +80,7 @@ dependencies {
// Integrations
compileOnly(libs.vaultapi)
compileOnly(libs.placeholderapi.bukkit)
}
processResources {

View File

@ -225,6 +225,7 @@ public class BukkitDiscordSRV extends ServerDiscordSRV<DiscordSRVBukkitBootstrap
// Integrations
registerIntegration("com.discordsrv.bukkit.integration.VaultIntegration");
registerIntegration("com.discordsrv.bukkit.integration.PlaceholderAPIIntegration");
super.enable();

View File

@ -37,6 +37,7 @@ public class DiscordSRVBukkitBootstrap extends BukkitBootstrap implements IBoots
private final Logger logger;
private final LifecycleManager lifecycleManager;
private BukkitDiscordSRV discordSRV;
private final List<Runnable> mainThreadTasksForDisable = new ArrayList<>();
public DiscordSRVBukkitBootstrap(JarInJarClassLoader classLoader, JavaPlugin plugin) throws IOException {
// Don't change these parameters
@ -73,6 +74,11 @@ public class DiscordSRVBukkitBootstrap extends BukkitBootstrap implements IBoots
@Override
public void onDisable() {
lifecycleManager.disable(discordSRV);
// Run tasks on the main thread (scheduler cannot be used when disabling)
for (Runnable runnable : mainThreadTasksForDisable) {
runnable.run();
}
}
@Override
@ -94,4 +100,8 @@ public class DiscordSRVBukkitBootstrap extends BukkitBootstrap implements IBoots
public Path dataDirectory() {
return getPlugin().getDataFolder().toPath();
}
public List<Runnable> mainThreadTasksForDisable() {
return mainThreadTasksForDisable;
}
}

View File

@ -0,0 +1,155 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.bukkit.integration;
import com.discordsrv.api.event.bus.Subscribe;
import com.discordsrv.api.event.events.placeholder.PlaceholderLookupEvent;
import com.discordsrv.api.placeholder.PlaceholderLookupResult;
import com.discordsrv.api.player.DiscordSRVPlayer;
import com.discordsrv.api.profile.IProfile;
import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.common.module.type.PluginIntegration;
import com.discordsrv.common.player.IOfflinePlayer;
import me.clip.placeholderapi.PlaceholderAPI;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public class PlaceholderAPIIntegration extends PluginIntegration<BukkitDiscordSRV> {
private static final String OPTIONAL_PREFIX = "placeholderapi_";
private Expansion expansion;
public PlaceholderAPIIntegration(BukkitDiscordSRV discordSRV) {
super(discordSRV);
}
@Override
public boolean isEnabled() {
try {
Class.forName("me.clip.placeholderapi.PlaceholderAPI");
} catch (ClassNotFoundException ignored) {
return false;
}
return super.isEnabled();
}
@Override
public void enable() {
expansion = new Expansion();
discordSRV.scheduler().runOnMainThread(() -> expansion.register());
}
@Override
public void disable() {
if (expansion != null) {
discordSRV.scheduler().runOnMainThread(() -> expansion.unregister());
}
}
@Subscribe
public void onPlaceholderLookup(PlaceholderLookupEvent event) {
String placeholder = event.getPlaceholder();
if (placeholder.startsWith(OPTIONAL_PREFIX)) {
placeholder = placeholder.substring(OPTIONAL_PREFIX.length());
}
placeholder = "%" + placeholder + "%";
Player player = event.getContext(DiscordSRVPlayer.class)
.map(p -> discordSRV.server().getPlayer(p.uniqueId()))
.orElse(null);
if (player != null) {
setResult(event, placeholder, PlaceholderAPI.setPlaceholders(player, placeholder));
return;
}
UUID uuid = event.getContext(IProfile.class)
.flatMap(IProfile::playerUUID)
.orElseGet(() -> event.getContext(IOfflinePlayer.class).map(IOfflinePlayer::uniqueId).orElse(null));
OfflinePlayer offlinePlayer = uuid != null ? discordSRV.server().getOfflinePlayer(uuid) : null;
setResult(event, placeholder, PlaceholderAPI.setPlaceholders(offlinePlayer, placeholder));
}
private void setResult(PlaceholderLookupEvent event, String placeholder, String result) {
if (result.equals(placeholder)) {
// Didn't resolve
return;
}
event.process(PlaceholderLookupResult.success(result));
}
public class Expansion extends PlaceholderExpansion {
@Override
public @NotNull String getIdentifier() {
return "discordsrv";
}
@Override
public @NotNull String getAuthor() {
return "DiscordSRV";
}
@Override
public @NotNull String getVersion() {
return discordSRV.version();
}
@Override
public @NotNull String getName() {
return "DiscordSRV";
}
@Override
public boolean persist() {
return true;
}
@Override
public @Nullable String onPlaceholderRequest(Player player, @NotNull String params) {
return onRequest(player, params);
}
@Override
public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) {
Set<Object> context;
if (player != null) {
context = new HashSet<>(2);
discordSRV.profileManager().getProfile(player.getUniqueId()).ifPresent(context::add);
if (player instanceof Player) {
context.add(discordSRV.playerProvider().player((Player) player));
} else {
context.add(discordSRV.playerProvider().offlinePlayer(player));
}
} else {
context = Collections.emptySet();
}
String placeholder = "%" + params + "%";
return discordSRV.placeholderService().replacePlaceholders(placeholder, context);
}
}
}

View File

@ -19,8 +19,13 @@
package com.discordsrv.bukkit.scheduler;
import com.discordsrv.bukkit.BukkitDiscordSRV;
import com.discordsrv.common.server.scheduler.ServerScheduler;
import com.discordsrv.bukkit.DiscordSRVBukkitBootstrap;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.scheduler.StandardScheduler;
import com.discordsrv.common.server.scheduler.ServerScheduler;
import org.bukkit.plugin.Plugin;
import java.util.function.BiConsumer;
public class BukkitScheduler extends StandardScheduler implements ServerScheduler {
@ -31,18 +36,28 @@ public class BukkitScheduler extends StandardScheduler implements ServerSchedule
this.discordSRV = discordSRV;
}
private void checkDisable(Runnable task, BiConsumer<org.bukkit.scheduler.BukkitScheduler, Plugin> runNormal) {
// Can't run tasks when disabling, so we'll push those to the bootstrap to run after disable
if (!discordSRV.plugin().isEnabled() && discordSRV.status() == DiscordSRV.Status.SHUTTING_DOWN) {
((DiscordSRVBukkitBootstrap) discordSRV.bootstrap()).mainThreadTasksForDisable().add(task);
return;
}
runNormal.accept(discordSRV.server().getScheduler(), discordSRV.plugin());
}
@Override
public void runOnMainThread(Runnable task) {
discordSRV.server().getScheduler().runTask(discordSRV.plugin(), task);
checkDisable(task, (scheduler, plugin) -> scheduler.runTask(plugin, task));
}
@Override
public void runOnMainThreadLaterInTicks(Runnable task, int ticks) {
discordSRV.server().getScheduler().runTaskLater(discordSRV.plugin(), task, ticks);
checkDisable(task, (scheduler, plugin) -> scheduler.runTaskLater(plugin, task, ticks));
}
@Override
public void runOnMainThreadAtFixedRateInTicks(Runnable task, int initialTicks, int rateTicks) {
discordSRV.server().getScheduler().runTaskTimer(discordSRV.plugin(), task, initialTicks, rateTicks);
checkDisable(task, (scheduler, plugin) -> scheduler.runTaskTimer(plugin, task, initialTicks, rateTicks));
}
}

View File

@ -175,12 +175,13 @@ public class PlaceholderServiceImpl implements PlaceholderService {
}
@Override
public CharSequence getResultAsPlain(@NotNull Matcher matcher, @NotNull Set<Object> context) {
public @NotNull CharSequence getResultAsPlain(@NotNull Matcher matcher, @NotNull Set<Object> context) {
Object result = getResult(matcher, context);
return getResultAsPlain(result);
}
private CharSequence getResultAsPlain(Object result) {
@Override
public @NotNull CharSequence getResultAsPlain(@Nullable Object result) {
if (result == null) {
return "";
} else if (result instanceof CharSequence) {
@ -214,10 +215,6 @@ public class PlaceholderServiceImpl implements PlaceholderService {
Object representation = getResultRepresentation(results, placeholder, matcher);
CharSequence output = getResultAsPlain(representation);
if (output == null) {
output = String.valueOf(representation);
}
return Pattern.compile(
matcher.group(1) + placeholder + matcher.group(3),
Pattern.LITERAL
@ -274,48 +271,57 @@ public class PlaceholderServiceImpl implements PlaceholderService {
private static class ClassProviderLoader implements CacheLoader<Class<?>, Set<PlaceholderProvider>> {
private Set<Class<?>> getAll(Class<?> clazz) {
Set<Class<?>> classes = new LinkedHashSet<>();
classes.add(clazz);
for (Class<?> anInterface : clazz.getInterfaces()) {
classes.addAll(getAll(anInterface));
}
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
classes.addAll(getAll(superClass));
}
return classes;
}
@Override
public @Nullable Set<PlaceholderProvider> load(@NonNull Class<?> key) {
Set<PlaceholderProvider> providers = new HashSet<>();
Class<?> currentClass = key;
do {
List<Class<?>> classes = new ArrayList<>(Arrays.asList(currentClass.getInterfaces()));
classes.add(currentClass);
Set<Class<?>> classes = getAll(key);
for (Class<?> clazz : classes) {
for (Method method : clazz.getMethods()) {
Placeholder annotation = method.getAnnotation(Placeholder.class);
if (annotation == null) {
continue;
}
for (Class<?> clazz : classes) {
for (Method method : clazz.getMethods()) {
Placeholder annotation = method.getAnnotation(Placeholder.class);
if (annotation == null) {
continue;
}
boolean startsWith = !annotation.relookup().isEmpty();
if (!startsWith) {
for (Parameter parameter : method.getParameters()) {
if (parameter.getAnnotation(PlaceholderRemainder.class) != null) {
startsWith = true;
break;
}
boolean startsWith = !annotation.relookup().isEmpty();
if (!startsWith) {
for (Parameter parameter : method.getParameters()) {
if (parameter.getAnnotation(PlaceholderRemainder.class) != null) {
startsWith = true;
break;
}
}
boolean isStatic = Modifier.isStatic(method.getModifiers());
providers.add(new AnnotationPlaceholderProvider(annotation, isStatic ? null : clazz, startsWith, method));
}
for (Field field : clazz.getFields()) {
Placeholder annotation = field.getAnnotation(Placeholder.class);
if (annotation == null) {
continue;
}
boolean isStatic = Modifier.isStatic(field.getModifiers());
providers.add(new AnnotationPlaceholderProvider(annotation, isStatic ? null : clazz, !annotation.relookup().isEmpty(), field));
}
boolean isStatic = Modifier.isStatic(method.getModifiers());
providers.add(new AnnotationPlaceholderProvider(annotation, isStatic ? null : clazz, startsWith, method));
}
for (Field field : clazz.getFields()) {
Placeholder annotation = field.getAnnotation(Placeholder.class);
if (annotation == null) {
continue;
}
currentClass = currentClass.getSuperclass();
} while (currentClass != null);
boolean isStatic = Modifier.isStatic(field.getModifiers());
providers.add(new AnnotationPlaceholderProvider(annotation, isStatic ? null : clazz, !annotation.relookup().isEmpty(), field));
}
}
return providers;
}

View File

@ -45,6 +45,7 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende
}
@NotNull
@Placeholder("player_name")
String username();
@Override

View File

@ -94,6 +94,7 @@ dependencyResolutionManagement {
// Integrations
library('luckperms', 'net.luckperms', 'api').version('5.4')
library('vaultapi', 'net.milkbowl.vault', 'VaultAPI').version('1.7')
library('placeholderapi-bukkit', 'me.clip', 'placeholderapi').version('2.11.1')
// Logging
library('slf4j-api', 'org.slf4j', 'slf4j-api').version('1.7.36')