Intial commit
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
target/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
|
||||||
70
README.md
Normal file
70
README.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# AntiInventories
|
||||||
|
|
||||||
|
AntiInventories is a small Paper 1.21.11 add-on for Multiverse-Inventories 5.
|
||||||
|
It lets selected worlds keep the inventory a player already has instead of
|
||||||
|
loading a different Multiverse-Inventories profile.
|
||||||
|
|
||||||
|
For example, with `AutoMine` configured:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Wild_Forest1 -> AutoMine
|
||||||
|
```
|
||||||
|
|
||||||
|
The player arrives in `AutoMine` with the exact inventory they had in
|
||||||
|
`Wild_Forest1`. Login, logout, and optional game-mode profile handling are also
|
||||||
|
bypassed while the player is in a configured world, so reconnecting does not
|
||||||
|
silently replace that inventory.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Paper 1.21.11
|
||||||
|
- Java 21 or newer
|
||||||
|
- Multiverse-Core 5
|
||||||
|
- Multiverse-Inventories 5.3.1 or newer
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
The finished plugin is `target/AntiInventories-1.0.0.jar`.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Back up the Multiverse-Inventories data directory.
|
||||||
|
2. Put `AntiInventories-1.0.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.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `/antiinv add <world>` | Add a bypass world |
|
||||||
|
| `/antiinv remove <world>` | Remove a bypass world |
|
||||||
|
| `/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.
|
||||||
|
|
||||||
|
## Direction setting
|
||||||
|
|
||||||
|
`bypass-direction` controls which crossings are ignored:
|
||||||
|
|
||||||
|
- `ENTERING`: keep the current inventory only when entering a bypass world.
|
||||||
|
- `LEAVING`: keep it only when leaving a bypass world.
|
||||||
|
- `EITHER`: keep it when either side is a bypass world.
|
||||||
|
|
||||||
|
`EITHER` is the default. Be aware that it intentionally lets a bypass world act
|
||||||
|
as a bridge between otherwise separated inventory groups. Use `ENTERING` if
|
||||||
|
players should carry items into `AutoMine` but should load the destination
|
||||||
|
profile when they leave it.
|
||||||
|
|
||||||
|
The plugin cancels Multiverse-Inventories' official share-handling event. It
|
||||||
|
does not duplicate, serialize, clear, or restore item stacks itself. Because
|
||||||
|
the whole Multiverse-Inventories transaction is skipped, other profile values
|
||||||
|
such as experience, health, and hunger also remain unchanged on a bypassed
|
||||||
|
crossing.
|
||||||
60
pom.xml
Normal file
60
pom.xml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>dev.antiinventories</groupId>
|
||||||
|
<artifactId>anti-inventories</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>AntiInventories</name>
|
||||||
|
<description>Bypass Multiverse-Inventories for selected worlds.</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.release>21</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
|
||||||
|
<junit.version>5.14.1</junit.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>papermc</id>
|
||||||
|
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.papermc.paper</groupId>
|
||||||
|
<artifactId>paper-api</artifactId>
|
||||||
|
<version>${paper.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>AntiInventories-${project.version}</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.14.1</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.5.4</version>
|
||||||
|
<configuration>
|
||||||
|
<useModulePath>false</useModulePath>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
146
src/main/java/dev/antiinventories/AntiInventoriesCommand.java
Normal file
146
src/main/java/dev/antiinventories/AntiInventoriesCommand.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/main/java/dev/antiinventories/AntiInventoriesPlugin.java
Normal file
53
src/main/java/dev/antiinventories/AntiInventoriesPlugin.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main/java/dev/antiinventories/BypassDirection.java
Normal file
30
src/main/java/dev/antiinventories/BypassDirection.java
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
70
src/main/java/dev/antiinventories/BypassSettings.java
Normal file
70
src/main/java/dev/antiinventories/BypassSettings.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/main/java/dev/antiinventories/MultiverseInventoriesHook.java
Normal file
139
src/main/java/dev/antiinventories/MultiverseInventoriesHook.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/resources/config.yml
Normal file
18
src/main/resources/config.yml
Normal 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
|
||||||
23
src/main/resources/plugin.yml
Normal file
23
src/main/resources/plugin.yml
Normal 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
|
||||||
|
|
||||||
58
src/test/java/dev/antiinventories/BypassSettingsTest.java
Normal file
58
src/test/java/dev/antiinventories/BypassSettingsTest.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user