Route McMMOPlayer addXp and addLevels through level up processing

Both wrote raw values into the profile, so plugins using them left
skills over their XP threshold with no level ups, events, or XP bar
updates. addXp now runs the normal XP gain path with the amount
unmodified, and addLevels fires the level change events like the
addlevels command. Internal callers that revert cancelled events or
apply vampirism gains keep raw writes via the profile.
This commit is contained in:
nossr50
2026-07-11 00:09:49 -07:00
parent bb6a395fc6
commit df4cdce38c
4 changed files with 152 additions and 4 deletions

View File

@ -120,6 +120,7 @@ Version 2.3.000
(API) Deprecated DelayedHerbalismXPCheckTask and HerbalismManager#awardXPForBlockSnapshots for removal
(API) Deprecated unused public utility methods and fields
(API) Deprecated the misspelled PlayerProfile#getChimaerWingDATS in favor of getChimaeraWingDATS
(API) McMMOPlayer#addXp and McMMOPlayer#addLevels now fire the XP and level change events and process level ups instead of writing raw values (See notes)
(API) Added DatabaseManager#readLeaderboardSnapshot for reading every leaderboard scope in one call
(Codebase) Added scoreboard-library 2.8.0 as a shaded dependency for the packet-based scoreboard implementation
(Codebase) Added ViaVersion to plugin.yml softdepend
@ -216,6 +217,9 @@ Version 2.3.000
-- Child skill levels through the API --
Adding levels to Salvage or Smelting through the mcMMO API caused an error for online players. Child skills have no levels of their own, so added levels now split evenly across the skill's parent skills, matching how child skill XP and the offline API variants already behaved.
-- XP and levels added through the API --
(API) Only matters on servers running plugins that give out mcMMO XP or levels through McMMOPlayer#addXp or McMMOPlayer#addLevels. These previously wrote raw values without awarding level ups, leaving skills stuck over their XP threshold with no level up message, sound, or scoreboard update. They now behave like any normal gain and fire the usual events, so plugins listening for mcMMO XP and level changes will start seeing these gains too.
Version 2.2.054
Added compatibility for new blocks and items from Chaos Cubed (Minecraft 26.2) to mcMMO
Fixed party/admin chat allowing players to use color codes without the 'mcmmo.chat.colors' permission

View File

