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> <dependency>
<groupId>fr.phoenixdevt</groupId> <groupId>fr.phoenixdevt</groupId>
<artifactId>Profile-API</artifactId> <artifactId>Profile-API</artifactId>
<version>1.2</version> <version>1.2-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>

View File

@ -68,6 +68,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -220,9 +221,36 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
this.getStats().updateStats(); this.getStats().updateStats();
PartyUtils.resolvePartyBonuses(this); // In case buffs not removed on logoff PartyUtils.resolvePartyBonuses(this); // In case buffs not removed on logoff
tryForceClassSelection(); // Force class selection
getMMOPlayerData().getProfileSession().addOpenCallback(session -> this.onProfileSessionReady()); 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() { private void castOnLoginScripts() {
// Class Skills // Class Skills
@ -499,7 +527,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
/** /**
* @return If the item is unlocked by the player * @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) { public boolean hasUnlocked(Unlockable unlockable) {
return unlockable.isUnlockedByDefault() || unlockedItems.contains(unlockable.getUnlockNamespacedKey()); 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. * @param key The identifier of an exp table item.
* @return Amount of times an item has been claimed * @return Amount of times an item has been claimed
* inside an experience table. * inside an experience table.
*/ */
public int getClaims(@NotNull String key) { public int getClaims(@NotNull String key) {
return tableItemClaims.getOrDefault(key, 0); return tableItemClaims.getOrDefault(key, 0);
@ -1138,7 +1166,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
/** /**
* @return If the PlayerEnterCastingModeEvent successfully put the player * @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() { public boolean setSkillCasting() {
Validate.isTrue(!isCasting(), "Player already in casting mode"); 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 * @return If player successfully left skill casting i.e the Bukkit
* event has not been cancelled * event has not been cancelled
*/ */
public boolean leaveSkillCasting() { public boolean leaveSkillCasting() {
return leaveSkillCasting(false); return leaveSkillCasting(false);
@ -1166,7 +1194,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
/** /**
* @param skipEvent Skip firing the exit event * @param skipEvent Skip firing the exit event
* @return If player successfully left skill casting i.e the Bukkit * @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) { public boolean leaveSkillCasting(boolean skipEvent) {
Validate.isTrue(isCasting(), "Player not in casting mode"); 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 * checks if they could potentially upgrade to one of these
* *
* @return If the player can change its current class to * @return If the player can change its current class to
* a subclass * a subclass
*/ */
@Deprecated @Deprecated
public boolean canChooseSubclass() { 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.ProfileSelectEvent;
import fr.phoenixdevt.profiles.event.ProfileUnloadEvent; import fr.phoenixdevt.profiles.event.ProfileUnloadEvent;
import io.lumine.mythic.lib.MythicLib; 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.comp.profile.ProfileMode;
import io.lumine.mythic.lib.util.lang3.Validate; import io.lumine.mythic.lib.util.lang3.Validate;
import net.Indyuce.mmocore.MMOCore; import net.Indyuce.mmocore.MMOCore;
import net.Indyuce.mmocore.api.player.PlayerData; import net.Indyuce.mmocore.api.player.PlayerData;
import net.Indyuce.mmocore.api.player.profess.ClassOption;
import net.Indyuce.mmocore.manager.InventoryManager; import net.Indyuce.mmocore.manager.InventoryManager;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
@ -47,7 +47,7 @@ public class ForceClassProfileDataModule implements ProfileDataModule {
@EventHandler @EventHandler
public void onProfileCreate(ProfileCreateEvent event) { public void onProfileCreate(ProfileCreateEvent event) {
// Proxy-based profiles // Will be prompted on profile application in proxy-mode
if (MythicLib.plugin.getProfileMode() == ProfileMode.PROXY) { if (MythicLib.plugin.getProfileMode() == ProfileMode.PROXY) {
event.validate(this); event.validate(this);
return; return;
@ -57,30 +57,14 @@ public class ForceClassProfileDataModule implements ProfileDataModule {
InventoryManager.CLASS_SELECT.newInventory(playerData, () -> event.validate(this)).open(); InventoryManager.CLASS_SELECT.newInventory(playerData, () -> event.validate(this)).open();
} }
/**
* Force class before profile selection once MMOCore loaded its data
*/
@EventHandler @EventHandler
public void onDataLoad(SynchronizedDataLoadEvent event) { public void onProfileApply(ProfileSelectEvent event) {
if (event.getManager().getOwningPlugin().equals(MMOCore.plugin)) { final var playerData = PlayerData.get(event.getPlayerData().getPlayer());
final PlayerData playerData = (PlayerData) event.getHolder(); playerData.bufferForceClassSelection(() -> {
if (playerData.getProfess().hasOption(ClassOption.DEFAULT))
// Proxy-based profiles InventoryManager.CLASS_SELECT.newInventory(playerData, () -> event.validate(this)).open();
if (!event.hasProfileEvent()) { else event.validate(this);
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);
}
} }
@EventHandler @EventHandler

View File

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

View File

@ -31,7 +31,7 @@ public class MMOCoreBukkit {
if (plugin.getConfig().getBoolean("vanilla-exp-redirection.enabled")) if (plugin.getConfig().getBoolean("vanilla-exp-redirection.enabled"))
Bukkit.getPluginManager().registerEvents(new RedirectVanillaExp(plugin.getConfig().getDouble("vanilla-exp-redirection.ratio")), plugin); 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(); new ForceClassProfileDataModule();
Bukkit.getPluginManager().registerEvents(new WaypointsListener(), plugin); Bukkit.getPluginManager().registerEvents(new WaypointsListener(), plugin);