Skip to content

[codex] Fix combat runtime issues#393

Draft
DimaSergeew wants to merge 2 commits into
EternalCodeTeam:masterfrom
DimaSergeew:codex/fix-runtime-combat-issues
Draft

[codex] Fix combat runtime issues#393
DimaSergeew wants to merge 2 commits into
EternalCodeTeam:masterfrom
DimaSergeew:codex/fix-runtime-combat-issues

Conversation

@DimaSergeew

Copy link
Copy Markdown

Summary

Fixes several combat runtime and logic issues found during review:

  • registers entity mount handling dynamically across Bukkit/Spigot event package variants so listener registration does not fail when one class is missing
  • removes duplicate PlaceBlockBlocker registration
  • moves update notifications and death flare particles back through the scheduler before touching Bukkit/player state
  • updates border trigger indexing on the scheduler thread and stores indexes in a concurrent map
  • fixes percent-based and player-health-based drop calculations so configured drop percentages match actual drops and XP
  • applies headDropOnlyInCombat before head-drop chance rolls
  • drops leftover kept items on respawn instead of silently losing them when inventory is full
  • avoids cancelling firework damage at MONITOR priority

Root Cause

The plugin mixed API-version-specific event classes, async callbacks, and Bukkit state access in several listener paths. A few drop calculations also treated the percentage to drop as the percentage to keep/remove.

Validation

  • :eternalcombat-plugin:compileJava
  • gradle check

Note: the project currently has no test sources, so Gradle reports test NO-SOURCE.

@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 several bug fixes, scheduler adjustments, and compatibility improvements, including a reflection-based implementation for handling entity mount events across different server platforms. Key feedback focuses on ensuring items are dropped at the correct respawn location during player respawn, using the correct max health attribute value that accounts for active modifiers, and caching the reflected getMount method in KnockbackRegionController to avoid performance overhead on every event execution.

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 on lines +143 to +144
HashMap<Integer, ItemStack> leftover = playerInventory.addItem(itemsToGive);
leftover.values().forEach(item -> player.getWorld().dropItemNaturally(player.getLocation(), item));

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.

high

In PlayerRespawnEvent, the player is in the process of respawning and may not be fully teleported to their respawn location yet. Using player.getLocation() and player.getWorld() can cause leftover items to drop at their death location or an incorrect world (e.g., if they respawn in a different world). Use event.getRespawnLocation() and its corresponding world to ensure items are dropped at the actual respawn location.

Suggested change
HashMap<Integer, ItemStack> leftover = playerInventory.addItem(itemsToGive);
leftover.values().forEach(item -> player.getWorld().dropItemNaturally(player.getLocation(), item));
HashMap<Integer, ItemStack> leftover = playerInventory.addItem(itemsToGive);
leftover.values().forEach(item -> event.getRespawnLocation().getWorld().dropItemNaturally(event.getRespawnLocation(), item));

Comment on lines +53 to +54
int dropPercent = MathUtil.clamp(100 - percentHealth, this.settings.playersHealthPercentClamp, 100);
int keepPercent = 100 - dropPercent;

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.

medium

Using player.getAttribute(Attribute.GENERIC_MAX_HEALTH).getBaseValue() retrieves the base maximum health (usually 20.0), which ignores any active modifiers such as health boost effects, absorption, or attribute modifiers from equipment. Since logout.health() represents the player's actual health at logout (which can exceed the base max health), this can result in incorrect percentage calculations (e.g., greater than 100%). Use getValue() instead of getBaseValue(), and safely handle potential null values.

Suggested change
int dropPercent = MathUtil.clamp(100 - percentHealth, this.settings.playersHealthPercentClamp, 100);
int keepPercent = 100 - dropPercent;
double correctedMaxHealth = Optional.ofNullable(player.getAttribute(org.bukkit.attribute.Attribute.GENERIC_MAX_HEALTH))
.map(org.bukkit.attribute.AttributeInstance::getValue)
.orElse(20.0);
int dropPercent = MathUtil.clamp(100 - MathUtil.getRoundedCountPercentage(health, correctedMaxHealth), this.settings.playersHealthPercentClamp, 100);
int keepPercent = 100 - dropPercent;

Comment on lines +30 to +33
private static final String[] ENTITY_MOUNT_EVENT_CLASSES = {
"org.bukkit.event.entity.EntityMountEvent",
"org.spigotmc.event.entity.EntityMountEvent"
};

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.

medium

To avoid performing expensive reflection lookups (getMethod) on every single EntityMountEvent execution, we should resolve and cache the getMount method once during initialization.

Suggested change
private static final String[] ENTITY_MOUNT_EVENT_CLASSES = {
"org.bukkit.event.entity.EntityMountEvent",
"org.spigotmc.event.entity.EntityMountEvent"
};
private static final String[] ENTITY_MOUNT_EVENT_CLASSES = {
"org.bukkit.event.entity.EntityMountEvent",
"org.spigotmc.event.entity.EntityMountEvent"
};
private Method getMountMethod;

Comment on lines +148 to +157
private void registerEntityMountEvent(Plugin plugin) {
Optional<Class<? extends Event>> eventClass = this.findEntityMountEventClass();
if (eventClass.isEmpty()) {
plugin.getLogger().fine("EntityMountEvent is not available. Mount region protection will be disabled.");
return;
}

EventExecutor executor = this::onEntityMount;
plugin.getServer().getPluginManager().registerEvent(eventClass.get(), this, EventPriority.HIGHEST, executor, plugin, true);
}

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.

medium

Resolve and cache the getMount method during event registration so it can be reused efficiently.

    private void registerEntityMountEvent(Plugin plugin) {
        Optional<Class<? extends Event>> eventClass = this.findEntityMountEventClass();
        if (eventClass.isEmpty()) {
            plugin.getLogger().fine("EntityMountEvent is not available. Mount region protection will be disabled.");
            return;
        }

        try {
            this.getMountMethod = eventClass.get().getMethod("getMount");
        } catch (NoSuchMethodException exception) {
            plugin.getLogger().warning("Failed to find getMount method on " + eventClass.get().getName());
            return;
        }

        EventExecutor executor = this::onEntityMount;
        plugin.getServer().getPluginManager().registerEvent(eventClass.get(), this, EventPriority.HIGHEST, executor, plugin, true);
    }

Comment on lines +199 to +212
private Entity getMount(Event event) {
try {
Method getMount = event.getClass().getMethod("getMount");
Object mount = getMount.invoke(event);

if (mount instanceof Entity entity) {
return entity;
}

return null;
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException exception) {
throw new IllegalStateException("Cannot read mount from " + event.getClass().getName(), exception);
}
}

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.

medium

Use the cached getMountMethod to invoke the method. Additionally, handle any reflection exceptions gracefully by returning null instead of throwing an IllegalStateException which would crash the event handler and spam the console.

Suggested change
private Entity getMount(Event event) {
try {
Method getMount = event.getClass().getMethod("getMount");
Object mount = getMount.invoke(event);
if (mount instanceof Entity entity) {
return entity;
}
return null;
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException exception) {
throw new IllegalStateException("Cannot read mount from " + event.getClass().getName(), exception);
}
}
private Entity getMount(Event event) {
if (this.getMountMethod == null) {
return null;
}
try {
Object mount = this.getMountMethod.invoke(event);
if (mount instanceof Entity entity) {
return entity;
}
return null;
} catch (IllegalAccessException | InvocationTargetException exception) {
return null;
}
}

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.

1 participant