Bounced arrows no longer have super long durations Fixes #5246 Fixes #5243

This commit is contained in:
nossr50
2026-04-19 13:33:51 -07:00
parent 4d653535c6
commit ebd2f902b6
4 changed files with 176 additions and 11 deletions

View File

@ -1,3 +1,7 @@
Version 2.2.051
Fixed bug which caused trickshot-bounced arrows to have much longer potion durations than intended
Version 2.2.050
Compatible with Tiny Takeover aka Minecraft 26.1.x (see notes)
Minimum supported Minecraft version raised to 1.20.5 (see notes)

View File

@ -15,8 +15,8 @@
</scm>
<properties>
<spigot.version>26.1.1-R0.1-SNAPSHOT</spigot.version>
<!-- <spigot.version>1.20.5-R0.1-SNAPSHOT</spigot.version>-->
<!-- <spigot.version>26.1.1-R0.1-SNAPSHOT</spigot.version>-->
<spigot.version>1.20.5-R0.1-SNAPSHOT</spigot.version>
<!-- <spigot.version>1.21.11-R0.1-SNAPSHOT</spigot.version>-->
<kyori.adventure.version>4.23.0</kyori.adventure.version>
<kyori.adventure.platform.version>4.4.1-SNAPSHOT</kyori.adventure.platform.version>

View File

