Compare commits

..
Author SHA1 Message Date
Martin Sulikowski bc11f46275 Respect ignored worlds for void combat 2026-08-17 21:32:18 +02:00
Martin Sulikowski 35e19d0eed Keep combat tag active below void height to prevent evasion 2026-08-17 21:24:24 +02:00
renovate[bot] b2776936ed dependency: Update junit-framework monorepo to v6.1.3 (#418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 20:58:09 +02:00
Martin Sulikowski d2aa5dfa84 GH-395 Add combat sign editing blocker (#395)
* Add combat sign editing blocker

* Fix.
2026-08-17 20:57:17 +02:00
renovate[bot] 5c88af692d dependency: Update Gradle to v9.7.0 (#419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 20:50:56 +02:00
renovate[bot] 390195edb2 dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.6.1 (#413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 10:11:56 +02:00
renovate[bot] 8808331f0a dependency: Update junit-framework monorepo to v6.1.2 (#411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14 17:14:08 +02:00
renovate[bot] 99b3a5aa27 dependency: Update dependency org.jetbrains.kotlin:kotlin-stdlib-jdk8 to v2.4.10 (#412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14 17:14:00 +02:00
renovate[bot] 9d1716d9db dependency: Update Gradle to v9.6.1 (#405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:52:33 +02:00
renovate[bot] 7b94ab4b7a dependency: Update Versions.PACKETS_EVENTS (#402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:52:13 +02:00
renovate[bot] daee189865 dependency: Update dependency me.clip:placeholderapi to v2.12.3 (#407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:51:59 +02:00
renovate[bot] 1aef1e6f1d dependency: Update junit-framework monorepo to v6.1.1 (#408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:51:49 +02:00
renovate[bot] 788a2a668c dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.5.1 (#409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:51:41 +02:00
Nico SendanandMartin Sulikowski 93d7ffdd0f GH-400 Resolve EntityMountEvent registration failure on modern Paper (#400)
* Fix: Resolve EntityMountEvent registration failure on modern Paper

Newer Paper builds (e.g. 26.1.2) removed org.spigotmc.event.entity.EntityMountEvent, causing KnockbackRegionController registration to fail and region knockback protection to be disabled.

Resolve EntityMountEvent at runtime (checking Bukkit, Paper, and Spigot packages) and register it reflectively while preserving the 1.19.3 compile target for compatibility.

* Optimize dynamic mount event handling

---------

Co-authored-by: Martin Sulikowski <vluckyyy.biznes@gmail.com>
2026-06-21 15:30:55 +02:00
16 changed files with 312 additions and 40 deletions

No files matched your search

+2 -2
View File
@@ -8,10 +8,10 @@ repositories {
}
dependencies {
implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.4.2")
implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.6.1")
implementation("com.modrinth.minotaur:com.modrinth.minotaur.gradle.plugin:2.9.0")
implementation("io.papermc.hangar-publish-plugin:io.papermc.hangar-publish-plugin.gradle.plugin:0.1.4")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.4.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.4.10")
implementation("net.minecrell:plugin-yml:0.6.0")
implementation("xyz.jpenilla:run-task:3.0.2")
}
+9 -11
View File
@@ -3,15 +3,15 @@ object Versions {
// Keep this version for best compatibility.
const val PAPER_API = "1.19.3-R0.1-SNAPSHOT"
const val JUNIT_JUPITER_API = "6.1.0"
const val JUNIT_JUPITER_PARAMS = "6.1.0"
const val JUNIT_JUPITER_ENGINE = "6.1.0"
const val JUNIT_JUPITER_API = "6.1.3"
const val JUNIT_JUPITER_PARAMS = "6.1.3"
const val JUNIT_JUPITER_ENGINE = "6.1.3"
const val JETBRAINS_ANNOTATIONS = "26.1.0"
const val ETERNALCODE_COMMONS = "1.4.1"
const val MULTIFICATION = "1.2.5-SNAPSHOT"
const val PACKETS_EVENTS = "2.12.2"
const val PACKETS_EVENTS = "2.13.0"
const val LITE_COMMANDS = "3.11.0"
const val OKAERI_CONFIGS_SERDES_COMMONS = "5.0.13"
@@ -24,14 +24,12 @@ object Versions {
const val B_STATS_BUKKIT = "3.2.1"
const val WORLD_GUARD_BUKKIT = "7.0.17"
const val PLACEHOLDER_API = "2.12.2"
const val PLACEHOLDER_API = "2.12.3"
const val LANDS_API = "7.25.4"
//runServer task
const val FAST_ASYNC_WORLD_EDIT = "2.15.2"
const val PACKETEVENTS = "2.12.2+spigot"
const val WORLDGUARD = "7.0.17"
const val LUCKPERMS = "v5.5.53-bukkit"
const val WORLDEDIT = "3ISh7ADm" //cannot use numeric version bc of duplicated version on modrinth
const val PACKETEVENTS = "2.11.1"
const val WORLDGUARD = "7.0.15-beta-01"
const val LUCKPERMS = "5.5.17"
}
@@ -14,9 +14,9 @@ tasks {
})
minecraftVersion("26.1.2")
downloadPlugins.modrinth("FastAsyncWorldEdit", Versions.FAST_ASYNC_WORLD_EDIT)
downloadPlugins.modrinth("PacketEvents", Versions.PACKETEVENTS)
downloadPlugins.modrinth("WorldGuard", Versions.WORLDGUARD)
downloadPlugins.modrinth("LuckPerms", Versions.LUCKPERMS)
downloadPlugins.modrinth("FastAsyncWorldEdit", "2.15.2")
downloadPlugins.modrinth("PacketEvents", "2.12.2+spigot")
downloadPlugins.modrinth("WorldGuard", "7.0.17")
downloadPlugins.modrinth("LuckPerms", "v5.5.53-bukkit")
}
}
@@ -34,6 +34,11 @@ public enum CauseOfTag {
ENDER_PEARL,
/**
* The player's combat tag was kept active because they were below the void combat height.
*/
VOID,
/**
* A custom cause, typically defined by external plugins or systems, applied the combat tag.
*/
@@ -36,10 +36,12 @@ import com.eternalcode.combat.fight.drop.DropServiceImpl;
import com.eternalcode.combat.fight.drop.impl.PercentDropModifier;
import com.eternalcode.combat.fight.drop.impl.PlayersHealthDropModifier;
import com.eternalcode.combat.fight.blocker.PlaceBlockBlocker;
import com.eternalcode.combat.fight.blocker.SignEditingBlocker;
import com.eternalcode.combat.fight.effect.FightEffectController;
import com.eternalcode.combat.fight.effect.FightEffectService;
import com.eternalcode.combat.fight.effect.FightEffectServiceImpl;
import com.eternalcode.combat.fight.firework.FireworkController;
import com.eternalcode.combat.fight.knockback.KnockbackMountController;
import com.eternalcode.combat.fight.knockback.KnockbackRegionController;
import com.eternalcode.combat.fight.knockback.KnockbackService;
import com.eternalcode.combat.fight.logout.LogoutController;
@@ -204,6 +206,7 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
new RespawnAnchorListener(this, this.fightManager, pluginConfig),
new FireworkController(this.fightManager, pluginConfig, noticeService),
new InventoryContainersBlocker(this.fightManager, pluginConfig, noticeService),
new SignEditingBlocker(this.fightManager, pluginConfig),
new CommandsBlocker(this.fightManager, noticeService, pluginConfig),
new ElytraBlocker(this.fightManager, pluginConfig),
new ElytraEquipBlocker(this.fightManager, noticeService, pluginConfig, server),
@@ -211,6 +214,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
new PlaceBlockBlocker(this.fightManager, noticeService, pluginConfig)
);
new KnockbackMountController(noticeService, this.regionProvider, this.fightManager).register(this);
eventManager.subscribe(
PlayerDeathEvent.class,
pluginConfig.drop.dropEventPriority,
@@ -90,6 +90,19 @@ public class CombatSettings extends OkaeriConfig {
})
public EventPriority quitPunishmentEventPriority = EventPriority.NORMAL;
@Comment({
"# Keep a player's combat tag continuously active while they are below 'voidCombatHeight'.",
"# This prevents players from evading combat by falling into the void (e.g. in the End).",
"# Set to 'true' to keep resetting the combat timer to its full duration while below that height."
})
public boolean keepCombatActiveInVoid = false;
@Comment({
"# The Y height below which the combat timer is kept active (see 'keepCombatActiveInVoid').",
"# Default: 0, matching the void threshold in worlds such as the End."
})
public double voidCombatHeight = 0;
@Comment({
"# List of kick reasons where players will NOT be punished for combat logging.",
"# If this list is empty, players are ALWAYS punished when kicked during combat.",
@@ -127,6 +127,13 @@ public class PluginConfig extends OkaeriConfig {
})
public InventorySettings inventory = new InventorySettings();
@Comment({
" ",
"# Settings related to sign editing during combat.",
"# Created for sign traps where opening the sign editor can block ender pearl throws."
})
public SignEditingSettings signEditing = new SignEditingSettings();
@Comment({
" ",
"# Settings related to placeholders used in the plugin.",
@@ -0,0 +1,13 @@
package com.eternalcode.combat.config.implementation;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
public class SignEditingSettings extends OkaeriConfig {
@Comment({
"# Prevent players from editing signs during combat.",
"# Disabled by default; created for sign traps where sign editing blocks ender pearl throws."
})
public boolean disableSignEditingDuringCombat = false;
}
@@ -1,7 +1,9 @@
package com.eternalcode.combat.fight;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.combat.fight.event.CauseOfUnTag;
import com.eternalcode.combat.fight.event.FightTagEvent;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import java.util.Optional;
@@ -41,6 +43,16 @@ public class FightTask implements Runnable {
return;
}
if (this.config.combat.keepCombatActiveInVoid
&& player.getLocation().getY() < this.config.combat.voidCombatHeight
&& !this.config.settings.ignoredWorlds.contains(player.getWorld().getName())) {
FightTagEvent tagEvent = this.fightManager.tag(playerUniqueId, this.config.settings.combatTimerDuration, CauseOfTag.VOID, fightTag.getTagger());
if (!tagEvent.isCancelled()) {
fightTag = this.fightManager.getTag(playerUniqueId);
}
}
Duration remaining = fightTag.getRemainingDuration();
String opponent = Optional.ofNullable(fightTag.getTagger())
@@ -0,0 +1,63 @@
package com.eternalcode.combat.fight.blocker;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Event.Result;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
public class SignEditingBlocker implements Listener {
private final FightManager fightManager;
private final PluginConfig config;
public SignEditingBlocker(FightManager fightManager, PluginConfig config) {
this.fightManager = fightManager;
this.config = config;
}
@EventHandler(ignoreCancelled = true)
void onInteract(PlayerInteractEvent event) {
if (!this.config.signEditing.disableSignEditingDuringCombat) {
return;
}
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return;
}
Block clickedBlock = event.getClickedBlock();
if (clickedBlock == null || !isSign(clickedBlock.getType())) {
return;
}
Player player = event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
event.setUseInteractedBlock(Result.DENY);
ItemStack item = event.getItem();
if (item != null && isEnderPearl(item.getType()) && event.useItemInHand() != Result.DENY) {
event.setUseItemInHand(Result.ALLOW);
}
}
static boolean isSign(Material material) {
return material.name().endsWith("_SIGN");
}
static boolean isEnderPearl(Material material) {
return material == Material.ENDER_PEARL;
}
}
@@ -0,0 +1,129 @@
package com.eternalcode.combat.fight.knockback;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.region.RegionProvider;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import org.bukkit.entity.Entity;
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.event.entity.EntityEvent;
import org.bukkit.plugin.Plugin;
// EntityMountEvent moved between org.spigotmc, io.papermc.paper and org.bukkit across versions,
// so we resolve it at runtime to stay compatible without bumping the compile API.
public final class KnockbackMountController implements Listener {
private static final String[] EVENT_CLASS_CANDIDATES = {
"org.bukkit.event.entity.EntityMountEvent",
"io.papermc.paper.event.entity.EntityMountEvent",
"org.spigotmc.event.entity.EntityMountEvent",
};
private final NoticeService noticeService;
private final RegionProvider regionProvider;
private final FightManager fightManager;
private final AtomicBoolean mountFailureLogged = new AtomicBoolean();
public KnockbackMountController(NoticeService noticeService, RegionProvider regionProvider, FightManager fightManager) {
this.noticeService = noticeService;
this.regionProvider = regionProvider;
this.fightManager = fightManager;
}
public void register(Plugin plugin) {
Class<? extends Event> eventClass = null;
for (String candidate : EVENT_CLASS_CANDIDATES) {
try {
eventClass = Class.forName(candidate).asSubclass(Event.class);
break;
} catch (ClassNotFoundException | ClassCastException ignored) {
}
}
if (eventClass == null) {
plugin.getLogger().warning("Could not find any EntityMountEvent class; mount protection in regions is disabled.");
return;
}
MethodHandle getMountHandle;
try {
getMountHandle = this.resolveGetMountHandle(eventClass);
} catch (NoSuchMethodException | IllegalAccessException exception) {
plugin.getLogger().log(Level.WARNING, "EntityMountEvent has incompatible getMount() method; mount protection in regions is disabled.", exception);
return;
}
plugin.getServer().getPluginManager().registerEvent(
eventClass,
this,
EventPriority.HIGHEST,
(listener, event) -> this.handle(event, getMountHandle, plugin),
plugin,
true
);
}
private MethodHandle resolveGetMountHandle(Class<? extends Event> eventClass) throws NoSuchMethodException, IllegalAccessException {
if (!EntityEvent.class.isAssignableFrom(eventClass) || !Cancellable.class.isAssignableFrom(eventClass)) {
throw new NoSuchMethodException(eventClass.getName() + " is not a cancellable entity event");
}
Method getMount = eventClass.getMethod("getMount");
if (!Entity.class.isAssignableFrom(getMount.getReturnType())) {
throw new NoSuchMethodException(eventClass.getName() + "#getMount() does not return Entity");
}
return MethodHandles.publicLookup()
.unreflect(getMount)
.asType(MethodType.methodType(Entity.class, Event.class));
}
private void handle(Event event, MethodHandle getMountHandle, Plugin plugin) {
if (!(event instanceof EntityEvent entityEvent) || !(event instanceof Cancellable cancellable)) {
return;
}
if (!(entityEvent.getEntity() instanceof Player player)) {
return;
}
if (!this.fightManager.isInCombat(player.getUniqueId())) {
return;
}
Entity mount;
try {
mount = (Entity) getMountHandle.invokeExact(event);
} catch (Throwable exception) {
this.logMountFailure(plugin, exception);
return;
}
if (mount == null || !this.regionProvider.isInRegion(mount.getLocation())) {
return;
}
cancellable.setCancelled(true);
this.noticeService.create()
.player(player.getUniqueId())
.notice(config -> config.messagesSettings.cantEnterOnRegion)
.send();
}
private void logMountFailure(Plugin plugin, Throwable exception) {
if (!this.mountFailureLogged.compareAndSet(false, true)) {
return;
}
plugin.getLogger().log(Level.WARNING, "Could not read EntityMountEvent mount; mount protection in regions is skipped.", exception);
}
}
@@ -17,7 +17,6 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.vehicle.VehicleMoveEvent;
import org.spigotmc.event.entity.EntityMountEvent;
public class KnockbackRegionController implements Listener {
@@ -132,27 +131,6 @@ public class KnockbackRegionController implements Listener {
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
void onEntityMount(EntityMountEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
if (!this.fightManager.isInCombat(player.getUniqueId())) {
return;
}
if (!this.regionProvider.isInRegion(event.getMount().getLocation())) {
return;
}
event.setCancelled(true);
this.noticeService.create()
.player(player.getUniqueId())
.notice(config -> config.messagesSettings.cantEnterOnRegion)
.send();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
void onTag(FightTagEvent event) {
Player player = this.server.getPlayer(event.getPlayer());
@@ -0,0 +1,15 @@
package com.eternalcode.combat.config.implementation;
import static org.junit.jupiter.api.Assertions.assertFalse;
import org.junit.jupiter.api.Test;
class SignEditingSettingsTest {
@Test
void shouldDisableSignEditingBlockerByDefault() {
SignEditingSettings signEditingSettings = new SignEditingSettings();
assertFalse(signEditingSettings.disableSignEditingDuringCombat);
}
}
@@ -0,0 +1,34 @@
package com.eternalcode.combat.fight.blocker;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.bukkit.Material;
import org.junit.jupiter.api.Test;
class SignEditingBlockerTest {
@Test
void shouldRecognizeStandingSignMaterials() {
assertTrue(SignEditingBlocker.isSign(Material.OAK_SIGN));
assertTrue(SignEditingBlocker.isSign(Material.CRIMSON_SIGN));
}
@Test
void shouldRecognizeWallSignMaterials() {
assertTrue(SignEditingBlocker.isSign(Material.OAK_WALL_SIGN));
assertTrue(SignEditingBlocker.isSign(Material.WARPED_WALL_SIGN));
}
@Test
void shouldIgnoreNonSignMaterials() {
assertFalse(SignEditingBlocker.isSign(Material.CHEST));
assertFalse(SignEditingBlocker.isSign(Material.ENDER_PEARL));
}
@Test
void shouldRecognizeEnderPearlMaterial() {
assertTrue(SignEditingBlocker.isEnderPearl(Material.ENDER_PEARL));
assertFalse(SignEditingBlocker.isEnderPearl(Material.OAK_SIGN));
}
}
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500