Intial commit

This commit is contained in:
2026-07-28 15:01:23 +05:00
commit 586306c9fa
11 changed files with 674 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
package dev.antiinventories;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
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 final AntiInventoriesPlugin plugin;
private final BypassSettings settings;
AntiInventoriesCommand(AntiInventoriesPlugin plugin, BypassSettings settings) {
this.plugin = plugin;
this.settings = settings;
}
@Override
public boolean onCommand(
@NotNull CommandSender sender,
@NotNull Command command,
@NotNull String label,
@NotNull String[] args) {
if (args.length == 0) {
sendUsage(sender, label);
return true;
}
return switch (args[0].toLowerCase(Locale.ROOT)) {
case "add" -> addWorld(sender, label, args);
case "remove" -> removeWorld(sender, label, args);
case "list" -> listWorlds(sender);
case "reload" -> reload(sender);
default -> {
sendUsage(sender, label);
yield true;
}
};
}
private boolean addWorld(CommandSender sender, String label, String[] args) {
if (args.length != 2) {
sender.sendMessage(Component.text("Usage: /" + label + " add <world>", NamedTextColor.RED));
return true;
}
String worldName = canonicalWorldName(args[1]);
if (!settings.add(worldName)) {
sender.sendMessage(Component.text(worldName + " is already a bypass world.", NamedTextColor.YELLOW));
return true;
}
plugin.saveBypassWorlds();
sender.sendMessage(Component.text("Added " + worldName + " to the bypass worlds.", NamedTextColor.GREEN));
if (Bukkit.getWorld(worldName) == null) {
sender.sendMessage(Component.text("Warning: that world is not currently loaded.", NamedTextColor.YELLOW));
}
return true;
}
private boolean removeWorld(CommandSender sender, String label, String[] args) {
if (args.length != 2) {
sender.sendMessage(Component.text("Usage: /" + label + " remove <world>", NamedTextColor.RED));
return true;
}
if (!settings.remove(args[1])) {
sender.sendMessage(Component.text(args[1] + " is not a bypass world.", NamedTextColor.YELLOW));
return true;
}
plugin.saveBypassWorlds();
sender.sendMessage(Component.text("Removed " + args[1] + " from the bypass worlds.", NamedTextColor.GREEN));
return true;
}
private boolean listWorlds(CommandSender sender) {
List<String> worlds = settings.worlds();
String formattedWorlds = worlds.isEmpty() ? "(none)" : String.join(", ", worlds);
sender.sendMessage(
Component.text("Bypass worlds: ", NamedTextColor.GOLD)
.append(Component.text(formattedWorlds, NamedTextColor.WHITE)));
sender.sendMessage(
Component.text("Direction: ", NamedTextColor.GOLD)
.append(Component.text(settings.direction().name(), NamedTextColor.WHITE)));
return true;
}
private boolean reload(CommandSender sender) {
plugin.reloadPluginConfig();
sender.sendMessage(Component.text("AntiInventories configuration reloaded.", NamedTextColor.GREEN));
return true;
}
private void sendUsage(CommandSender sender, String label) {
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 + " list", NamedTextColor.YELLOW));
sender.sendMessage(Component.text("/" + label + " reload", NamedTextColor.YELLOW));
}
private static String canonicalWorldName(String input) {
World world = Bukkit.getWorld(input);
return world == null ? input : world.getName();
}
@Override
public @Nullable List<String> onTabComplete(
@NotNull CommandSender sender,
@NotNull Command command,
@NotNull String alias,
@NotNull String[] args) {
if (args.length == 1) {
return filter(SUBCOMMANDS, args[0]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("add")) {
return filter(Bukkit.getWorlds().stream().map(World::getName).toList(), args[1]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("remove")) {
return filter(settings.worlds(), args[1]);
}
return List.of();
}
private static List<String> filter(List<String> candidates, String prefix) {
String normalizedPrefix = prefix.toLowerCase(Locale.ROOT);
List<String> matches = new ArrayList<>();
for (String candidate : candidates) {
if (candidate.toLowerCase(Locale.ROOT).startsWith(normalizedPrefix)) {
matches.add(candidate);
}
}
return matches;
}
}

View File

@@ -0,0 +1,53 @@
package dev.antiinventories;
import java.util.Objects;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;
public final class AntiInventoriesPlugin extends JavaPlugin {
private final BypassSettings settings = new BypassSettings();
@Override
public void onEnable() {
saveDefaultConfig();
reloadPluginConfig();
MultiverseInventoriesHook hook = new MultiverseInventoriesHook(this, settings);
if (!hook.register()) {
getServer().getPluginManager().disablePlugin(this);
return;
}
AntiInventoriesCommand commandHandler = new AntiInventoriesCommand(this, settings);
PluginCommand command = Objects.requireNonNull(
getCommand("antiinventories"),
"antiinventories command is missing from plugin.yml");
command.setExecutor(commandHandler);
command.setTabCompleter(commandHandler);
getLogger().info(
"Enabled with " + settings.worlds().size()
+ " bypass world(s), direction " + settings.direction() + ".");
}
void reloadPluginConfig() {
reloadConfig();
String configuredDirection = getConfig().getString("bypass-direction", "EITHER");
BypassDirection parsedDirection = BypassDirection.parse(configuredDirection);
if (!parsedDirection.name().equalsIgnoreCase(configuredDirection.trim())) {
getLogger().warning(
"Unknown bypass-direction '" + configuredDirection + "'; using EITHER.");
}
settings.load(
getConfig().getStringList("bypass-worlds"),
configuredDirection,
getConfig().getBoolean("bypass-gamemode-handling", true),
getConfig().getBoolean("bypass-session-handling", true));
}
void saveBypassWorlds() {
getConfig().set("bypass-worlds", settings.worlds());
saveConfig();
}
}

View File

@@ -0,0 +1,30 @@
package dev.antiinventories;
import java.util.Locale;
enum BypassDirection {
ENTERING,
LEAVING,
EITHER;
static BypassDirection parse(String value) {
if (value == null) {
return EITHER;
}
try {
return valueOf(value.trim().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException ignored) {
return EITHER;
}
}
boolean matches(boolean sourceIsBypassed, boolean destinationIsBypassed) {
return switch (this) {
case ENTERING -> destinationIsBypassed;
case LEAVING -> sourceIsBypassed;
case EITHER -> sourceIsBypassed || destinationIsBypassed;
};
}
}

View File

@@ -0,0 +1,70 @@
package dev.antiinventories;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
final class BypassSettings {
private final Map<String, String> worlds = new LinkedHashMap<>();
private BypassDirection direction = BypassDirection.EITHER;
private boolean bypassGameModeHandling = true;
private boolean bypassSessionHandling = true;
void load(
Collection<String> configuredWorlds,
String configuredDirection,
boolean bypassGameMode,
boolean bypassSession) {
worlds.clear();
configuredWorlds.forEach(this::add);
direction = BypassDirection.parse(configuredDirection);
bypassGameModeHandling = bypassGameMode;
bypassSessionHandling = bypassSession;
}
boolean add(String worldName) {
String cleanName = clean(worldName);
if (cleanName.isEmpty()) {
return false;
}
return worlds.putIfAbsent(normalize(cleanName), cleanName) == null;
}
boolean remove(String worldName) {
return worlds.remove(normalize(clean(worldName))) != null;
}
boolean contains(String worldName) {
return worlds.containsKey(normalize(clean(worldName)));
}
boolean shouldBypass(String fromWorld, String toWorld) {
return direction.matches(contains(fromWorld), contains(toWorld));
}
boolean shouldBypassGameMode(String currentWorld) {
return bypassGameModeHandling && contains(currentWorld);
}
boolean shouldBypassSession(String currentWorld) {
return bypassSessionHandling && contains(currentWorld);
}
List<String> worlds() {
return List.copyOf(worlds.values());
}
BypassDirection direction() {
return direction;
}
private static String clean(String value) {
return value == null ? "" : value.trim();
}
private static String normalize(String value) {
return value.toLowerCase(Locale.ROOT);
}
}

View File

@@ -0,0 +1,139 @@
package dev.antiinventories;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Level;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
final class MultiverseInventoriesHook {
private static final String WORLD_CHANGE_EVENT =
"org.mvplugins.multiverse.inventories.event.WorldChangeShareHandlingEvent";
private static final String GAME_MODE_CHANGE_EVENT =
"org.mvplugins.multiverse.inventories.event.GameModeChangeShareHandlingEvent";
private static final String READ_ONLY_EVENT =
"org.mvplugins.multiverse.inventories.event.ReadOnlyShareHandlingEvent";
private static final String WRITE_ONLY_EVENT =
"org.mvplugins.multiverse.inventories.event.WriteOnlyShareHandlingEvent";
private final AntiInventoriesPlugin plugin;
private final BypassSettings settings;
private final Listener listener = new Listener() {
};
MultiverseInventoriesHook(AntiInventoriesPlugin plugin, BypassSettings settings) {
this.plugin = plugin;
this.settings = settings;
}
boolean register() {
PluginManager pluginManager = plugin.getServer().getPluginManager();
Plugin inventories = pluginManager.getPlugin("Multiverse-Inventories");
if (inventories == null || !inventories.isEnabled()) {
plugin.getLogger().severe("Multiverse-Inventories is not installed or enabled.");
return false;
}
try {
registerWorldChangeHook(pluginManager, inventories.getClass().getClassLoader());
registerGameModeChangeHook(pluginManager, inventories.getClass().getClassLoader());
registerSessionHook(pluginManager, inventories.getClass().getClassLoader(), READ_ONLY_EVENT);
registerSessionHook(pluginManager, inventories.getClass().getClassLoader(), WRITE_ONLY_EVENT);
return true;
} catch (ReflectiveOperationException | LinkageError exception) {
plugin.getLogger().log(
Level.SEVERE,
"This Multiverse-Inventories version does not expose the required share-handling events.",
exception);
return false;
}
}
private void registerWorldChangeHook(PluginManager manager, ClassLoader classLoader)
throws ReflectiveOperationException {
Class<? extends Event> eventClass = loadEventClass(WORLD_CHANGE_EVENT, classLoader);
Method getFromWorld = eventClass.getMethod("getFromWorld");
Method getToWorld = eventClass.getMethod("getToWorld");
manager.registerEvent(
eventClass,
listener,
EventPriority.HIGHEST,
(ignored, event) -> {
try {
String fromWorld = (String) getFromWorld.invoke(event);
String toWorld = (String) getToWorld.invoke(event);
if (settings.shouldBypass(fromWorld, toWorld)) {
((Cancellable) event).setCancelled(true);
}
} catch (IllegalAccessException | InvocationTargetException exception) {
plugin.getLogger().log(Level.SEVERE, "Could not process a Multiverse world change.", exception);
}
},
plugin,
false);
}
private void registerGameModeChangeHook(PluginManager manager, ClassLoader classLoader)
throws ReflectiveOperationException {
Class<? extends Event> eventClass = loadEventClass(GAME_MODE_CHANGE_EVENT, classLoader);
registerCurrentWorldHook(
manager,
eventClass,
event -> settings.shouldBypassGameMode(event.getWorld().getName()),
"game-mode");
}
private void registerSessionHook(PluginManager manager, ClassLoader classLoader, String eventClassName)
throws ReflectiveOperationException {
Class<? extends Event> eventClass = loadEventClass(eventClassName, classLoader);
registerCurrentWorldHook(
manager,
eventClass,
player -> settings.shouldBypassSession(player.getWorld().getName()),
"session");
}
private void registerCurrentWorldHook(
PluginManager manager,
Class<? extends Event> eventClass,
PlayerRule rule,
String handlingName)
throws NoSuchMethodException {
Method getPlayer = eventClass.getMethod("getPlayer");
manager.registerEvent(
eventClass,
listener,
EventPriority.HIGHEST,
(ignored, event) -> {
try {
Player player = (Player) getPlayer.invoke(event);
if (rule.shouldBypass(player)) {
((Cancellable) event).setCancelled(true);
}
} catch (IllegalAccessException | InvocationTargetException exception) {
plugin.getLogger().log(
Level.SEVERE,
"Could not process Multiverse " + handlingName + " handling.",
exception);
}
},
plugin,
false);
}
private static Class<? extends Event> loadEventClass(String className, ClassLoader classLoader)
throws ClassNotFoundException {
return Class.forName(className, true, classLoader).asSubclass(Event.class);
}
@FunctionalInterface
private interface PlayerRule {
boolean shouldBypass(Player player);
}
}

View File

@@ -0,0 +1,18 @@
# Worlds listed here bypass Multiverse-Inventories share handling.
# World names are matched case-insensitively.
bypass-worlds:
- AutoMine
# When a bypass world should make a world change keep the current inventory:
# ENTERING - only when the destination is listed
# LEAVING - only when the source is listed
# EITHER - when the source or destination is listed
bypass-direction: EITHER
# If Multiverse-Inventories separates data by game mode, also prevent a
# game-mode inventory swap while a player is inside a bypass world.
bypass-gamemode-handling: true
# Prevent Multiverse-Inventories from loading or saving one of its profiles
# when a player joins or leaves the server inside a bypass world.
bypass-session-handling: true

View File

@@ -0,0 +1,23 @@
name: AntiInventories
version: '1.0.0'
main: dev.antiinventories.AntiInventoriesPlugin
description: Keeps the current inventory when players cross selected world borders.
api-version: '1.21.11'
author: AntiInventories
depend:
- Multiverse-Inventories
commands:
antiinventories:
description: Manage worlds that bypass Multiverse-Inventories.
usage: /antiinventories <add|remove|list|reload> [world]
aliases:
- antiinv
- ai
permission: antiinventories.admin
permissions:
antiinventories.admin:
description: Allows management of AntiInventories.
default: op

View File

@@ -0,0 +1,58 @@
package dev.antiinventories;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import org.junit.jupiter.api.Test;
class BypassSettingsTest {
@Test
void matchesWorldNamesCaseInsensitively() {
BypassSettings settings = settings(BypassDirection.EITHER, "AutoMine");
assertTrue(settings.contains("automine"));
assertTrue(settings.contains("AUTOMINE"));
assertFalse(settings.contains("Wild_Forest1"));
}
@Test
void eitherDirectionBypassesEnteringAndLeaving() {
BypassSettings settings = settings(BypassDirection.EITHER, "AutoMine");
assertTrue(settings.shouldBypass("Wild_Forest1", "AutoMine"));
assertTrue(settings.shouldBypass("AutoMine", "Wild_Forest1"));
assertFalse(settings.shouldBypass("Wild_Forest1", "Lobby"));
}
@Test
void enteringDirectionOnlyChecksDestination() {
BypassSettings settings = settings(BypassDirection.ENTERING, "AutoMine");
assertTrue(settings.shouldBypass("Wild_Forest1", "AutoMine"));
assertFalse(settings.shouldBypass("AutoMine", "Wild_Forest1"));
}
@Test
void leavingDirectionOnlyChecksSource() {
BypassSettings settings = settings(BypassDirection.LEAVING, "AutoMine");
assertFalse(settings.shouldBypass("Wild_Forest1", "AutoMine"));
assertTrue(settings.shouldBypass("AutoMine", "Wild_Forest1"));
}
@Test
void invalidDirectionFallsBackToEither() {
BypassSettings settings = new BypassSettings();
settings.load(List.of("AutoMine"), "invalid", true, true);
assertTrue(settings.shouldBypass("Wild_Forest1", "AutoMine"));
assertTrue(settings.shouldBypass("AutoMine", "Wild_Forest1"));
}
private static BypassSettings settings(BypassDirection direction, String... worlds) {
BypassSettings settings = new BypassSettings();
settings.load(List.of(worlds), direction.name(), true, true);
return settings;
}
}