@ -1241,12 +1241,41 @@ public class McMMOPlayer implements Identified {
profile.modifySkill(skill, level);
}
/**
* Adds levels to a skill and fires the level change events, like the /addlevels command.
* Levels added to a child skill split evenly across its parent skills.
*
* @param skill the skill to add levels to
* @param levels the number of levels to add
*/
public void addLevels(PrimarySkillType skill, int levels) {
if (SkillTools.isChildSkill(skill)) {
var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(skill);
int dividedLevels = levels / parentSkills.size();
for (PrimarySkillType parentSkill : parentSkills) {
addLevels(parentSkill, dividedLevels);
}
return;
}
final float xpRemoved = profile.getSkillXpLevelRaw(skill);
profile.addLevels(skill, levels);
EventUtils.tryLevelChangeEvent(this, skill, levels, xpRemoved, true,
XPGainReason.UNKNOWN);
}
/**
* Adds XP as a normal gain: fires the XP gain events, awards level ups, and updates the
* XP bar. The amount is applied as-is, without the rate and perk modifiers that
* {@link #beginXpGain(PrimarySkillType, float, XPGainReason, XPGainSource)} applies.
*
* @param skill the skill to add XP to
* @param xp the amount of XP to add
*/
public void addXp(PrimarySkillType skill, float xp) {
profile.addXp(skill, xp);
applyXpGain(skill, xp, XPGainReason.UNKNOWN, XPGainSource.SELF);
}
public void setAbilityDATS(SuperAbilityType ability, long DATS) {

View File

@ -285,7 +285,7 @@ public final class EventUtils {
} else {
mmoPlayer.modifySkill(skill, mmoPlayer.getSkillLevel(skill)
- (isLevelUp ? levelsChanged : -levelsChanged));
mmoPlayer.addXp(skill, xpRemoved);
mmoPlayer.getProfile().addXp(skill, xpRemoved);
}
} else if (isLevelUp && mmoPlayer != null) {
NotificationManager.processLevelUpBroadcasting(mmoPlayer, skill,
@ -462,7 +462,7 @@ public final class EventUtils {
boolean isCancelled = event.isCancelled();
if (!isCancelled) {
mmoPlayer.addXp(skill, event.getRawXpGained());
mmoPlayer.getProfile().addXp(skill, event.getRawXpGained());
mmoPlayer.getProfile().registerXpGain(skill, event.getRawXpGained());
}
@ -546,7 +546,10 @@ public final class EventUtils {
String skillName = primarySkillType.toString();
int victimSkillLevel = victimProfile.getSkillLevel(primarySkillType);
killerPlayer.addLevels(primarySkillType, levelChangedKiller.get(skillName));
// Raw write: these gains are governed by the vampirism event above, so the
// level change events that addLevels fires must not run here
killerPlayer.getProfile()
.addLevels(primarySkillType, levelChangedKiller.get(skillName));
killerPlayer.beginUnsharedXpGain(primarySkillType,
experienceChangedKiller.get(skillName), XPGainReason.VAMPIRISM,
XPGainSource.VAMPIRISM);

View File

@ -0,0 +1,112 @@
package com.gmail.nossr50.datatypes.player;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.gmail.nossr50.MMOTestEnvironment;
import com.gmail.nossr50.config.experience.ExperienceConfig;
import com.gmail.nossr50.datatypes.experience.FormulaType;
import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
import com.gmail.nossr50.events.experience.McMMOPlayerLevelUpEvent;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.Permissions;
import com.gmail.nossr50.util.experience.FormulaManager;
import java.util.logging.Logger;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
/**
* Covers the public XP and level adding methods on {@link McMMOPlayer}. Other plugins award
* progression through these, so they must behave like any normal gain: XP that crosses a
* threshold levels the player up, and added levels fire the level change events. Raw writes
* that leave a skill sitting over its XP threshold must not be reachable here.
*/
class McMMOPlayerAddXpAndLevelsTest extends MMOTestEnvironment {
private static final Logger logger = Logger.getLogger(
McMMOPlayerAddXpAndLevelsTest.class.getName());
private static final int XP_TO_NEXT_LEVEL = 10;
@BeforeEach
void setUp() {
mockBaseEnvironment(logger);
stubLevelUpEnvironment();
}
@AfterEach
void tearDown() {
cleanUpStaticMocks();
}
/**
* Stubs everything the level-up path needs so gains translate into level-ups: skill
* permissions, disabled level caps, and a flat XP curve.
*/
private void stubLevelUpEnvironment() {
when(Permissions.skillEnabled(any(Player.class), any(PrimarySkillType.class)))
.thenReturn(true);
when(generalConfig.getPowerLevelCap()).thenReturn(Integer.MAX_VALUE);
when(generalConfig.getLevelCap(any(PrimarySkillType.class))).thenReturn(Integer.MAX_VALUE);
when(ExperienceConfig.getInstance().getFormulaType()).thenReturn(FormulaType.LINEAR);
final FormulaManager formulaManager = mock(FormulaManager.class);
when(formulaManager.getXPtoNextLevel(anyInt(), any(FormulaType.class)))
.thenReturn(XP_TO_NEXT_LEVEL);
when(mcMMO.getFormulaManager()).thenReturn(formulaManager);
}
@Test
void addXpShouldAwardALevelUpWhenTheGainCrossesTheThreshold() {
// Given - a level 0 player on a flat 10-XP-per-level curve
// When - another plugin adds 15 XP to Mining
mmoPlayer.addXp(PrimarySkillType.MINING, XP_TO_NEXT_LEVEL + 5F);
// Then - the player levels up and keeps the remainder instead of sitting at 15/10 XP
assertThat(mmoPlayer.getSkillLevel(PrimarySkillType.MINING)).isEqualTo(1);
assertThat(mmoPlayer.getProfile().getSkillXpLevelRaw(PrimarySkillType.MINING))
.isEqualTo(5F);
}
@Test
void addXpShouldAccrueTheExactAmountWhenBelowTheThreshold() {
// Given - a level 0 player on a flat 10-XP-per-level curve
// When - another plugin adds less XP than the next level needs
mmoPlayer.addXp(PrimarySkillType.MINING, XP_TO_NEXT_LEVEL - 5F);
// Then - the XP accrues unmodified by rates or perks and no level is awarded
assertThat(mmoPlayer.getSkillLevel(PrimarySkillType.MINING)).isZero();
assertThat(mmoPlayer.getProfile().getSkillXpLevelRaw(PrimarySkillType.MINING))
.isEqualTo(5F);
}
@Test
void addLevelsShouldFireTheLevelUpEventForTheAddedLevels() {
// Given - a level 0 player
// When - another plugin adds 3 Mining levels
mmoPlayer.addLevels(PrimarySkillType.MINING, 3);
// Then - the levels are applied and the level up event announces the change
assertThat(mmoPlayer.getSkillLevel(PrimarySkillType.MINING)).isEqualTo(3);
final ArgumentCaptor<Event> eventCaptor = ArgumentCaptor.forClass(Event.class);
verify(pluginManager, atLeastOnce()).callEvent(eventCaptor.capture());
assertThat(eventCaptor.getAllValues())
.filteredOn(McMMOPlayerLevelUpEvent.class::isInstance)
.map(McMMOPlayerLevelUpEvent.class::cast)
.singleElement()
.satisfies(levelUpEvent -> {
assertThat(levelUpEvent.getSkill()).isEqualTo(PrimarySkillType.MINING);
assertThat(levelUpEvent.getLevelsGained()).isEqualTo(3);
});
}
}