Fixed force-class-selection thread safety

This commit is contained in:
Jules 2025-10-29 22:44:57 +01:00
parent a688ed32ed
commit 60b958f986
5 changed files with 48 additions and 35 deletions

View File

@ -186,7 +186,7 @@
<dependency>
<groupId>fr.phoenixdevt</groupId>
<artifactId>Profile-API</artifactId>
<version>1.2</version>
<version>1.2-SNAPSHOT</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>

View File

@ -68,6 +68,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.stream.Collectors;
@ -220,9 +221,36 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
this.getStats().updateStats();
PartyUtils.resolvePartyBonuses(this); // In case buffs not removed on logoff
tryForceClassSelection(); // Force class selection
getMMOPlayerData().getProfileSession().addOpenCallback(session -> this.onProfileSessionReady());
}
//region Force class selection
private Runnable bufferedForcedClassSelectionCallback;
private final AtomicBoolean bufferedForcedClassSelection = new AtomicBoolean(false);
private void tryForceClassSelection() {
final var canOpen = this.bufferedForcedClassSelection.getAndSet(true);
// Useless to check if `forceClassSelect` is enabled, it must be if `canOpen` is `true`
if (canOpen) {
this.bufferedForcedClassSelectionCallback.run();
this.bufferedForcedClassSelectionCallback = null;
}
}
public void bufferForceClassSelection(@NotNull Runnable callback) {
this.bufferedForcedClassSelectionCallback = callback;
final var canOpen = this.bufferedForcedClassSelection.getAndSet(true);
if (canOpen) {
this.bufferedForcedClassSelectionCallback.run();
this.bufferedForcedClassSelectionCallback = null;
}
}
//endregion
private void castOnLoginScripts() {
// Class Skills
@ -499,7 +527,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
/**
* @return If the item is unlocked by the player
* This is used for skills that can be locked & unlocked.
* This is used for skills that can be locked & unlocked.
*/
public boolean hasUnlocked(Unlockable unlockable) {
return unlockable.isUnlockedByDefault() || unlockedItems.contains(unlockable.getUnlockNamespacedKey());
@ -673,7 +701,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
/**
* @param key The identifier of an exp table item.
* @return Amount of times an item has been claimed
* inside an experience table.
* inside an experience table.
*/
public int getClaims(@NotNull String key) {
return tableItemClaims.getOrDefault(key, 0);
@ -1138,7 +1166,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
/**
* @return If the PlayerEnterCastingModeEvent successfully put the player
* into casting mode, otherwise if the event is cancelled, returns false.
* into casting mode, otherwise if the event is cancelled, returns false.
*/
public boolean setSkillCasting() {
Validate.isTrue(!isCasting(), "Player already in casting mode");
@ -1157,7 +1185,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
/**
* @return If player successfully left skill casting i.e the Bukkit
* event has not been cancelled
* event has not been cancelled
*/
public boolean leaveSkillCasting() {
return leaveSkillCasting(false);
@ -1166,7 +1194,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
/**
* @param skipEvent Skip firing the exit event
* @return If player successfully left skill casting i.e the Bukkit
* event has not been cancelled
* event has not been cancelled
*/
public boolean leaveSkillCasting(boolean skipEvent) {
Validate.isTrue(isCasting(), "Player not in casting mode");
@ -1554,7 +1582,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
* checks if they could potentially upgrade to one of these
*
* @return If the player can change its current class to
* a subclass
* a subclass
*/
@Deprecated
public boolean canChooseSubclass() {

View File

@ -7,11 +7,11 @@ import fr.phoenixdevt.profiles.event.ProfileRemoveEvent;
import fr.phoenixdevt.profiles.event.ProfileSelectEvent;
import fr.phoenixdevt.profiles.event.ProfileUnloadEvent;
import io.lumine.mythic.lib.MythicLib;
import io.lumine.mythic.lib.api.event.SynchronizedDataLoadEvent;
import io.lumine.mythic.lib.comp.profile.ProfileMode;
import io.lumine.mythic.lib.util.lang3.Validate;
import net.Indyuce.mmocore.MMOCore;
import net.Indyuce.mmocore.api.player.PlayerData;
import net.Indyuce.mmocore.api.player.profess.ClassOption;
import net.Indyuce.mmocore.manager.InventoryManager;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
@ -47,7 +47,7 @@ public class ForceClassProfileDataModule implements ProfileDataModule {
@EventHandler
public void onProfileCreate(ProfileCreateEvent event) {
// Proxy-based profiles
// Will be prompted on profile application in proxy-mode
if (MythicLib.plugin.getProfileMode() == ProfileMode.PROXY) {
event.validate(this);
return;
@ -57,30 +57,14 @@ public class ForceClassProfileDataModule implements ProfileDataModule {
InventoryManager.CLASS_SELECT.newInventory(playerData, () -> event.validate(this)).open();
}
/**
* Force class before profile selection once MMOCore loaded its data
*/
@EventHandler
public void onDataLoad(SynchronizedDataLoadEvent event) {
if (event.getManager().getOwningPlugin().equals(MMOCore.plugin)) {
final PlayerData playerData = (PlayerData) event.getHolder();
// Proxy-based profiles
if (!event.hasProfileEvent()) {
Validate.isTrue(MythicLib.plugin.getProfileMode() == ProfileMode.PROXY, "Listened to a data load event with no profile event attached but proxy-based profiles are disabled");
if (playerData.getProfess().equals(MMOCore.plugin.classManager.getDefaultClass()))
InventoryManager.CLASS_SELECT.newInventory(playerData, () -> event.getHolder().getMMOPlayerData().getProfileSession().markAsReady(this.key)).open();
else event.getHolder().getMMOPlayerData().getProfileSession().markAsReady(this.key);
return;
}
final ProfileSelectEvent event1 = (ProfileSelectEvent) event.getProfileEvent();
// Validate if necessary
if (playerData.getProfess().equals(MMOCore.plugin.classManager.getDefaultClass()))
InventoryManager.CLASS_SELECT.newInventory(playerData, () -> event1.validate(this)).open();
else event1.validate(this);
}
public void onProfileApply(ProfileSelectEvent event) {
final var playerData = PlayerData.get(event.getPlayerData().getPlayer());
playerData.bufferForceClassSelection(() -> {
if (playerData.getProfess().hasOption(ClassOption.DEFAULT))
InventoryManager.CLASS_SELECT.newInventory(playerData, () -> event.validate(this)).open();
else event.validate(this);
});
}
@EventHandler

View File

@ -23,8 +23,9 @@ import java.util.List;
import java.util.logging.Level;
public class ConfigManager {
public boolean overrideVanillaExp, canCreativeCast, passiveSkillsNeedBinding, cobbleGeneratorXP, saveDefaultClassInfo, splitMainExp, splitProfessionExp, disableQuestBossBar,
pvpModeEnabled, pvpModeInvulnerabilityCanDamage, forceClassSelection, enableGlobalSkillTreeGUI, enableSpecificSkillTreeGUI, waypointAutoPathCalculation, waypointLinkReciprocity;
public boolean overrideVanillaExp, canCreativeCast, passiveSkillsNeedBinding, cobbleGeneratorXP, saveDefaultClassInfo,
splitMainExp, splitProfessionExp, disableQuestBossBar, pvpModeEnabled, pvpModeInvulnerabilityCanDamage, forceClassSelection,
enableGlobalSkillTreeGUI, enableSpecificSkillTreeGUI, waypointAutoPathCalculation, waypointLinkReciprocity;
public String partyChatPrefix, noSkillBoundPlaceholder;
public ChatColor staminaFull, staminaHalf, staminaEmpty;
public long combatLogTimer, lootChestExpireTime, lootChestPlayerCooldown, globalSkillCooldown;

View File

@ -31,7 +31,7 @@ public class MMOCoreBukkit {
if (plugin.getConfig().getBoolean("vanilla-exp-redirection.enabled"))
Bukkit.getPluginManager().registerEvents(new RedirectVanillaExp(plugin.getConfig().getDouble("vanilla-exp-redirection.ratio")), plugin);
if (plugin.getConfig().getBoolean("force-class-selection") && MythicLib.plugin.hasProfiles())
if (plugin.configManager.forceClassSelection && MythicLib.plugin.hasProfiles())
new ForceClassProfileDataModule();
Bukkit.getPluginManager().registerEvents(new WaypointsListener(), plugin);