From ed0faea0663b53be30f1a7121011c3ce637bf59d Mon Sep 17 00:00:00 2001 From: Martin Sulikowski Date: Sun, 15 Feb 2026 20:22:01 +0100 Subject: [PATCH 1/3] fix: block namespaced commands during combat and add regression test --- .../eternalcombat-java-unit-test.gradle.kts | 1 + eternalcombat-plugin/build.gradle.kts | 4 + .../com/eternalcode/combat/CombatPlugin.java | 2 + .../FightActionBlockerController.java | 32 ----- .../controller/FightCommandController.java | 113 ++++++++++++++++++ .../FightCommandControllerTest.java | 38 ++++++ 6 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightCommandController.java create mode 100644 eternalcombat-plugin/test/com/eternalcode/combat/fight/controller/FightCommandControllerTest.java diff --git a/buildSrc/src/main/kotlin/eternalcombat-java-unit-test.gradle.kts b/buildSrc/src/main/kotlin/eternalcombat-java-unit-test.gradle.kts index ce8752ae..af6ae82c 100644 --- a/buildSrc/src/main/kotlin/eternalcombat-java-unit-test.gradle.kts +++ b/buildSrc/src/main/kotlin/eternalcombat-java-unit-test.gradle.kts @@ -7,6 +7,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:${Versions.JUNIT_JUPITER_API}") testImplementation("org.junit.jupiter:junit-jupiter-params:${Versions.JUNIT_JUPITER_PARAMS}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${Versions.JUNIT_JUPITER_ENGINE}") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:6.0.2") } tasks.test { diff --git a/eternalcombat-plugin/build.gradle.kts b/eternalcombat-plugin/build.gradle.kts index fb3a8d48..49d22ab8 100644 --- a/eternalcombat-plugin/build.gradle.kts +++ b/eternalcombat-plugin/build.gradle.kts @@ -4,6 +4,7 @@ import org.gradle.kotlin.dsl.shadowJar plugins { `eternalcombat-java` + `eternalcombat-java-unit-test` `eternalcombat-repositories` id("net.minecrell.plugin-yml.bukkit") @@ -70,6 +71,9 @@ dependencies { implementation("com.eternalcode:multification-okaeri:${Versions.MULTIFICATION}") compileOnly("com.github.retrooper:packetevents-spigot:${Versions.PACKETS_EVENTS}") implementation("io.papermc:paperlib:${Versions.PAPERLIB}") + + testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.0.0") + testImplementation("org.mockito:mockito-core:5.20.0") } bukkit { diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java index 7872ac47..6deceace 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java @@ -11,6 +11,7 @@ import com.eternalcode.combat.fight.controller.FightBypassAdminController; import com.eternalcode.combat.fight.controller.FightBypassCreativeController; import com.eternalcode.combat.fight.controller.FightBypassPermissionController; +import com.eternalcode.combat.fight.controller.FightCommandController; import com.eternalcode.combat.fight.controller.FightInventoryController; import com.eternalcode.combat.fight.drop.DropKeepInventoryService; import com.eternalcode.combat.fight.FightManager; @@ -180,6 +181,7 @@ public void onEnable() { new FightBypassPermissionController(server, pluginConfig), new FightBypassCreativeController(server, pluginConfig), new FightActionBlockerController(this.fightManager, noticeService, pluginConfig, server), + new FightCommandController(this.fightManager, noticeService, pluginConfig), new FightPearlController(pluginConfig.pearl, noticeService, this.fightManager, this.fightPearlService), new UpdaterNotificationController(updaterService, pluginConfig, this.audienceProvider, miniMessage), new KnockbackRegionController(noticeService, this.regionProvider, this.fightManager, knockbackService, server), diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java index cd193717..aa6bb2ec 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java @@ -1,7 +1,6 @@ package com.eternalcode.combat.fight.controller; import com.eternalcode.combat.config.implementation.PluginConfig; -import com.eternalcode.combat.WhitelistBlacklistMode; import com.eternalcode.combat.config.implementation.BlockPlacementSettings; import com.eternalcode.combat.fight.FightManager; import com.eternalcode.combat.fight.event.FightUntagEvent; @@ -16,15 +15,11 @@ import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityToggleGlideEvent; -import org.bukkit.event.inventory.InventoryOpenEvent; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerToggleFlightEvent; import java.util.List; import java.util.UUID; -import org.bukkit.util.StringUtil; public class FightActionBlockerController implements Listener { @@ -186,31 +181,4 @@ void onDamage(EntityDamageEvent event) { } } - @EventHandler - void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { - Player player = event.getPlayer(); - UUID playerUniqueId = player.getUniqueId(); - - if (!this.fightManager.isInCombat(playerUniqueId)) { - return; - } - - String command = event.getMessage().substring(1); - - boolean isAnyMatch = this.config.commands.restrictedCommands.stream() - .anyMatch(restrictedCommand -> StringUtil.startsWithIgnoreCase(command, restrictedCommand)); - - WhitelistBlacklistMode mode = this.config.commands.commandRestrictionMode; - - boolean shouldCancel = mode.shouldBlock(isAnyMatch); - - if (shouldCancel) { - event.setCancelled(true); - this.noticeService.create() - .player(playerUniqueId) - .notice(this.config.messagesSettings.commandDisabledDuringCombat) - .send(); - - } - } } diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightCommandController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightCommandController.java new file mode 100644 index 00000000..3ee01f6c --- /dev/null +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightCommandController.java @@ -0,0 +1,113 @@ +package com.eternalcode.combat.fight.controller; + +import com.eternalcode.combat.WhitelistBlacklistMode; +import com.eternalcode.combat.config.implementation.PluginConfig; +import com.eternalcode.combat.fight.FightManager; +import com.eternalcode.combat.notification.NoticeService; +import java.util.List; +import java.util.UUID; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; + +public class FightCommandController implements Listener { + + private static final char SLASH = '/'; + private static final char NAMESPACE_SEPARATOR = ':'; + private static final char SPACE = ' '; + private static final String EMPTY = ""; + private static final String WHITESPACE_REGEX = "\\s+"; + private static final String SINGLE_SPACE = " "; + + private final FightManager fightManager; + private final NoticeService noticeService; + private final PluginConfig config; + + public FightCommandController(FightManager fightManager, NoticeService noticeService, PluginConfig config) { + this.fightManager = fightManager; + this.noticeService = noticeService; + this.config = config; + } + + static String normalizeCommand(String command) { + if (command == null || command.isBlank()) { + return EMPTY; + } + + String normalized = command.strip(); + + if (normalized.charAt(0) == SLASH) { + normalized = normalized.substring(1).stripLeading(); + } + + if (normalized.isEmpty()) { + return EMPTY; + } + + int colonIndex = normalized.indexOf(NAMESPACE_SEPARATOR); + int firstSpaceIndex = normalized.indexOf(SPACE); + + if (colonIndex >= 0 && (firstSpaceIndex < 0 || colonIndex < firstSpaceIndex)) { + normalized = normalized.substring(colonIndex + 1); + } + + return normalized.replaceAll(WHITESPACE_REGEX, SINGLE_SPACE); + } + + static boolean matches(String command, List restrictedCommands) { + if (command.isEmpty()) { + return false; + } + + for (String restrictedCommand : restrictedCommands) { + String normalizedRestricted = normalizeCommand(restrictedCommand); + + if (normalizedRestricted.isEmpty()) { + continue; + } + + boolean exactMatch = command.equalsIgnoreCase(normalizedRestricted); + boolean prefixMatch = command.regionMatches(true, 0, normalizedRestricted, 0, normalizedRestricted.length()) + && command.length() > normalizedRestricted.length() + && command.charAt(normalizedRestricted.length()) == SPACE; + + if (exactMatch || prefixMatch) { + return true; + } + } + + return false; + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { + Player player = event.getPlayer(); + UUID uniqueId = player.getUniqueId(); + + if (!this.fightManager.isInCombat(uniqueId)) { + return; + } + + String command = normalizeCommand(event.getMessage()); + + if (command.isEmpty()) { + return; + } + + boolean matched = matches(command, this.config.commands.restrictedCommands); + + WhitelistBlacklistMode mode = this.config.commands.commandRestrictionMode; + + if (!mode.shouldBlock(matched)) { + return; + } + + event.setCancelled(true); + this.noticeService.create() + .player(uniqueId) + .notice(this.config.messagesSettings.commandDisabledDuringCombat) + .send(); + } +} diff --git a/eternalcombat-plugin/test/com/eternalcode/combat/fight/controller/FightCommandControllerTest.java b/eternalcombat-plugin/test/com/eternalcode/combat/fight/controller/FightCommandControllerTest.java new file mode 100644 index 00000000..3bcc4425 --- /dev/null +++ b/eternalcombat-plugin/test/com/eternalcode/combat/fight/controller/FightCommandControllerTest.java @@ -0,0 +1,38 @@ +package com.eternalcode.combat.fight.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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 FightCommandControllerTest { + + @Test + void normalizeCommand_stripsSlashAndNamespace_collapsesWhitespace() { + assertEquals("tp Steve", FightCommandController.normalizeCommand("/minecraft:tp Steve")); + assertEquals("home set 1", FightCommandController.normalizeCommand("/home set 1")); + assertEquals("spawn", FightCommandController.normalizeCommand("/spawn")); + assertEquals("", FightCommandController.normalizeCommand("/ ")); + assertEquals("", FightCommandController.normalizeCommand(null)); + } + + @Test + void matches_exactAndPrefixMatch_caseInsensitive() { + List restricted = List.of("home set", "tp", " ", "/minecraft:msg admin"); + + assertTrue(FightCommandController.matches("home set", restricted)); + assertTrue(FightCommandController.matches("home set 1", restricted)); + + assertTrue(FightCommandController.matches("TP", restricted)); + assertTrue(FightCommandController.matches("tp Steve", restricted)); + + assertTrue(FightCommandController.matches("msg admin", restricted)); + assertTrue(FightCommandController.matches("msg admin hello", restricted)); + + assertFalse(FightCommandController.matches("home setup", restricted)); + assertFalse(FightCommandController.matches("tpper", restricted)); + assertFalse(FightCommandController.matches("", restricted)); + } +} From 5aa8d0a07c78e1fddab3e2635ea5610b7a33a4b9 Mon Sep 17 00:00:00 2001 From: Martin Sulikowski Date: Sun, 15 Feb 2026 20:25:00 +0100 Subject: [PATCH 2/3] Fix. --- eternalcombat-plugin/build.gradle.kts | 3 --- 1 file changed, 3 deletions(-) diff --git a/eternalcombat-plugin/build.gradle.kts b/eternalcombat-plugin/build.gradle.kts index 49d22ab8..3ef7a46a 100644 --- a/eternalcombat-plugin/build.gradle.kts +++ b/eternalcombat-plugin/build.gradle.kts @@ -71,9 +71,6 @@ dependencies { implementation("com.eternalcode:multification-okaeri:${Versions.MULTIFICATION}") compileOnly("com.github.retrooper:packetevents-spigot:${Versions.PACKETS_EVENTS}") implementation("io.papermc:paperlib:${Versions.PAPERLIB}") - - testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.0.0") - testImplementation("org.mockito:mockito-core:5.20.0") } bukkit { From 68a6ce23503156fe43db04f6635b7280443e7f15 Mon Sep 17 00:00:00 2001 From: Martin Sulikowski Date: Sun, 15 Feb 2026 20:26:47 +0100 Subject: [PATCH 3/3] Fix. --- .../combat/fight/controller/FightCommandController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightCommandController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightCommandController.java index 3ee01f6c..63da1949 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightCommandController.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightCommandController.java @@ -6,6 +6,7 @@ import com.eternalcode.combat.notification.NoticeService; import java.util.List; import java.util.UUID; +import java.util.regex.Pattern; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -18,8 +19,8 @@ public class FightCommandController implements Listener { private static final char NAMESPACE_SEPARATOR = ':'; private static final char SPACE = ' '; private static final String EMPTY = ""; - private static final String WHITESPACE_REGEX = "\\s+"; private static final String SINGLE_SPACE = " "; + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); private final FightManager fightManager; private final NoticeService noticeService; @@ -53,7 +54,7 @@ static String normalizeCommand(String command) { normalized = normalized.substring(colonIndex + 1); } - return normalized.replaceAll(WHITESPACE_REGEX, SINGLE_SPACE); + return WHITESPACE_PATTERN.matcher(normalized).replaceAll(SINGLE_SPACE); } static boolean matches(String command, List restrictedCommands) {