@ -13,6 +13,7 @@ import com.gmail.nossr50.skills.SkillManager;
import com.gmail.nossr50.util.MetadataConstants;
import com.gmail.nossr50.util.Permissions;
import com.gmail.nossr50.util.random.ProbabilityUtil;
import com.gmail.nossr50.util.skills.ArrowItemStackHandler;
import com.gmail.nossr50.util.skills.ProjectileUtils;
import com.gmail.nossr50.util.skills.RankUtils;
import org.bukkit.Location;
@ -79,15 +80,12 @@ public class CrossbowsManager extends SkillManager {
spawnedArrow.setPickupStatus(originalArrow.getPickupStatus());
spawnedArrow.setKnockbackStrength(originalArrow.getKnockbackStrength());
if (originalArrow.getBasePotionType() != null) {
spawnedArrow.setBasePotionType(originalArrow.getBasePotionType());
}
if (originalArrow.hasCustomEffects()) {
for (var effect : originalArrow.getCustomEffects()) {
spawnedArrow.addCustomEffect(effect, true);
}
}
// Copy the full item stack from the original arrow to preserve the tipped arrow
// item type and its POTION_DURATION_SCALE (0.125) data component. Without this,
// the spawned arrow has a plain Items.ARROW pickup item, causing
// POTION_DURATION_SCALE to default to 1.0 — making effects last 8× longer.
// Also fixes the arrow losing its tipped-arrow color/texture after ricochet.
ArrowItemStackHandler.copyArrowItemStack(originalArrow, spawnedArrow);
// copy metadata from old arrow
ProjectileUtils.copyArrowMetadata(pluginRef, originalArrow, spawnedArrow);

View File

@ -0,0 +1,163 @@
package com.gmail.nossr50.util.skills;
import com.gmail.nossr50.mcMMO;
import java.lang.reflect.Method;
import org.bukkit.entity.Arrow;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Handles copying the full pickup item between arrows to preserve tipped-arrow state
* (item type, {@code POTION_DURATION_SCALE}, potion contents, custom effects, and color)
* across Trickshot ricochets.
*
* <p>Uses a three-tier strategy, resolved once at class load via cached reflection:</p>
* <ol>
* <li><strong>Paper</strong> — {@code AbstractArrow.getItemStack()} / {@code setItemStack()}.
* Single call copies everything including color.</li>
* <li><strong>Spigot</strong> — {@code AbstractArrow.getItem()} / {@code setItem()}
* ({@code @ApiStatus.Experimental}). Copies the pickup item stack (fixing duration scale),
* then calls {@code setBasePotionType()} to trigger the internal {@code updateColor()}
* so clients see the correct tipped-arrow particles.</li>
* <li><strong>Fallback</strong> — manual copy of base potion type, custom effects, and color
* via standard Bukkit API. Does <em>not</em> fix the duration scale issue (pickup item
* remains {@code Items.ARROW}), but preserves as much potion state as possible.</li>
* </ol>
*/
public final class ArrowItemStackHandler {
// Tier 1: Paper API — AbstractArrow.getItemStack() / setItemStack(ItemStack)
private static final @Nullable Method PAPER_GET_ITEM_STACK;
private static final @Nullable Method PAPER_SET_ITEM_STACK;
// Tier 2: Spigot Bukkit API — AbstractArrow.getItem() / setItem(ItemStack)
private static final @Nullable Method SPIGOT_GET_ITEM;
private static final @Nullable Method SPIGOT_SET_ITEM;
static {
// Tier 1: probe for Paper API
Method paperGetter = null;
Method paperSetter = null;
try {
paperGetter = Arrow.class.getMethod("getItemStack");
paperSetter = Arrow.class.getMethod("setItemStack", ItemStack.class);
} catch (final NoSuchMethodException ignored) {
// Not on Paper
}
PAPER_GET_ITEM_STACK = paperGetter;
PAPER_SET_ITEM_STACK = paperSetter;
// Tier 2: probe for Spigot's experimental getItem/setItem
Method spigotGetter = null;
Method spigotSetter = null;
try {
spigotGetter = Arrow.class.getMethod("getItem");
spigotSetter = Arrow.class.getMethod("setItem", ItemStack.class);
} catch (final NoSuchMethodException ignored) {
// Not available on this Spigot version
}
SPIGOT_GET_ITEM = spigotGetter;
SPIGOT_SET_ITEM = spigotSetter;
}
private ArrowItemStackHandler() {
}
/**
* Copies the full pickup item from one arrow to another using the best available API,
* preserving the tipped-arrow item type and its {@code POTION_DURATION_SCALE} component.
*
* @param sourceArrow the original arrow to copy from
* @param targetArrow the newly spawned arrow to copy to
*/
public static void copyArrowItemStack(@NotNull final Arrow sourceArrow,
@NotNull final Arrow targetArrow) {
if (PAPER_GET_ITEM_STACK != null && PAPER_SET_ITEM_STACK != null) {
copyItemStackViaPaper(sourceArrow, targetArrow);
} else if (SPIGOT_GET_ITEM != null && SPIGOT_SET_ITEM != null) {
copyItemViaSpigot(sourceArrow, targetArrow);
} else {
copyPotionDataFallback(sourceArrow, targetArrow);
}
}
/**
* Tier 1 — Paper: {@code getItemStack()} / {@code setItemStack()}.
* Copies the full item stack. Follow up with {@code setBasePotionType()} to ensure
* the internal {@code updateColor()} fires for client-side particle sync.
*/
private static void copyItemStackViaPaper(@NotNull final Arrow sourceArrow,
@NotNull final Arrow targetArrow) {
try {
final ItemStack itemStack = (ItemStack) PAPER_GET_ITEM_STACK.invoke(sourceArrow);
if (itemStack != null) {
PAPER_SET_ITEM_STACK.invoke(targetArrow, itemStack);
}
// Trigger updateColor() via setBasePotionType() in case Paper doesn't do it
syncColor(sourceArrow, targetArrow);
} catch (final ReflectiveOperationException e) {
mcMMO.p.getLogger().warning(
"Paper arrow item-stack copy failed, falling back: " + e.getMessage());
copyPotionDataFallback(sourceArrow, targetArrow);
}
}
/**
* Tier 2 — Spigot: {@code getItem()} / {@code setItem()}.
* Copies the pickup item stack (fixing duration scale and item type), then calls
* {@code setBasePotionType()} to trigger the NMS {@code updateColor()} side-effect
* so clients see the correct tipped-arrow color and particles.
*/
private static void copyItemViaSpigot(@NotNull final Arrow sourceArrow,
@NotNull final Arrow targetArrow) {
try {
final ItemStack item = (ItemStack) SPIGOT_GET_ITEM.invoke(sourceArrow);
if (item != null) {
SPIGOT_SET_ITEM.invoke(targetArrow, item);
}
// setItem() writes pickupItemStack directly but does NOT call updateColor().
// Re-applying the base potion type triggers setPotionContents() → updateColor().
syncColor(sourceArrow, targetArrow);
} catch (final ReflectiveOperationException e) {
mcMMO.p.getLogger().warning(
"Spigot arrow item copy failed, falling back: " + e.getMessage());
copyPotionDataFallback(sourceArrow, targetArrow);
}
}
/**
* Re-applies the base potion type to trigger the internal {@code updateColor()} call,
* ensuring clients see the correct tipped-arrow color and particles.
*/
private static void syncColor(@NotNull final Arrow sourceArrow,
@NotNull final Arrow targetArrow) {
if (sourceArrow.getBasePotionType() != null) {
targetArrow.setBasePotionType(sourceArrow.getBasePotionType());
}
}
/**
* Tier 3 — Fallback: copies potion base type, custom effects, and color individually
* via standard Bukkit API. Does <strong>not</strong> fix the {@code POTION_DURATION_SCALE}
* issue (the pickup item remains {@code Items.ARROW} with a default scale of 1.0),
* but preserves as much potion state as the API allows.
*/
private static void copyPotionDataFallback(@NotNull final Arrow sourceArrow,
@NotNull final Arrow targetArrow) {
if (sourceArrow.getBasePotionType() != null) {
targetArrow.setBasePotionType(sourceArrow.getBasePotionType());
}
if (sourceArrow.hasCustomEffects()) {
for (final var effect : sourceArrow.getCustomEffects()) {
targetArrow.addCustomEffect(effect, true);
}
}
if (sourceArrow.getColor() != null) {
targetArrow.setColor(sourceArrow.getColor());
}
}
}