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..3ef7a46a 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") 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..63da1949 --- /dev/null +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightCommandController.java @@ -0,0 +1,114 @@ +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 java.util.regex.Pattern; +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 SINGLE_SPACE = " "; + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); + + 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 WHITESPACE_PATTERN.matcher(normalized).replaceAll(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)); + } +}