Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ jobs:

steps:
- uses: actions/checkout@v6.0.2
- name: Set up JDK 21
- name: Set up JDK 25
uses: actions/setup-java@v5.2.0
with:
java-version: "21"
java-version: "25"
distribution: "microsoft"

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
uses: gradle/actions/setup-gradle@v6.1.0
with:
gradle-version: current
- name: Build with Gradle Wrapper
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,18 @@ worlds:

## Build

Requires Java 21+.
Requires Java 25+.

```bash
./gradlew build
```

Jar will be in `build/libs/AntiDupingMechanics-1.1.0.jar`.
Jar will be in `build/libs/AntiDupingMechanics-1.2.0.jar`.

## Notes

Bundle behavior in Minecraft can vary across versions/clients. This plugin blocks the common insert methods:
Bundle behaviour in Minecraft can vary across versions/clients. This plugin blocks the common insert methods:
- click/drag items onto a bundle in inventories
- certain right-click interactions involving item entities (where applicable)

If you find a specific interaction path that still inserts into bundles on your server, note the exact steps and we can tighten the checks.
If you find a specific interaction path that still inserts into bundles on your server, note the exact steps, and we can tighten the checks.
20 changes: 17 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import org.apache.tools.ant.filters.ReplaceTokens

plugins {
id 'java'
id("io.github.greymagic27.McTestServer") version "1.0.0"
}

group = 'com.mrfloris'
version = '1.1.0'
version = '26.1.2'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(25)
}
}

Expand All @@ -17,11 +20,22 @@ repositories {
}

dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT")
compileOnly("io.papermc.paper:paper-api:${version}.build.+")
implementation("org.jspecify:jspecify:1.0.0")
}

processResources {
from(sourceSets.main.resources.srcDirs) {
filter ReplaceTokens, tokens: [version: version]
duplicatesStrategy = DuplicatesStrategy.WARN
}
}

tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
options.compilerArgs += ['-Xlint:deprecation']
}

tasks.jar {
archiveVersion.set("${version}")
}
25 changes: 10 additions & 15 deletions src/main/java/com/mrfloris/antiduping/AntiDupingCommand.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.mrfloris.antiduping;

import com.mrfloris.antiduping.util.ColorUtil;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
Expand All @@ -19,37 +19,32 @@ public AntiDupingCommand(AntiDupingMechanics plugin, AntiDupingListener listener
this.listener = listener;
}

private String color(String s) {
return ChatColor.translateAlternateColorCodes('&', s);
}

@Override
public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, @NonNull String @NonNull [] args) {
if (!sender.hasPermission("antiduping.admin")) {
sender.sendMessage(color("&cNo permission."));
sender.sendMessage(ColorUtil.color("&cNo permission."));
return true;
}

if (args.length == 0) {
sender.sendMessage(color("&7Usage: &f/antiduping <reload|status>"));
sender.sendMessage(ColorUtil.color("&7Usage: &f/antiduping <reload|status>"));
return true;
}

switch (args[0].toLowerCase()) {
case "reload" -> {
plugin.reloadAll();
sender.sendMessage(color("&aAntiDupingMechanics reloaded."));
sender.sendMessage(ColorUtil.color("&aAntiDupingMechanics reloaded."));
}
case "status" -> {
sender.sendMessage(color("&6AntiDupingMechanics status:"));
sender.sendMessage(color("&7- Debug: &f" + listener.statusDebug()));
sender.sendMessage(color("&7- Worlds loaded: &f" + listener.statusWorldsLoaded()));
sender.sendMessage(color("&7- Bypass perm: &fantiduping.bypass"));
sender.sendMessage(color("&7Use /antiduping reload after config changes."));
sender.sendMessage(ColorUtil.color("&6AntiDupingMechanics status:"));
sender.sendMessage(ColorUtil.color("&7- Debug: &f" + listener.statusDebug()));
sender.sendMessage(ColorUtil.color("&7- Worlds loaded: &f" + listener.statusWorldsLoaded()));
sender.sendMessage(ColorUtil.color("&7- Bypass perm: &fantiduping.bypass"));
sender.sendMessage(ColorUtil.color("&7Use /antiduping reload after config changes."));
}
default -> sender.sendMessage(color("&7Usage: &f/antiduping <reload|status>"));
default -> sender.sendMessage(ColorUtil.color("&7Usage: &f/antiduping <reload|status>"));
}

return true;
}

Expand Down
138 changes: 13 additions & 125 deletions src/main/java/com/mrfloris/antiduping/AntiDupingListener.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.mrfloris.antiduping;

