diff --git a/build.gradle.kts b/build.gradle.kts index 182ae77..0d46baa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -65,12 +65,16 @@ repositories { maven("https://repo.bluecolored.de/releases") { name = "bluemap" } + maven("https://repo.extendedclip.com/releases/") { + name = "clip" + } } dependencies { compileOnly("io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT") compileOnly("net.luckperms:api:5.4") compileOnly("de.bluecolored:bluemap-api:2.7.4") + compileOnly("me.clip:placeholderapi:2.11.7") implementation("org.bstats:bstats-bukkit:3.1.0") } @@ -101,6 +105,7 @@ tasks.shadowJar { val plugins = runPaper.downloadPluginsSpec { modrinth("viaversion", "5.6.0") // makes testing much easier modrinth("bluemap", "5.5-paper") + modrinth("placeholderapi", "2.11.7") } // Paper (non-Folia!) server diff --git a/src/main/java/org/modernbeta/admintoolbox/AdminToolboxPlugin.java b/src/main/java/org/modernbeta/admintoolbox/AdminToolboxPlugin.java index 9544b4a..dd595c6 100644 --- a/src/main/java/org/modernbeta/admintoolbox/AdminToolboxPlugin.java +++ b/src/main/java/org/modernbeta/admintoolbox/AdminToolboxPlugin.java @@ -12,7 +12,9 @@ import org.modernbeta.admintoolbox.commands.*; import org.modernbeta.admintoolbox.integration.BlueMapIntegration; import org.modernbeta.admintoolbox.integration.luckperms.LuckPermsIntegration; +import org.modernbeta.admintoolbox.integration.placeholderapi.PlaceholderAPIIntegration; import org.modernbeta.admintoolbox.managers.FreezeManager; +import org.modernbeta.admintoolbox.managers.StreamerModeManager; import org.modernbeta.admintoolbox.managers.admin.AdminManager; import javax.annotation.Nullable; @@ -26,8 +28,9 @@ public class AdminToolboxPlugin extends JavaPlugin { static AdminToolboxPlugin instance; - AdminManager adminManager; - FreezeManager freezeManager; + private AdminManager adminManager; + private FreezeManager freezeManager; + private @Nullable StreamerModeManager streamerModeManager; PermissionAudience broadcastAudience; @@ -36,6 +39,7 @@ public class AdminToolboxPlugin extends JavaPlugin { private @Nullable BlueMapIntegration blueMapIntegration = null; private @Nullable LuckPermsIntegration luckPermsIntegration = null; + private @Nullable PlaceholderAPIIntegration placeholderAPIIntegration = null; private static final String ADMIN_STATE_CONFIG_FILENAME = "admin-state.yml"; @@ -50,7 +54,6 @@ public void onEnable() { this.adminManager = new AdminManager(); this.freezeManager = new FreezeManager(); - this.broadcastAudience = new PermissionAudience(BROADCAST_AUDIENCE_PERMISSION); createAdminStateConfig(); @@ -78,7 +81,8 @@ public void onEnable() { this.luckPermsIntegration = new LuckPermsIntegration(provider.getProvider()); this.luckPermsIntegration.registerCalculator(); - getCommand("streamermode").setExecutor(new StreamerModeCommand()); + this.streamerModeManager = new StreamerModeManager(this, luckPermsIntegration); + getCommand("streamermode").setExecutor(new StreamerModeCommand(streamerModeManager)); } } catch (NoClassDefFoundError e) { getLogger().warning("LuckPerms not found! Some features will be unavailable."); @@ -94,6 +98,13 @@ public void onEnable() { getLogger().warning("BlueMap API not found! Some features will be unavailable."); } + try { + this.placeholderAPIIntegration = new PlaceholderAPIIntegration(this); + this.placeholderAPIIntegration.registerPlaceholders(); + } catch (NoClassDefFoundError e) { + getLogger().warning("PlaceholderAPI is not available! Some features will be unavailable."); + } + // bStats - plugin analytics. Toggleable in server-level bStats config. new Metrics(this, BSTATS_PLUGIN_ID); @@ -149,6 +160,10 @@ public FreezeManager getFreezeManager() { return freezeManager; } + public Optional getStreamerModeManager() { + return Optional.ofNullable(streamerModeManager); + } + public PermissionAudience getAdminAudience() { return broadcastAudience; } diff --git a/src/main/java/org/modernbeta/admintoolbox/commands/StreamerModeCommand.java b/src/main/java/org/modernbeta/admintoolbox/commands/StreamerModeCommand.java index 57eb6d2..23d9652 100644 --- a/src/main/java/org/modernbeta/admintoolbox/commands/StreamerModeCommand.java +++ b/src/main/java/org/modernbeta/admintoolbox/commands/StreamerModeCommand.java @@ -1,12 +1,6 @@ package org.modernbeta.admintoolbox.commands; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; -import net.luckperms.api.LuckPerms; -import net.luckperms.api.model.user.User; -import net.luckperms.api.node.Node; -import net.luckperms.api.node.NodeType; -import net.luckperms.api.node.types.MetaNode; -import net.luckperms.api.node.types.PermissionNode; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -15,6 +9,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.modernbeta.admintoolbox.AdminToolboxPlugin; +import org.modernbeta.admintoolbox.managers.StreamerModeManager; import java.time.Duration; import java.time.temporal.ChronoUnit; @@ -25,16 +20,19 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.modernbeta.admintoolbox.managers.StreamerModeManager.STREAMER_MODE_USE_PERMISSION; + public class StreamerModeCommand implements CommandExecutor, TabCompleter { private final AdminToolboxPlugin plugin = AdminToolboxPlugin.getInstance(); + private final StreamerModeManager manager; - private static final String STREAMER_MODE_COMMAND_PERMISSION = "admintoolbox.streamermode"; - private static final String STREAMER_MODE_BYPASS_MAX_DURATION_PERMISSION = "admintoolbox.streamermode.unlimited"; - private static final String STREAMER_MODE_LP_META_KEY = "at-streamer-mode-enabled"; + public StreamerModeCommand(StreamerModeManager streamerModeManager) { + this.manager = streamerModeManager; + } @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!sender.hasPermission(STREAMER_MODE_COMMAND_PERMISSION)) + if (!sender.hasPermission(STREAMER_MODE_USE_PERMISSION)) return false; // Bukkit should handle this for us, just a sanity-check if (!(sender instanceof Player player)) { sender.sendRichMessage("Only players may use Streamer Mode."); @@ -50,21 +48,12 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command sender.sendRichMessage("LuckPerms is required to use Streamer Mode. Is it enabled?"); return true; } - LuckPerms luckPerms = plugin.getLuckPerms().get().api(); - - List disablePermissions = plugin.getConfig().getStringList("streamer-mode.disable-permissions"); - User user = luckPerms.getPlayerAdapter(Player.class).getUser(player); - if (args.length == 0 && isStreamerModeActive(luckPerms, player)) { - user.data().clear(NodeType.META.predicate((node) -> node.getMetaKey().equals(STREAMER_MODE_LP_META_KEY))); - user.data().clear(NodeType.PERMISSION.predicate((node) -> // only delete negated, expiring nodes that match configured permissions - node.isNegated() - && node.getExpiryDuration() != null - && disablePermissions.contains(node.getPermission()) - )); - luckPerms.getUserManager().saveUser(user); - - sender.sendRichMessage("Streamer Mode has been disabled."); + if (args.length == 0 && manager.isActive(player)) { + manager.disable(player) + .thenAccept(state -> { + sender.sendRichMessage("Streamer Mode has been disabled."); + }); return true; } @@ -85,40 +74,16 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command Duration duration = parsedDuration.get(); - final double maxDurationMinutes = plugin.getConfig().getDouble("streamer-mode.max-duration"); - if ((duration.getSeconds() > (maxDurationMinutes * 60)) - && !sender.hasPermission(STREAMER_MODE_BYPASS_MAX_DURATION_PERMISSION)) { + if (!manager.isAllowableDuration(duration, player)) { sender.sendRichMessage("That duration is above the maximum allowed!"); return true; } - MetaNode metaNode = MetaNode.builder() - .key(STREAMER_MODE_LP_META_KEY) - .value(Boolean.toString(true)) - .expiry(duration) - .build(); - - user.data().clear(NodeType.META.predicate((node) -> node.getMetaKey().equals(STREAMER_MODE_LP_META_KEY))); - user.data().add(metaNode); - - // using LuckPerms API, add negated/'false' versions of permissions from config.yml to user for duration - for (String permission : disablePermissions) { - Node permissionNode = PermissionNode.builder() - .permission(permission) - .expiry(duration) - .negated(true) - .build(); - - user.data().clear(NodeType.PERMISSION.predicate( - (node) -> node.getPermission().equals(permission) && node.isNegated() - )); - user.data().add(permissionNode); - } - - luckPerms.getUserManager().saveUser(user); - - sender.sendRichMessage("Streamer Mode will be enabled for .", - Placeholder.unparsed("duration", formatDuration(duration))); + manager.enable(player, duration) + .thenAccept(state -> { + sender.sendRichMessage("Streamer Mode will be enabled for .", + Placeholder.unparsed("duration", formatDuration(state.duration()))); + }); return true; } @@ -130,7 +95,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command String partialEntry = args[0]; - if(partialEntry.isEmpty()) { + if (partialEntry.isEmpty()) { // Suggest durations if nothing is entered yet -- this is a good UX hint for how to use the command! return List.of("15m", "30m", "5h", "8h"); } @@ -159,7 +124,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command /// Only one duration segment is supported. That means durations such as /// '1h15m' will fail to parse. private Optional parseDuration(String input) { - Pattern durationPattern = Pattern.compile("^\\s*(?\\d{1,3})(?[mh])\\s*$", Pattern.CASE_INSENSITIVE); + Pattern durationPattern = Pattern.compile("^\\s*(?[1-9]\\d{0,2})(?[mh])\\s*$", Pattern.CASE_INSENSITIVE); Matcher matcher = durationPattern.matcher(input); if (!matcher.matches()) @@ -197,11 +162,4 @@ private String formatDuration(Duration duration) { return String.join(" ", resultList); } - - private boolean isStreamerModeActive(LuckPerms luckPerms, Player player) { - return luckPerms.getPlayerAdapter(Player.class) - .getMetaData(player) - .getMetaValue(STREAMER_MODE_LP_META_KEY, Boolean::valueOf) - .orElse(false); - } } diff --git a/src/main/java/org/modernbeta/admintoolbox/integration/placeholderapi/PlaceholderAPIIntegration.java b/src/main/java/org/modernbeta/admintoolbox/integration/placeholderapi/PlaceholderAPIIntegration.java new file mode 100644 index 0000000..d659085 --- /dev/null +++ b/src/main/java/org/modernbeta/admintoolbox/integration/placeholderapi/PlaceholderAPIIntegration.java @@ -0,0 +1,20 @@ +package org.modernbeta.admintoolbox.integration.placeholderapi; + +import me.clip.placeholderapi.PlaceholderAPI; +import org.modernbeta.admintoolbox.AdminToolboxPlugin; +import org.modernbeta.admintoolbox.integration.placeholderapi.expansion.StreamerModePlaceholder; + +public class PlaceholderAPIIntegration { + private final AdminToolboxPlugin plugin; + + private final StreamerModePlaceholder streamerModePlaceholder; + + public PlaceholderAPIIntegration(AdminToolboxPlugin plugin) { + this.plugin = plugin; + this.streamerModePlaceholder = new StreamerModePlaceholder(plugin); + } + + public boolean registerPlaceholders() { + return this.streamerModePlaceholder.register(); + } +} diff --git a/src/main/java/org/modernbeta/admintoolbox/integration/placeholderapi/expansion/StreamerModePlaceholder.java b/src/main/java/org/modernbeta/admintoolbox/integration/placeholderapi/expansion/StreamerModePlaceholder.java new file mode 100644 index 0000000..37882c3 --- /dev/null +++ b/src/main/java/org/modernbeta/admintoolbox/integration/placeholderapi/expansion/StreamerModePlaceholder.java @@ -0,0 +1,63 @@ +package org.modernbeta.admintoolbox.integration.placeholderapi.expansion; + +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import me.clip.placeholderapi.expansion.Relational; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.modernbeta.admintoolbox.AdminToolboxPlugin; + +import javax.annotation.Nullable; + +public class StreamerModePlaceholder extends PlaceholderExpansion implements Relational { + private final AdminToolboxPlugin plugin; + + private static final String SM_VIEW_PERMISSION = "admintoolbox.streamermode.placeholder.view"; + private static final String SM_WEAR_PERMISSION = "admintoolbox.streamermode.placeholder.wear"; + + public StreamerModePlaceholder(AdminToolboxPlugin plugin) { + this.plugin = plugin; + } + + @Override + public @NotNull String getIdentifier() { + return "streamermode"; + } + + @SuppressWarnings("UnstableApiUsage") + @Override + public @NotNull String getAuthor() { + return String.join(", ", plugin.getPluginMeta().getAuthors()); + } + + @SuppressWarnings("UnstableApiUsage") + @Override + public @NotNull String getVersion() { + return plugin.getPluginMeta().getVersion(); + } + + @Override + public boolean persist() { + return true; + } + + @Override + public String onPlaceholderRequest(Player viewer, Player wearer, String identifier) { + if (viewer == null || wearer == null) return ""; + if (!viewer.hasPermission(SM_VIEW_PERMISSION)) return ""; + if (!wearer.hasPermission(SM_WEAR_PERMISSION)) return ""; + + boolean isActive = plugin.getStreamerModeManager() + .map(sm -> sm.isActive(wearer)) + .orElse(false); + if (!isActive) return ""; + + String tag = ChatColor.RED + "[SM]"; + return switch (identifier.toLowerCase()) { + case "prefix" -> tag + " "; + case "suffix" -> " " + tag; + case "tag" -> tag; + default -> null; + }; + } +} diff --git a/src/main/java/org/modernbeta/admintoolbox/managers/StreamerModeManager.java b/src/main/java/org/modernbeta/admintoolbox/managers/StreamerModeManager.java new file mode 100644 index 0000000..d20e076 --- /dev/null +++ b/src/main/java/org/modernbeta/admintoolbox/managers/StreamerModeManager.java @@ -0,0 +1,137 @@ +package org.modernbeta.admintoolbox.managers; + +import net.luckperms.api.model.user.User; +import net.luckperms.api.model.user.UserManager; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.MetaNode; +import net.luckperms.api.node.types.PermissionNode; +import net.luckperms.api.platform.PlayerAdapter; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.modernbeta.admintoolbox.AdminToolboxPlugin; +import org.modernbeta.admintoolbox.integration.luckperms.LuckPermsIntegration; + +import javax.annotation.Nullable; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class StreamerModeManager { + public static final String STREAMER_MODE_USE_PERMISSION = "admintoolbox.streamermode"; + public static final String STREAMER_MODE_BYPASS_MAX_DURATION_PERMISSION = "admintoolbox.streamermode.unlimited"; + private static final String STREAMER_MODE_LP_META_KEY = "at-streamer-mode-enabled"; + + private final AdminToolboxPlugin plugin; + private final LuckPermsIntegration luckPerms; + + public StreamerModeManager(AdminToolboxPlugin plugin, LuckPermsIntegration luckPerms) { + this.plugin = plugin; + this.luckPerms = luckPerms; + } + + public record StreamerModeState( + OfflinePlayer player, + boolean isEnabled, + @Nullable Duration duration + ) { + } + + public CompletableFuture enable(Player player, Duration duration) { + UserManager userManager = luckPerms.api().getUserManager(); + User user = luckPerms.api().getPlayerAdapter(Player.class).getUser(player); + List disablePermissions = plugin.getConfig().getStringList("streamer-mode.disable-permissions"); + + MetaNode metaNode = MetaNode.builder() + .key(STREAMER_MODE_LP_META_KEY) + .value(Boolean.toString(true)) + .expiry(duration) + .build(); + + user.data().clear(NodeType.META.predicate((node) -> node.getMetaKey().equals(STREAMER_MODE_LP_META_KEY))); + user.data().add(metaNode); + + // using LuckPerms API, add negated/'false' versions of permissions from config.yml to user for duration + for (String permission : disablePermissions) { + Node permissionNode = PermissionNode.builder() + .permission(permission) + .expiry(duration) + .negated(true) + .build(); + + user.data().clear(NodeType.PERMISSION.predicate( + (node) -> node.getPermission().equals(permission) && node.isNegated() + )); + user.data().add(permissionNode); + } + + return userManager.saveUser(user) + .thenApply((_void) -> new StreamerModeState( + player, + true, + duration + )); + } + + public CompletableFuture disable(Player player) { + UserManager userManager = luckPerms.api().getUserManager(); + User user = luckPerms.api().getPlayerAdapter(Player.class).getUser(player); + List disablePermissions = plugin.getConfig().getStringList("streamer-mode.disable-permissions"); + + user.data().clear(NodeType.META.predicate((node) -> node.getMetaKey().equals(STREAMER_MODE_LP_META_KEY))); + user.data().clear(NodeType.PERMISSION.predicate((node) -> // only delete negated, expiring nodes that match configured permissions + node.isNegated() + && node.getExpiryDuration() != null + && node.getExpiryDuration().isPositive() + && disablePermissions.contains(node.getPermission()) + )); + + return userManager.saveUser(user) + .thenApply((_void) -> new StreamerModeState( + player, + false, + null + )); + } + + public boolean isActive(Player player) { + return getState(player).isEnabled(); + } + + public boolean isAllowableDuration(Duration duration, Player player) { + final double maxDurationMinutes = plugin.getConfig().getDouble("streamer-mode.max-duration", 720d); + return (duration.getSeconds() <= (maxDurationMinutes * 60)) + || player.hasPermission(STREAMER_MODE_BYPASS_MAX_DURATION_PERMISSION); + } + + public StreamerModeState getState(Player player) { + final PlayerAdapter playerAdapter = + luckPerms.api().getPlayerAdapter(Player.class); + + boolean isEnabled = playerAdapter + .getMetaData(player) + .getMetaValue(STREAMER_MODE_LP_META_KEY, Boolean::valueOf) + .orElse(false); + + Duration duration = null; + if (isEnabled) getDuration:{ + Node node = playerAdapter + .getMetaData(player) + .queryMetaValue(STREAMER_MODE_LP_META_KEY) + .node(); + + if (node == null || node.hasExpired()) { + isEnabled = false; + break getDuration; + } + + duration = node.getExpiryDuration(); + } + + return new StreamerModeState( + player, + isEnabled, + duration + ); + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index bc66e6e..d593b16 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -4,7 +4,7 @@ authors: [ Karltroid, iLynxcat ] main: org.modernbeta.admintoolbox.AdminToolboxPlugin api-version: '1.20' folia-supported: true -softdepend: [ BlueMap, LuckPerms ] +softdepend: [ BlueMap, LuckPerms, PlaceholderAPI ] default-permission: op permissions: @@ -56,6 +56,12 @@ permissions: admintoolbox.streamermode.unlimited: description: Can bypass maximum duration in Streamer Mode. default: false + admintoolbox.streamermode.placeholder.wear: + description: Can wear placeholder while in Streamer Mode. + default: true + admintoolbox.streamermode.placeholder.view: + description: Can see other players' Streamer Mode placeholder. + default: op commands: admintoolbox: