diff --git a/api/src/main/java/com/discordsrv/api/event/events/placeholder/PlaceholderLookupEvent.java b/api/src/main/java/com/discordsrv/api/event/events/placeholder/PlaceholderLookupEvent.java index 616e22f1..f1f3ceb2 100644 --- a/api/src/main/java/com/discordsrv/api/event/events/placeholder/PlaceholderLookupEvent.java +++ b/api/src/main/java/com/discordsrv/api/event/events/placeholder/PlaceholderLookupEvent.java @@ -52,10 +52,11 @@ public class PlaceholderLookupEvent implements Event, Processable { return contexts; } - public Optional getContext(Class type) { + @SuppressWarnings("unchecked") + public Optional getContext(Class type) { for (Object o : contexts) { if (type.isAssignableFrom(o.getClass())) { - return Optional.of(o); + return Optional.of((T) o); } } return Optional.empty(); diff --git a/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java index bd3511b6..3bb72b46 100644 --- a/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java +++ b/api/src/main/java/com/discordsrv/api/placeholder/PlaceholderService.java @@ -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 context); + @NotNull CharSequence getResultAsPlain(@NotNull Matcher matcher, @NotNull Set context); + @NotNull + CharSequence getResultAsPlain(@Nullable Object result); } diff --git a/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java b/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java index f5abc039..36b2de88 100644 --- a/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java +++ b/api/src/main/java/com/discordsrv/api/player/DiscordSRVPlayer.java @@ -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(); diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 60669b3f..471db6be 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -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 { diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java index 62052921..a47d9fc6 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java @@ -225,6 +225,7 @@ public class BukkitDiscordSRV extends ServerDiscordSRV 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 mainThreadTasksForDisable() { + return mainThreadTasksForDisable; + } } diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/integration/PlaceholderAPIIntegration.java b/bukkit/src/main/java/com/discordsrv/bukkit/integration/PlaceholderAPIIntegration.java new file mode 100644 index 00000000..48439e24 --- /dev/null +++ b/bukkit/src/main/java/com/discordsrv/bukkit/integration/PlaceholderAPIIntegration.java @@ -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 . + */ + +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 { + + 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 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); + } + } +} diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/scheduler/BukkitScheduler.java b/bukkit/src/main/java/com/discordsrv/bukkit/scheduler/BukkitScheduler.java index fa1465c8..f5a8dbfd 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/scheduler/BukkitScheduler.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/scheduler/BukkitScheduler.java @@ -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 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)); } } diff --git a/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java b/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java index 2370eb07..a13ede30 100644 --- a/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java +++ b/common/src/main/java/com/discordsrv/common/placeholder/PlaceholderServiceImpl.java @@ -175,12 +175,13 @@ public class PlaceholderServiceImpl implements PlaceholderService { } @Override - public CharSequence getResultAsPlain(@NotNull Matcher matcher, @NotNull Set context) { + public @NotNull CharSequence getResultAsPlain(@NotNull Matcher matcher, @NotNull Set 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, Set> { + private Set> getAll(Class clazz) { + Set> 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 load(@NonNull Class key) { Set providers = new HashSet<>(); - Class currentClass = key; - do { - List> classes = new ArrayList<>(Arrays.asList(currentClass.getInterfaces())); - classes.add(currentClass); + Set> 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; } diff --git a/common/src/main/java/com/discordsrv/common/player/IPlayer.java b/common/src/main/java/com/discordsrv/common/player/IPlayer.java index 813e4bce..993b11cf 100644 --- a/common/src/main/java/com/discordsrv/common/player/IPlayer.java +++ b/common/src/main/java/com/discordsrv/common/player/IPlayer.java @@ -45,6 +45,7 @@ public interface IPlayer extends DiscordSRVPlayer, IOfflinePlayer, ICommandSende } @NotNull + @Placeholder("player_name") String username(); @Override diff --git a/settings.gradle b/settings.gradle index 132a12e8..9b51a993 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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')