Add force-remove command for Multiverse inventory groups

This commit is contained in:
2026-07-30 16:03:43 +05:00
parent 586306c9fa
commit 72619c95d8
7 changed files with 168 additions and 10 deletions

View File

@@ -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<String> SUBCOMMANDS = List.of("add", "remove", "list", "reload");
private static final List<String> 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 <world>", 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<String> 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 <world>", NamedTextColor.YELLOW));
sender.sendMessage(Component.text("/" + label + " remove <world>", NamedTextColor.YELLOW));
sender.sendMessage(Component.text("/" + label + " forceremove <world>", 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<String> candidates = new LinkedHashSet<>(settings.worlds());
Bukkit.getWorlds().stream().map(World::getName).forEach(candidates::add);
return filter(List.copyOf(candidates), args[1]);
}
return List.of();
}

View File

@@ -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");

View File

@@ -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<Object> groups = new ArrayList<>((List<Object>) 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<String> removedGroups = new ArrayList<>();
List<String> patternGroups = new ArrayList<>();
List<Object> explicitGroups = new ArrayList<>();
String normalizedWorld = worldName.toLowerCase(Locale.ROOT);
for (Object group : groups) {
String groupName = (String) getName.invoke(group);
@SuppressWarnings("unchecked")
Set<String> configuredWorlds = (Set<String>) 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<? extends Event> eventClass = loadEventClass(WORLD_CHANGE_EVENT, classLoader);
@@ -136,4 +198,7 @@ final class MultiverseInventoriesHook {
private interface PlayerRule {
boolean shouldBypass(Player player);
}
record ForceRemovalResult(List<String> removedGroups, List<String> patternGroups) {
}
}

View File

@@ -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 <add|remove|list|reload> [world]
usage: /antiinventories <add|remove|forceremove|list|reload> [world]
aliases:
- antiinv
- ai
@@ -20,4 +20,3 @@ permissions:
antiinventories.admin:
description: Allows management of AntiInventories.
default: op

View File

@@ -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);