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
renovate[bot] fb6db7a008 dependency: Update Gradle to v9.6.0 (#397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 14:14:16 +02:00
renovate[bot] 7d8a01b7d7 dependency: Update Versions.LITE_COMMANDS to v3.11.0 (#398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 14:13:50 +02:00
renovate[bot] 08c7482cee dependency: Update actions/checkout action to v7 (#399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 14:12:36 +02:00
22 changed files with 315 additions and 41 deletions

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
contents: read
if: (github.event_name == 'push' && !contains(github.event.head_commit.message, '[ci skip]')) || github.event_name == 'release'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up JDK 21
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
contents: read
if: (github.event_name == 'push' && !contains(github.event.head_commit.message, '[ci skip]')) || github.event_name == 'release'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up JDK 21
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
environment: deployment
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up JDK
uses: actions/setup-java@v5
+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")
}
+7 -7
View File
@@ -1,19 +1,19 @@
object Versions {
// Keep this version for best compatibility.
const val PAPER_API = "1.20.4-R0.1-SNAPSHOT"
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.10.9"
const val LITE_COMMANDS = "3.11.0"
const val OKAERI_CONFIGS_SERDES_COMMONS = "5.0.13"
const val OKAERI_CONFIGS_SERDES_BUKKIT = "5.0.13"
@@ -24,7 +24,7 @@ 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"
const val WORLDEDIT = "3ISh7ADm" //cannot use numeric version bc of duplicated version on modrinth
const val PACKETEVENTS = "2.11.1"
@@ -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.
*/
+1 -1
View File
@@ -71,7 +71,7 @@ dependencies {
paper {
main = "com.eternalcode.combat.CombatPlugin"
authors = listOf("EternalCodeTeam")
apiVersion = "1.20.4"
apiVersion = "1.19"
prefix = "EternalCombat"
name = "EternalCombat"
generateLibrariesJson = true
@@ -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);
}
}
@@ -14,7 +14,6 @@ import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityMountEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.vehicle.VehicleMoveEvent;
@@ -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.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Vendored
+2 -2
View File
@@ -20,7 +20,7 @@
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
Vendored
+2 -2
View File
@@ -19,7 +19,7 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2
@rem Execute Gradle
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel