refactor: replace Rupture's Bukkit metadata with an owned registry (#5332)

This commit is contained in:
MrlingXD
2026-08-17 14:28:29 +08:00
committed by GitHub
parent ad8444c2f3
commit aaec3dcbfe
7 changed files with 81 additions and 52 deletions

View File

@ -2,7 +2,7 @@ package com.gmail.nossr50.api;
import com.gmail.nossr50.datatypes.player.McMMOPlayer;
import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
import com.gmail.nossr50.util.MetadataConstants;
import com.gmail.nossr50.runnables.skills.RuptureTask;
import com.gmail.nossr50.util.player.UserManager;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
@ -110,10 +110,6 @@ public final class AbilityAPI {
}
public static boolean isBleeding(LivingEntity entity) {
if (entity.isValid()) {
return entity.hasMetadata(MetadataConstants.METADATA_KEY_RUPTURE);
}
return false;
return entity.isValid() && RuptureTask.getActive(entity) != null;
}
}

View File

@ -1,10 +1,22 @@
package com.gmail.nossr50.datatypes.meta;
import com.gmail.nossr50.api.AbilityAPI;
import com.gmail.nossr50.runnables.skills.RuptureTask;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.metadata.FixedMetadataValue;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
/**
* No longer used. mcMMO stopped attaching rupture state to entities as Bukkit metadata
* in 2.3.001; active ruptures are tracked internally instead. Use
* {@link AbilityAPI#isBleeding(LivingEntity)} to check for an active bleed, or
* {@link RuptureTask#getActive(Entity)} to retrieve the task.
*
* @deprecated mcMMO no longer creates or reads this metadata.
*/
@Deprecated(forRemoval = true, since = "2.3.001")
public class RuptureTaskMeta extends FixedMetadataValue {
private final @NotNull RuptureTask ruptureTask;

View File

@ -12,7 +12,6 @@ import com.gmail.nossr50.datatypes.chat.ChatChannel;
import com.gmail.nossr50.datatypes.experience.XPGainReason;
import com.gmail.nossr50.datatypes.experience.XPGainSource;
import com.gmail.nossr50.datatypes.interactions.NotificationType;
import com.gmail.nossr50.datatypes.meta.RuptureTaskMeta;
import com.gmail.nossr50.datatypes.party.Party;
import com.gmail.nossr50.datatypes.party.PartyTeleportRecord;
import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
@ -48,7 +47,6 @@ import com.gmail.nossr50.skills.unarmed.UnarmedManager;
import com.gmail.nossr50.skills.woodcutting.WoodcuttingManager;
import com.gmail.nossr50.util.BlockUtils;
import com.gmail.nossr50.util.EventUtils;
import com.gmail.nossr50.util.MetadataConstants;
import com.gmail.nossr50.util.Misc;
import com.gmail.nossr50.util.Permissions;
import com.gmail.nossr50.util.experience.ExperienceBarManager;
@ -1296,17 +1294,9 @@ public class McMMOPlayer implements Identified {
*/
public void logout(boolean syncSave) {
final Player thisPlayer = getPlayer();
if (thisPlayer.hasMetadata(MetadataConstants.METADATA_KEY_RUPTURE)) {
final RuptureTaskMeta ruptureTaskMeta
= (RuptureTaskMeta) thisPlayer.getMetadata(
MetadataConstants.METADATA_KEY_RUPTURE).get(0);
if (ruptureTaskMeta != null) {
final RuptureTask ruptureTimerTask = ruptureTaskMeta.getRuptureTimerTask();
if (ruptureTimerTask != null) {
ruptureTimerTask.cancel();
}
thisPlayer.removeMetadata(MetadataConstants.METADATA_KEY_RUPTURE, mcMMO.p);
}
final RuptureTask ruptureTask = RuptureTask.getActive(thisPlayer);
if (ruptureTask != null) {
ruptureTask.cancel();
}
cleanup();

View File

@ -6,21 +6,29 @@ import com.gmail.nossr50.datatypes.player.McMMOPlayer;
import com.gmail.nossr50.events.skills.rupture.McMMOEntityDamageByRuptureEvent;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.CancellableRunnable;
import com.gmail.nossr50.util.MetadataConstants;
import com.gmail.nossr50.util.MobHealthbarUtils;
import com.gmail.nossr50.util.skills.ParticleEffectUtils;
import com.gmail.nossr50.util.sounds.SoundManager;
import com.gmail.nossr50.util.sounds.SoundType;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class RuptureTask extends CancellableRunnable {
public static final int DAMAGE_TICK_INTERVAL = 10;
public static final int ANIMATION_TICK_INTERVAL = 1;
// Active rupture per target entity.
private static final @NotNull Map<UUID, RuptureTask> ACTIVE_RUPTURES =
new ConcurrentHashMap<>();
private final @NotNull McMMOPlayer ruptureSource;
private final @NotNull LivingEntity targetEntity;
private final int expireTick;
@ -53,6 +61,20 @@ public class RuptureTask extends CancellableRunnable {
this.pureTickDamage = pureTickDamage;
}
public static @Nullable RuptureTask getActive(@NotNull Entity target) {
return ACTIVE_RUPTURES.get(target.getUniqueId());
}
/**
* Registers this rupture as the target's active bleed and starts it ticking.
* The task removes itself from the registry when it is cancelled; on Folia the
* retired callback covers entities that are removed before the next tick.
*/
public void schedule() {
ACTIVE_RUPTURES.put(targetEntity.getUniqueId(), this);
mcMMO.p.getFoliaLib().getScheduler().runAtEntityTimer(targetEntity, this, this::cancel, 1, 1);
}
/**
* Deprecated constructor for the RuptureTask class.
*
@ -106,10 +128,15 @@ public class RuptureTask extends CancellableRunnable {
}
} else {
this.cancel(); //Task no longer needed
targetEntity.removeMetadata(MetadataConstants.METADATA_KEY_RUPTURE, mcMMO.p);
}
}
@Override
public void cancel() {
ACTIVE_RUPTURES.remove(targetEntity.getUniqueId(), this);
super.cancel();
}
private void playAnimation() {
if (animationTick >= ANIMATION_TICK_INTERVAL) {
ParticleEffectUtils.playBleedEffect(targetEntity); //Animate
@ -176,7 +203,6 @@ public class RuptureTask extends CancellableRunnable {
}
private void endRupture() {
targetEntity.removeMetadata(MetadataConstants.METADATA_KEY_RUPTURE, mcMMO.p);
this.cancel(); //Task no longer needed
}

View File

@ -1,7 +1,6 @@
package com.gmail.nossr50.skills.swords;
import com.gmail.nossr50.datatypes.interactions.NotificationType;
import com.gmail.nossr50.datatypes.meta.RuptureTaskMeta;
import com.gmail.nossr50.datatypes.player.McMMOPlayer;
import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
import com.gmail.nossr50.datatypes.skills.SubSkillType;
@ -10,7 +9,6 @@ import com.gmail.nossr50.datatypes.skills.ToolType;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.runnables.skills.RuptureTask;
import com.gmail.nossr50.skills.SkillManager;
import com.gmail.nossr50.util.MetadataConstants;
import com.gmail.nossr50.util.Permissions;
import com.gmail.nossr50.util.player.NotificationManager;
import com.gmail.nossr50.util.random.ProbabilityUtil;
@ -81,17 +79,15 @@ public class SwordsManager extends SkillManager {
return;
}
if (target.hasMetadata(MetadataConstants.METADATA_KEY_RUPTURE)) {
RuptureTaskMeta ruptureTaskMeta = (RuptureTaskMeta) target.getMetadata(
MetadataConstants.METADATA_KEY_RUPTURE).get(0);
final RuptureTask ongoingRupture = RuptureTask.getActive(target);
if (ongoingRupture != null) {
if (mmoPlayer.isDebugMode()) {
mmoPlayer.getPlayer()
.sendMessage("Rupture task ongoing for target " + target);
mmoPlayer.getPlayer().sendMessage(ruptureTaskMeta.getRuptureTimerTask().toString());
mmoPlayer.getPlayer().sendMessage(ongoingRupture.toString());
}
ruptureTaskMeta.getRuptureTimerTask().refreshRupture();
ongoingRupture.refreshRupture();
return; //Don't apply bleed
}
@ -118,10 +114,7 @@ public class SwordsManager extends SkillManager {
mcMMO.p.getAdvancedConfig()
.getRuptureTickDamage(target instanceof Player, getRuptureRank()));
final RuptureTaskMeta ruptureTaskMeta = new RuptureTaskMeta(mcMMO.p, ruptureTask);
mcMMO.p.getFoliaLib().getScheduler().runAtEntityTimer(target, ruptureTask, 1, 1);
target.setMetadata(MetadataConstants.METADATA_KEY_RUPTURE, ruptureTaskMeta);
ruptureTask.schedule();
}
}

View File

@ -17,7 +17,6 @@ public class MetadataConstants {
* removed by {@link MobHealthbarUtils#restoreNameFromSnapshot}.
*/
public static final @NotNull Set<String> MOB_METADATA_KEYS = Set.of(
MetadataConstants.METADATA_KEY_RUPTURE,
MetadataConstants.METADATA_KEY_EXPLOSION_FROM_RUPTURE
);
@ -66,7 +65,17 @@ public class MetadataConstants {
public static final @NotNull String METADATA_KEY_VILLAGER_TRADE_ORIGIN_ITEM = "mcmmo_villager_trade_origin_item";
public static final @NotNull String METADATA_KEY_EXPLOITED_ENDERMEN = "mcmmo_exploited_endermen";
/**
* No longer used. mcMMO stopped applying this metadata to bleeding entities in 2.3.001,
* so {@code hasMetadata} checks against this key always return false. Use
* {@link com.gmail.nossr50.api.AbilityAPI#isBleeding(org.bukkit.entity.LivingEntity)} instead.
*
* @deprecated mcMMO no longer applies this metadata; see
* {@link com.gmail.nossr50.api.AbilityAPI#isBleeding}.
*/
@Deprecated(forRemoval = true, since = "2.3.001")
public static final @NotNull String METADATA_KEY_RUPTURE = "mcmmo_rupture";
public static final byte SIMPLE_FLAG_VALUE = (byte) 0x1;
/**

View File

@ -14,14 +14,12 @@ import static org.mockito.Mockito.when;
import com.gmail.nossr50.MMOTestEnvironment;
import com.gmail.nossr50.api.exceptions.InvalidSkillException;
import com.gmail.nossr50.datatypes.interactions.NotificationType;
import com.gmail.nossr50.datatypes.meta.RuptureTaskMeta;
import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
import com.gmail.nossr50.datatypes.skills.SubSkillType;
import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
import com.gmail.nossr50.datatypes.skills.ToolType;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.runnables.skills.RuptureTask;
import com.gmail.nossr50.util.MetadataConstants;
import com.gmail.nossr50.util.Permissions;
import com.gmail.nossr50.util.player.NotificationManager;
import com.gmail.nossr50.util.random.ProbabilityUtil;
@ -29,7 +27,9 @@ import com.gmail.nossr50.util.skills.CombatUtils;
import com.gmail.nossr50.util.skills.RankUtils;
import com.tcoded.folialib.FoliaLib;
import com.tcoded.folialib.impl.PlatformScheduler;
import java.util.List;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.UUID;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
@ -67,6 +67,7 @@ class SwordsManagerTest extends MMOTestEnvironment {
swordsManager = new SwordsManager(mmoPlayer);
target = Mockito.mock(LivingEntity.class);
when(target.getUniqueId()).thenReturn(UUID.randomUUID());
// Simulate Paper 26.1.2+ during a damage event: the ticker was already reset, so the
// live cooldown misreports the charge of the committed hit.
@ -86,7 +87,6 @@ class SwordsManagerTest extends MMOTestEnvironment {
.thenReturn(true);
Mockito.when(RankUtils.getRank(player, SubSkillType.SWORDS_RUPTURE)).thenReturn(3);
when(advancedConfig.getRuptureChanceToApplyOnHit(3)).thenReturn(33.0);
when(target.hasMetadata(MetadataConstants.METADATA_KEY_RUPTURE)).thenReturn(false);
try (MockedStatic<ProbabilityUtil> probabilityUtil = mockStatic(ProbabilityUtil.class)) {
// When - a hit committed at half attack strength lands and the roll fails
@ -171,18 +171,21 @@ class SwordsManagerTest extends MMOTestEnvironment {
when(advancedConfig.getRuptureChanceToApplyOnHit(3)).thenReturn(100.0);
}
// Seeds RuptureTask's active-rupture registry for the test.
@SuppressWarnings("unchecked")
private void trackRupture(Entity target, RuptureTask task) throws Exception {
Field field = RuptureTask.class.getDeclaredField("ACTIVE_RUPTURES");
field.setAccessible(true);
((Map<UUID, RuptureTask>) field.get(null)).put(target.getUniqueId(), task);
}
@Test
void existingRuptureShouldBeRefreshedInsteadOfReRolled() {
void existingRuptureShouldBeRefreshedInsteadOfReRolled() throws Exception {
try (MockedStatic<ProbabilityUtil> probabilityUtil =
mockStatic(ProbabilityUtil.class)) {
// Given - the target is already bleeding from a rupture
final RuptureTask ongoingRupture = Mockito.mock(RuptureTask.class);
final RuptureTaskMeta ruptureMeta = Mockito.mock(RuptureTaskMeta.class);
when(ruptureMeta.getRuptureTimerTask()).thenReturn(ongoingRupture);
when(target.hasMetadata(MetadataConstants.METADATA_KEY_RUPTURE))
.thenReturn(true);
when(target.getMetadata(MetadataConstants.METADATA_KEY_RUPTURE))
.thenReturn(List.of(ruptureMeta));
trackRupture(target, ongoingRupture);
// When - another rupture-capable hit lands
swordsManager.processRupture(target, 1.0);
@ -207,10 +210,9 @@ class SwordsManagerTest extends MMOTestEnvironment {
swordsManager.processRupture(target, 1.0);
// Then - a bleed task starts ticking on the target and is remembered on it
verify(scheduler).runAtEntityTimer(eq(target), any(RuptureTask.class), eq(1L),
eq(1L));
verify(target).setMetadata(eq(MetadataConstants.METADATA_KEY_RUPTURE),
any(RuptureTaskMeta.class));
verify(scheduler).runAtEntityTimer(eq(target), any(RuptureTask.class),
any(Runnable.class), eq(1L), eq(1L));
assertThat(RuptureTask.getActive(target)).isNotNull();
}
}
@ -220,6 +222,7 @@ class SwordsManagerTest extends MMOTestEnvironment {
mockStatic(ProbabilityUtil.class)) {
// Given - a defending player who is blocking with a shield
final Player defender = Mockito.mock(Player.class);
when(defender.getUniqueId()).thenReturn(UUID.randomUUID());
when(defender.isBlocking()).thenReturn(true);
probabilityUtil.when(() -> ProbabilityUtil.isStaticSkillRNGSuccessful(
eq(PrimarySkillType.SWORDS), eq(mmoPlayer), eq(100.0)))
@ -229,8 +232,7 @@ class SwordsManagerTest extends MMOTestEnvironment {
swordsManager.processRupture(defender, 1.0);
// Then - no bleed starts
verify(defender, never()).setMetadata(eq(MetadataConstants.METADATA_KEY_RUPTURE),
any());
assertThat(RuptureTask.getActive(defender)).isNull();
}
}
@ -240,6 +242,7 @@ class SwordsManagerTest extends MMOTestEnvironment {
mockStatic(ProbabilityUtil.class)) {
// Given - a non-blocking defending player who uses notifications
final Player defender = Mockito.mock(Player.class);
when(defender.getUniqueId()).thenReturn(UUID.randomUUID());
when(NotificationManager.doesPlayerUseNotifications(defender)).thenReturn(true);
probabilityUtil.when(() -> ProbabilityUtil.isStaticSkillRNGSuccessful(
eq(PrimarySkillType.SWORDS), eq(mmoPlayer), eq(100.0)))