From 72619c95d8df2dd9cdf1e6bb8ead0c64b91dd5b2 Mon Sep 17 00:00:00 2001 From: pavel444-byte Date: Thu, 30 Jul 2026 16:03:43 +0500 Subject: [PATCH] Add force-remove command for Multiverse inventory groups --- README.md | 11 ++- pom.xml | 2 +- .../AntiInventoriesCommand.java | 82 ++++++++++++++++++- .../AntiInventoriesPlugin.java | 2 +- .../MultiverseInventoriesHook.java | 67 ++++++++++++++- src/main/resources/plugin.yml | 5 +- .../antiinventories/BypassSettingsTest.java | 9 ++ 7 files changed, 168 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4229402..e6ede34 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,12 @@ silently replace that inventory. mvn clean package ``` -The finished plugin is `target/AntiInventories-1.0.0.jar`. +The finished plugin is `target/AntiInventories-1.1.0.jar`. ## Install 1. Back up the Multiverse-Inventories data directory. -2. Put `AntiInventories-1.0.0.jar` in the server's `plugins` directory. +2. Put `AntiInventories-1.1.0.jar` in the server's `plugins` directory. 3. Make sure Multiverse-Core and Multiverse-Inventories are installed. 4. Start the server. 5. Edit `plugins/AntiInventories/config.yml`, or use the commands below. @@ -44,12 +44,19 @@ The finished plugin is `target/AntiInventories-1.0.0.jar`. | --- | --- | | `/antiinv add ` | Add a bypass world | | `/antiinv remove ` | Remove a bypass world | +| `/antiinv forceremove ` | Remove the world from every explicit Multiverse-Inventories group and AntiInventories, then reload both plugins | | `/antiinv list` | Show bypass worlds and direction | | `/antiinv reload` | Reload `config.yml` | The aliases `/antiinventories`, `/antiinv`, and `/ai` are available. Commands require `antiinventories.admin`, which defaults to server operators. +`/antiinv list` also shows a clickable **Force remove** control beside each +bypass world. It places the force-removal command in chat for confirmation. +If a world belongs to a Multiverse-Inventories group through a wildcard or +regular expression, the command stops atomically and reports that pattern +instead of changing it and potentially removing other worlds. + ## Direction setting `bypass-direction` controls which crossings are ignored: diff --git a/pom.xml b/pom.xml index 013ab86..c6a0f3f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ dev.antiinventories anti-inventories - 1.0.0 + 1.1.0 AntiInventories Bypass Multiverse-Inventories for selected worlds. diff --git a/src/main/java/dev/antiinventories/AntiInventoriesCommand.java b/src/main/java/dev/antiinventories/AntiInventoriesCommand.java index c23233e..b8b0682 100644 --- a/src/main/java/dev/antiinventories/AntiInventoriesCommand.java +++ b/src/main/java/dev/antiinventories/AntiInventoriesCommand.java @@ -1,9 +1,13 @@ package dev.antiinventories; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.World; @@ -15,14 +19,19 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; final class AntiInventoriesCommand implements CommandExecutor, TabCompleter { - private static final List SUBCOMMANDS = List.of("add", "remove", "list", "reload"); + private static final List SUBCOMMANDS = List.of("add", "remove", "forceremove", "list", "reload"); private final AntiInventoriesPlugin plugin; private final BypassSettings settings; + private final MultiverseInventoriesHook multiverseHook; - AntiInventoriesCommand(AntiInventoriesPlugin plugin, BypassSettings settings) { + AntiInventoriesCommand( + AntiInventoriesPlugin plugin, + BypassSettings settings, + MultiverseInventoriesHook multiverseHook) { this.plugin = plugin; this.settings = settings; + this.multiverseHook = multiverseHook; } @Override @@ -39,6 +48,7 @@ final class AntiInventoriesCommand implements CommandExecutor, TabCompleter { return switch (args[0].toLowerCase(Locale.ROOT)) { case "add" -> addWorld(sender, label, args); case "remove" -> removeWorld(sender, label, args); + case "forceremove" -> forceRemoveWorld(sender, label, args); case "list" -> listWorlds(sender); case "reload" -> reload(sender); default -> { @@ -84,6 +94,59 @@ final class AntiInventoriesCommand implements CommandExecutor, TabCompleter { return true; } + private boolean forceRemoveWorld(CommandSender sender, String label, String[] args) { + if (args.length != 2) { + sender.sendMessage(Component.text("Usage: /" + label + " forceremove ", NamedTextColor.RED)); + return true; + } + + String worldName = canonicalWorldName(args[1]); + MultiverseInventoriesHook.ForceRemovalResult result; + try { + result = multiverseHook.forceRemoveWorld(worldName); + } catch (ReflectiveOperationException | LinkageError exception) { + plugin.getLogger().log( + java.util.logging.Level.SEVERE, + "Could not force-remove " + worldName + " from Multiverse-Inventories.", + exception); + sender.sendMessage(Component.text( + "Force removal failed. Check the server console; no AntiInventories setting was changed.", + NamedTextColor.RED)); + return true; + } + + if (!result.patternGroups().isEmpty()) { + sender.sendMessage(Component.text( + "Force removal stopped: " + worldName + " is matched by a wildcard or regex in group(s): " + + String.join(", ", result.patternGroups()) + ".", + NamedTextColor.RED)); + sender.sendMessage(Component.text( + "No AntiInventories or explicit Multiverse-Inventories setting was changed.", + NamedTextColor.YELLOW)); + return true; + } + + boolean removedFromAntiInventories = settings.remove(worldName); + plugin.saveBypassWorlds(); + boolean multiverseReloaded = multiverseHook.reloadMultiverseInventories(); + plugin.reloadPluginConfig(); + + sender.sendMessage(Component.text("Force removal completed for " + worldName + ".", NamedTextColor.GREEN)); + sender.sendMessage(Component.text( + "Multiverse groups removed: " + + (result.removedGroups().isEmpty() ? "(none)" : String.join(", ", result.removedGroups())), + NamedTextColor.YELLOW)); + sender.sendMessage(Component.text( + "Removed from AntiInventories: " + (removedFromAntiInventories ? "yes" : "not previously listed"), + NamedTextColor.YELLOW)); + sender.sendMessage(Component.text( + "Reloaded Multiverse-Inventories: " + (multiverseReloaded ? "yes" : "command was not available"), + multiverseReloaded ? NamedTextColor.GREEN : NamedTextColor.RED)); + sender.sendMessage(Component.text("Reloaded AntiInventories: yes", NamedTextColor.GREEN)); + + return true; + } + private boolean listWorlds(CommandSender sender) { List worlds = settings.worlds(); String formattedWorlds = worlds.isEmpty() ? "(none)" : String.join(", ", worlds); @@ -93,6 +156,15 @@ final class AntiInventoriesCommand implements CommandExecutor, TabCompleter { sender.sendMessage( Component.text("Direction: ", NamedTextColor.GOLD) .append(Component.text(settings.direction().name(), NamedTextColor.WHITE))); + for (String world : worlds) { + sender.sendMessage( + Component.text("• " + world + " ", NamedTextColor.WHITE) + .append(Component.text("[Force remove]", NamedTextColor.RED) + .clickEvent(ClickEvent.suggestCommand("/antiinv forceremove " + world)) + .hoverEvent(HoverEvent.showText(Component.text( + "Remove from Multiverse-Inventories and AntiInventories, then reload both.", + NamedTextColor.YELLOW))))); + } return true; } @@ -106,6 +178,7 @@ final class AntiInventoriesCommand implements CommandExecutor, TabCompleter { sender.sendMessage(Component.text("AntiInventories commands:", NamedTextColor.GOLD)); sender.sendMessage(Component.text("/" + label + " add ", NamedTextColor.YELLOW)); sender.sendMessage(Component.text("/" + label + " remove ", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/" + label + " forceremove ", NamedTextColor.RED)); sender.sendMessage(Component.text("/" + label + " list", NamedTextColor.YELLOW)); sender.sendMessage(Component.text("/" + label + " reload", NamedTextColor.YELLOW)); } @@ -130,6 +203,11 @@ final class AntiInventoriesCommand implements CommandExecutor, TabCompleter { if (args.length == 2 && args[0].equalsIgnoreCase("remove")) { return filter(settings.worlds(), args[1]); } + if (args.length == 2 && args[0].equalsIgnoreCase("forceremove")) { + Set candidates = new LinkedHashSet<>(settings.worlds()); + Bukkit.getWorlds().stream().map(World::getName).forEach(candidates::add); + return filter(List.copyOf(candidates), args[1]); + } return List.of(); } diff --git a/src/main/java/dev/antiinventories/AntiInventoriesPlugin.java b/src/main/java/dev/antiinventories/AntiInventoriesPlugin.java index f3b4f0e..a0010b4 100644 --- a/src/main/java/dev/antiinventories/AntiInventoriesPlugin.java +++ b/src/main/java/dev/antiinventories/AntiInventoriesPlugin.java @@ -18,7 +18,7 @@ public final class AntiInventoriesPlugin extends JavaPlugin { return; } - AntiInventoriesCommand commandHandler = new AntiInventoriesCommand(this, settings); + AntiInventoriesCommand commandHandler = new AntiInventoriesCommand(this, settings, hook); PluginCommand command = Objects.requireNonNull( getCommand("antiinventories"), "antiinventories command is missing from plugin.yml"); diff --git a/src/main/java/dev/antiinventories/MultiverseInventoriesHook.java b/src/main/java/dev/antiinventories/MultiverseInventoriesHook.java index 53a5eeb..c042085 100644 --- a/src/main/java/dev/antiinventories/MultiverseInventoriesHook.java +++ b/src/main/java/dev/antiinventories/MultiverseInventoriesHook.java @@ -2,6 +2,11 @@ package dev.antiinventories; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Set; import java.util.logging.Level; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; @@ -20,11 +25,16 @@ final class MultiverseInventoriesHook { "org.mvplugins.multiverse.inventories.event.ReadOnlyShareHandlingEvent"; private static final String WRITE_ONLY_EVENT = "org.mvplugins.multiverse.inventories.event.WriteOnlyShareHandlingEvent"; + private static final String INVENTORIES_API = + "org.mvplugins.multiverse.inventories.MultiverseInventoriesApi"; + private static final String WORLD_GROUP = + "org.mvplugins.multiverse.inventories.profile.group.WorldGroup"; private final AntiInventoriesPlugin plugin; private final BypassSettings settings; private final Listener listener = new Listener() { }; + private Plugin inventories; MultiverseInventoriesHook(AntiInventoriesPlugin plugin, BypassSettings settings) { this.plugin = plugin; @@ -33,7 +43,7 @@ final class MultiverseInventoriesHook { boolean register() { PluginManager pluginManager = plugin.getServer().getPluginManager(); - Plugin inventories = pluginManager.getPlugin("Multiverse-Inventories"); + inventories = pluginManager.getPlugin("Multiverse-Inventories"); if (inventories == null || !inventories.isEnabled()) { plugin.getLogger().severe("Multiverse-Inventories is not installed or enabled."); return false; @@ -54,6 +64,58 @@ final class MultiverseInventoriesHook { } } + ForceRemovalResult forceRemoveWorld(String worldName) throws ReflectiveOperationException { + ClassLoader classLoader = inventories.getClass().getClassLoader(); + Class apiClass = Class.forName(INVENTORIES_API, true, classLoader); + Object api = apiClass.getMethod("get").invoke(null); + Method getWorldGroupManager = apiClass.getMethod("getWorldGroupManager"); + Object groupManager = getWorldGroupManager.invoke(api); + Class groupManagerType = getWorldGroupManager.getReturnType(); + Method getGroupsForWorld = groupManagerType.getMethod("getGroupsForWorld", String.class); + + @SuppressWarnings("unchecked") + List groups = new ArrayList<>((List) getGroupsForWorld.invoke(groupManager, worldName)); + Class worldGroupType = Class.forName(WORLD_GROUP, true, classLoader); + Method getName = worldGroupType.getMethod("getName"); + Method getConfigWorlds = worldGroupType.getMethod("getConfigWorlds"); + Method removeWorlds = worldGroupType.getMethod("removeWorlds", Collection.class); + + List removedGroups = new ArrayList<>(); + List patternGroups = new ArrayList<>(); + List explicitGroups = new ArrayList<>(); + String normalizedWorld = worldName.toLowerCase(Locale.ROOT); + + for (Object group : groups) { + String groupName = (String) getName.invoke(group); + @SuppressWarnings("unchecked") + Set configuredWorlds = (Set) getConfigWorlds.invoke(group); + if (!configuredWorlds.contains(normalizedWorld)) { + patternGroups.add(groupName); + continue; + } + explicitGroups.add(group); + } + + if (!patternGroups.isEmpty()) { + return new ForceRemovalResult(List.of(), List.copyOf(patternGroups)); + } + + for (Object group : explicitGroups) { + boolean removed = (boolean) removeWorlds.invoke(group, Set.of(worldName)); + if (removed) { + removedGroups.add((String) getName.invoke(group)); + } + } + + return new ForceRemovalResult(List.copyOf(removedGroups), List.copyOf(patternGroups)); + } + + boolean reloadMultiverseInventories() { + return plugin.getServer().dispatchCommand( + plugin.getServer().getConsoleSender(), + "mvinv reload"); + } + private void registerWorldChangeHook(PluginManager manager, ClassLoader classLoader) throws ReflectiveOperationException { Class eventClass = loadEventClass(WORLD_CHANGE_EVENT, classLoader); @@ -136,4 +198,7 @@ final class MultiverseInventoriesHook { private interface PlayerRule { boolean shouldBypass(Player player); } + + record ForceRemovalResult(List removedGroups, List patternGroups) { + } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 51ab37b..bf1d1f6 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: AntiInventories -version: '1.0.0' +version: '1.1.0' main: dev.antiinventories.AntiInventoriesPlugin description: Keeps the current inventory when players cross selected world borders. api-version: '1.21.11' @@ -10,7 +10,7 @@ depend: commands: antiinventories: description: Manage worlds that bypass Multiverse-Inventories. - usage: /antiinventories [world] + usage: /antiinventories [world] aliases: - antiinv - ai @@ -20,4 +20,3 @@ permissions: antiinventories.admin: description: Allows management of AntiInventories. default: op - diff --git a/src/test/java/dev/antiinventories/BypassSettingsTest.java b/src/test/java/dev/antiinventories/BypassSettingsTest.java index 54e88bb..73e6a2e 100644 --- a/src/test/java/dev/antiinventories/BypassSettingsTest.java +++ b/src/test/java/dev/antiinventories/BypassSettingsTest.java @@ -50,6 +50,15 @@ class BypassSettingsTest { assertTrue(settings.shouldBypass("AutoMine", "Wild_Forest1")); } + @Test + void removesConfiguredWorldCaseInsensitively() { + BypassSettings settings = settings(BypassDirection.EITHER, "AutoMine", "Wild_Forest1"); + + assertTrue(settings.remove("automine")); + assertFalse(settings.contains("AutoMine")); + assertTrue(settings.contains("Wild_Forest1")); + } + private static BypassSettings settings(BypassDirection direction, String... worlds) { BypassSettings settings = new BypassSettings(); settings.load(List.of(worlds), direction.name(), true, true);