Skip to content

GH-414 feat: add spear with lunge cooldown #414

Open
CitralFlo wants to merge 2 commits into
masterfrom
spear-cooldown
Open

GH-414 feat: add spear with lunge cooldown #414
CitralFlo wants to merge 2 commits into
masterfrom
spear-cooldown

Conversation

@CitralFlo

Copy link
Copy Markdown
Member

name: Pull Request
about: Submit a contribution to EternalCombat
title: 'GH-{NUMBER} <title>'
labels: []
assignees: []

📝 Description

Add cooldown for using spears with lunge attribute.
Remove static versions of plugins missed in 5645f04

🎯 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 🔧 Refactoring (no functional changes, just code improvements)
  • 🎨 Style/Cosmetic (formatting, whitespace, visual changes)
  • 📚 Documentation (updates to documentation or README)
  • 🤖 CI/CD/Build (changes to workflows, gradle, etc.)

Test Environment:
runServer,
minecraft 26.1.2

📸 Screenshots (if applicable)

…t building against it due to support for older versions

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>
@CitralFlo CitralFlo changed the title feat: add spear with lunge cooldown GH-414 feat: add spear with lunge cooldown Jul 21, 2026
@CitralFlo

Copy link
Copy Markdown
Member Author

Second time using #getMethod() so if any mistakes ware made, please make sure to leave review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a spear lunge cooldown feature, including the SpearService interface, its implementation SpearServiceImpl, a configuration class SpearSettings, and SpearLungeController which reflectively registers PaperMC's EntityLungeEvent. The review feedback focuses on preventing memory leaks by implementing a removeCooldown mechanism to clean up player data upon disconnect, optimizing map lookups in the cooldown check logic to avoid redundant operations, and ensuring proper event registration for the controller.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java Outdated
Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>
@Jakubk15

Copy link
Copy Markdown
Member

@gemini-code-assist why were the version constants removed?

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@CitralFlo

Copy link
Copy Markdown
Member Author

@gemini-code-assist why were the version constants removed?

read description pls :)

because they where left in a commit linked there

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@Jakubk15 Jakubk15 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reduced reflection usage in my suggestion + I think it would be good to add a bypass permission

Comment on lines +3 to +79
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.Plugin;

import java.lang.reflect.Method;
import java.time.Duration;
import java.util.UUID;