import com.mrfloris.antiduping.settings.world.WorldSettings;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.ChestBoat;
Expand Down Expand Up @@ -31,41 +34,31 @@ public final class AntiDupingListener implements Listener {
private final Map<String, WorldSettings> perWorld = new HashMap<>();
private boolean debugEnabled;
private boolean debugToConsole;
private String msgPrefix;
private TextComponent msgPrefix;
private WorldSettings defaultSettings;

public AntiDupingListener(AntiDupingMechanics plugin) {
this.plugin = plugin;
reloadFromConfig();
}

private static @NonNull String colorStatic(String s) {
return ChatColor.translateAlternateColorCodes('&', s == null ? "" : s);
}

public void reloadFromConfig() {
var c = plugin.getConfig();

debugEnabled = c.getBoolean("debug.enabled", false);
debugToConsole = c.getBoolean("debug.log_to_console", true);
msgPrefix = color(c.getString("messages.prefix", "&8[&6AntiDuping&8] &r"));

perWorld.clear();

ConfigurationSection worlds = c.getConfigurationSection("worlds");
ConfigurationSection def = worlds != null ? worlds.getConfigurationSection("__default__") : null;
defaultSettings = WorldSettings.fromConfig(def, new WorldSettings()); // baseline defaults if missing

if (worlds != null) {
for (String key : worlds.getKeys(false)) {
if (key == null) continue;
if (key.equalsIgnoreCase("__default__")) continue;

ConfigurationSection worldSec = worlds.getConfigurationSection(key);
if (worldSec == null) continue;

// merge: start from defaultSettings, then override
WorldSettings ws = defaultSettings.copy();
WorldSettings ws = Objects.requireNonNull(defaultSettings).copy();
ws.applyOverrides(worldSec);
perWorld.put(key.toLowerCase(), ws);
}
Expand All @@ -85,19 +78,20 @@ private WorldSettings settingsForWorld(String worldName) {
return perWorld.getOrDefault(worldName.toLowerCase(), defaultSettings);
}

private @NonNull String color(String s) {
return ChatColor.translateAlternateColorCodes('&', s == null ? "" : s);
private @NonNull TextComponent color(String s) {
return LegacyComponentSerializer.legacyAmpersand().deserialize(s);
}

private void msg(Player p, String message) {
if (message != null && !message.isBlank()) {
p.sendMessage(msgPrefix + message);
private void msg(Player p, TextComponent message) {
if (message != null) {
p.sendMessage(msgPrefix.append(message));
}
}

private void debug(String line) {
if (!debugEnabled) return;
if (debugToConsole) {
//noinspection UnstableApiUsage
Bukkit.getLogger().info("[AntiDupingMechanics] " + line);
}
}
Expand Down Expand Up @@ -147,10 +141,8 @@ public void onInventoryOpen(@NonNull InventoryOpenEvent event) {
public void onInteractEntity(@NonNull PlayerInteractEntityEvent event) {
Player player = event.getPlayer();
if (bypass(player)) return;

WorldSettings ws = settingsForWorld(player.getWorld().getName());
Entity clicked = event.getRightClicked();

if (ws.donkeys.enabled && ws.donkeys.blockAttachChest) {
if (clicked instanceof ChestedHorse chestedHorse) {
ItemStack inHand = player.getInventory().getItem(event.getHand());
Expand Down Expand Up @@ -182,22 +174,18 @@ public void onInteractEntity(@NonNull PlayerInteractEntityEvent event) {
public void onInventoryClick(@NonNull InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) return;
if (bypass(player)) return;

WorldSettings ws = settingsForWorld(player.getWorld().getName());
if (!ws.bundles.enabled || !ws.bundles.blockInsertItems) return;

ItemStack current = event.getCurrentItem(); // clicked slot item
ItemStack cursor = event.getCursor(); // cursor item

// Putting a cursor item onto a bundle (insert)
if (isBundle(current) && cursor.getType() != Material.AIR) {
event.setCancelled(true);
msg(player, ws.bundles.message);
debug("Blocked bundle insert via click (cursor -> bundle): player=" + player.getName() + ", world=" + player.getWorld().getName() + ", cursor=" + cursor.getType());
return;
}

// Risky swap case: bundle on cursor + clicking a non-air item (could insert depending on client behavior)
// Risky swap case: bundle on cursor + clicking a non-air item (could insert depending on client behaviour)
if (isBundle(cursor) && current != null && current.getType() != Material.AIR) {
event.setCancelled(true);
msg(player, ws.bundles.message);
Expand All @@ -209,13 +197,10 @@ public void onInventoryClick(@NonNull InventoryClickEvent event) {
public void onInventoryDrag(@NonNull InventoryDragEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) return;
if (bypass(player)) return;

WorldSettings ws = settingsForWorld(player.getWorld().getName());
if (!ws.bundles.enabled || !ws.bundles.blockInsertItems) return;

ItemStack cursor = event.getOldCursor();
if (cursor.getType() == Material.AIR) return;

for (int rawSlot : event.getRawSlots()) {
ItemStack inSlot = event.getView().getItem(rawSlot);
if (isBundle(inSlot)) {
Expand All @@ -226,101 +211,4 @@ public void onInventoryDrag(@NonNull InventoryDragEvent event) {
}
}
}

// --- Settings types ---
static final class WorldSettings {
ChestBoatSettings chestBoats = new ChestBoatSettings();
DonkeySettings donkeys = new DonkeySettings();
BundleSettings bundles = new BundleSettings();

static @NonNull WorldSettings fromConfig(ConfigurationSection sec, WorldSettings fallback) {
WorldSettings ws = (fallback == null ? new WorldSettings() : fallback.copy());
if (sec == null) return ws;
ws.applyOverrides(sec);
return ws;
}

private static @NonNull String colorLocal(String s) {
return ChatColor.translateAlternateColorCodes('&', s == null ? "" : s);
}

void applyOverrides(@NonNull ConfigurationSection sec) {
// chest_boats
ConfigurationSection cb = sec.getConfigurationSection("chest_boats");
if (cb != null) {
if (cb.contains("enabled")) chestBoats.enabled = cb.getBoolean("enabled");
if (cb.contains("block_open_inventory")) chestBoats.blockOpenInventory = cb.getBoolean("block_open_inventory");
if (cb.contains("message")) chestBoats.message = colorLocal(cb.getString("message"));
}

// donkeys
ConfigurationSection dk = sec.getConfigurationSection("donkeys");
if (dk != null) {
if (dk.contains("enabled")) donkeys.enabled = dk.getBoolean("enabled");
if (dk.contains("block_open_inventory")) donkeys.blockOpenInventory = dk.getBoolean("block_open_inventory");
if (dk.contains("block_attach_chest")) donkeys.blockAttachChest = dk.getBoolean("block_attach_chest");
if (dk.contains("message")) donkeys.message = colorLocal(dk.getString("message"));
}

// bundles
ConfigurationSection bd = sec.getConfigurationSection("bundles");
if (bd != null) {
if (bd.contains("enabled")) bundles.enabled = bd.getBoolean("enabled");
if (bd.contains("block_insert_items")) bundles.blockInsertItems = bd.getBoolean("block_insert_items");
if (bd.contains("message")) bundles.message = colorLocal(bd.getString("message"));
}
}

@NonNull WorldSettings copy() {
WorldSettings ws = new WorldSettings();
ws.chestBoats = chestBoats.copy();
ws.donkeys = donkeys.copy();
ws.bundles = bundles.copy();
return ws;
}
}

static final class ChestBoatSettings {
boolean enabled = true;
boolean blockOpenInventory = true;
String message = colorStatic("&cYou can't use chest storage in chest boats on this server.");

@NonNull ChestBoatSettings copy() {
ChestBoatSettings s = new ChestBoatSettings();
s.enabled = enabled;
s.blockOpenInventory = blockOpenInventory;
s.message = message;
return s;
}
}

static final class DonkeySettings {
boolean enabled = true;
boolean blockOpenInventory = true;
boolean blockAttachChest = true;
String message = colorStatic("&cYou can't use chest storage on pack animals on this server.");

@NonNull DonkeySettings copy() {
DonkeySettings s = new DonkeySettings();
s.enabled = enabled;
s.blockOpenInventory = blockOpenInventory;
s.blockAttachChest = blockAttachChest;
s.message = message;
return s;
}
}

static final class BundleSettings {
boolean enabled = true;
boolean blockInsertItems = true;
String message = colorStatic("&cYou can't put items into bundles on this server.");

@NonNull BundleSettings copy() {
BundleSettings s = new BundleSettings();
s.enabled = enabled;
s.blockInsertItems = blockInsertItems;
s.message = message;
return s;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ public final class AntiDupingMechanics extends JavaPlugin {
@Override
public void onEnable() {
saveDefaultConfig();

listener = new AntiDupingListener(this);
getServer().getPluginManager().registerEvents(listener, this);

var cmd = getCommand("antiduping");
if (cmd != null) {
var command = new AntiDupingCommand(this, listener);
Expand Down
Loading
Loading