public class SpearLungeController implements Listener {

private final SpearService spearService;

public SpearLungeController(Plugin plugin, FightManager fightManager, SpearService spearService, SpearSettings settings, NoticeService noticeService) {
this.spearService = spearService;


Bukkit.getPluginManager().registerEvents(this, plugin);

try {
Class<? extends Event> lungeEventClass = (Class<? extends Event>) Class.forName("io.papermc.paper.event.entity.EntityLungeEvent");

Method getEntityMethod = lungeEventClass.getMethod("getEntity");
Method setCancelledMethod = lungeEventClass.getMethod("setCancelled", boolean.class);

Bukkit.getPluginManager().registerEvent(
lungeEventClass,
this,
EventPriority.NORMAL,
(listener, event) -> {
if (!settings.lungeCooldown) return;
if (!lungeEventClass.isInstance(event)) return;

try {
Object entity = getEntityMethod.invoke(event);

if (entity instanceof Player player) {
UUID uuid = player.getUniqueId();

if (settings.onlyForFight && !fightManager.isInCombat(uuid)) {
return;
}

// Query the remaining cooldown once to avoid redundant lookups and race conditions
Duration remaining = spearService.getRemainingCooldown(uuid);

if (!remaining.isZero() && !remaining.isNegative()) {
setCancelledMethod.invoke(event, true);

noticeService.create()
.player(uuid)
.notice(settings.lungeOnCooldown)
.placeholder("{TIME}", DurationUtil.format(remaining, !settings.useMillis))
.send();
} else {
spearService.saveCooldown(uuid);
}
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to handle EntityLungeEvent reflectively: " + e.getMessage());
}
},
plugin
);
} catch (ClassNotFoundException e) {
plugin.getLogger().info("EntityLungeEvent not found, skipping spear lunge cooldown registration.");
} catch (NoSuchMethodException e) {
plugin.getLogger().warning("Failed to find necessary methods for EntityLungeEvent: " + e.getMessage());
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.Plugin;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.UUID;
public class SpearLungeController implements Listener {
private final SpearService spearService;
public SpearLungeController(Plugin plugin, FightManager fightManager, SpearService spearService, SpearSettings settings, NoticeService noticeService) {
this.spearService = spearService;
Bukkit.getPluginManager().registerEvents(this, plugin);
try {
Class<? extends Event> lungeEventClass = (Class<? extends Event>) Class.forName("io.papermc.paper.event.entity.EntityLungeEvent");
Method getEntityMethod = lungeEventClass.getMethod("getEntity");
Method setCancelledMethod = lungeEventClass.getMethod("setCancelled", boolean.class);
Bukkit.getPluginManager().registerEvent(
lungeEventClass,
this,
EventPriority.NORMAL,
(listener, event) -> {
if (!settings.lungeCooldown) return;
if (!lungeEventClass.isInstance(event)) return;
try {
Object entity = getEntityMethod.invoke(event);
if (entity instanceof Player player) {
UUID uuid = player.getUniqueId();
if (settings.onlyForFight && !fightManager.isInCombat(uuid)) {
return;
}
// Query the remaining cooldown once to avoid redundant lookups and race conditions
Duration remaining = spearService.getRemainingCooldown(uuid);
if (!remaining.isZero() && !remaining.isNegative()) {
setCancelledMethod.invoke(event, true);
noticeService.create()
.player(uuid)
.notice(settings.lungeOnCooldown)
.placeholder("{TIME}", DurationUtil.format(remaining, !settings.useMillis))
.send();
} else {
spearService.saveCooldown(uuid);
}
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to handle EntityLungeEvent reflectively: " + e.getMessage());
}
},
plugin
);
} catch (ClassNotFoundException e) {
plugin.getLogger().info("EntityLungeEvent not found, skipping spear lunge cooldown registration.");
} catch (NoSuchMethodException e) {
plugin.getLogger().warning("Failed to find necessary methods for EntityLungeEvent: " + e.getMessage());
}
}
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.Plugin;
import java.time.Duration;
import java.util.UUID;
public class SpearLungeController implements Listener {
private final SpearService spearService;
public SpearLungeController(Plugin plugin, FightManager fightManager, SpearService spearService, SpearSettings settings, NoticeService noticeService) {
this.spearService = spearService;
Bukkit.getPluginManager().registerEvents(this, plugin);
try {
Class<? extends Event> lungeEventClass = Class.forName("io.papermc.paper.event.entity.EntityLungeEvent").asSubclass(EntityEvent.class);
Bukkit.getPluginManager().registerEvent(
lungeEventClass,
this,
EventPriority.NORMAL,
(listener, event) -> {
if (!settings.lungeCooldown) return;
if (!(event instanceof EntityEvent entityEvent)) return;
if (!(entityEvent.getEntity() instanceof Player player)) return;
UUID uuid = player.getUniqueId();
if (settings.onlyForFight && !fightManager.isInCombat(uuid)) return;
Duration remaining = spearService.getRemainingCooldown(uuid);
if (!remaining.isZero() && !remaining.isNegative()) {
((Cancellable) event).setCancelled(true);
noticeService.create()
.player(uuid)
.notice(settings.lungeOnCooldown)
.placeholder("{TIME}", DurationUtil.format(remaining, !settings.useMillis))
.send();
} else {
spearService.saveCooldown(uuid);
}
},
plugin
);
} catch (ClassNotFoundException e) {
plugin.getLogger().info("EntityLungeEvent not found, skipping spear lunge cooldown registration.");
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants