diff --git a/MCPerks/pom.xml b/MCPerks/pom.xml
index b9e1093..e6f6c36 100644
--- a/MCPerks/pom.xml
+++ b/MCPerks/pom.xml
@@ -8,7 +8,7 @@
jar
MCPerks
- LATEST
+ 3.8.2-SNAPSHOT
21
21
diff --git a/MCPerks/src/main/java/com/bencodez/mcperks/MCPerksMain.java b/MCPerks/src/main/java/com/bencodez/mcperks/MCPerksMain.java
index 8fa39b5..08cf368 100644
--- a/MCPerks/src/main/java/com/bencodez/mcperks/MCPerksMain.java
+++ b/MCPerks/src/main/java/com/bencodez/mcperks/MCPerksMain.java
@@ -60,6 +60,10 @@
* The Class Main.
*/
public class MCPerksMain extends AdvancedCorePlugin {
+ private volatile int featureLifecycle;
+ public int getFeatureLifecycle() { return featureLifecycle; }
+ private com.bencodez.mcperks.data.OrderedStorage storage;
+ public com.bencodez.mcperks.data.OrderedStorage getStorage() { return storage; }
/** The plugin. */
public static MCPerksMain plugin;
@@ -137,18 +141,95 @@ public void loadListeners() {
*
* @see org.bukkit.plugin.java.JavaPlugin#onDisable()
*/
+ private volatile java.util.concurrent.CompletableFuture shutdownPreparation;
+ private volatile java.util.concurrent.CompletionStage shutdownFinalFiles;
+ private boolean shutdownCompletionHookDispatched;
+
+ @Override
+ public void onDisable() {
+ prepareShutdown();
+ super.onDisable();
+ }
+
+ /** Capture and remove native effects on the disable owner, before any asynchronous retirement. */
+ private synchronized void prepareShutdown() {
+ if (shutdownPreparation != null) return;
+ shutdownPreparation = new java.util.concurrent.CompletableFuture<>();
+ java.util.concurrent.CompletableFuture activations = java.util.concurrent.CompletableFuture.completedFuture(null);
+ java.util.concurrent.CompletableFuture saved;
+ try {
+ featureLifecycle++;
+ if (getPerkHandler() != null) activations = getPerkHandler().retireNativeActivations().toCompletableFuture();
+ if (getMcperksServerData() != null) getMcperksServerData().retireActivationCallbacks();
+ var legacy = new ArrayList();
+ if (getPerkHandler() != null) for (Perk perk : getPerkHandler().getActivePerks()) legacy.add(perk);
+ if (getPerkHandler() != null) getPerkHandler().retireTimers();
+ legacy.removeIf(perk -> !perk.detachForShutdown());
+ if (legacy.isEmpty()) saved = java.util.concurrent.CompletableFuture.completedFuture(null);
+ else {
+ if (storage == null) throw new IllegalStateException("Shutdown persistence owner unavailable");
+ saved = storage.runtimeStorage(() -> legacy.stream().map(Perk::persistForShutdown).toList())
+ .thenCompose(stages -> java.util.concurrent.CompletableFuture.allOf(
+ stages.toArray(java.util.concurrent.CompletableFuture[]::new)));
+ }
+ } catch (Throwable failure) { saved = java.util.concurrent.CompletableFuture.failedFuture(failure); }
+ java.util.concurrent.CompletableFuture.allOf(activations, saved).whenComplete((ignored, failure) -> {
+ if (failure == null) shutdownPreparation.complete(null);
+ else shutdownPreparation.completeExceptionally(failure);
+ });
+ }
+
+ @Override public java.util.concurrent.CompletionStage onBeforeStorageShutdown() {
+ var preparation = shutdownPreparation;
+ return preparation == null ? java.util.concurrent.CompletableFuture.failedFuture(
+ new IllegalStateException("Shutdown owner preparation was not admitted")) : preparation.minimalCompletionStage();
+ }
+
+ @Override public void onShutdownComplete(java.util.concurrent.CompletionStage finalFilesStage) {
+ synchronized (this) {
+ // A duplicate/reentrant hook cannot replace the admitted final-file fence.
+ if (shutdownFinalFiles == null) shutdownFinalFiles = java.util.Objects.requireNonNull(finalFilesStage, "finalFilesStage");
+ if (shutdownCompletionHookDispatched) return;
+ shutdownCompletionHookDispatched = true;
+ }
+ onShutdownComplete(); // Preserve virtual dispatch to existing subclass hooks once.
+ }
+
+ @Override public void onShutdownComplete() {
+ var owner = storage;
+ var preparation = shutdownPreparation;
+ var files = shutdownFinalFiles;
+ if (owner != null) {
+ var prepared = preparation == null ? java.util.concurrent.CompletableFuture.completedFuture(null)
+ : preparation;
+ var finalFiles = files == null ? java.util.concurrent.CompletableFuture.completedFuture(null)
+ : files.toCompletableFuture();
+ // allOf waits for every settlement, including exceptional ones. Termination is
+ // deliberately outside these stages: closing the worker must not wait on itself.
+ java.util.concurrent.CompletableFuture.allOf(prepared, finalFiles)
+ .whenComplete((ignored, failure) -> owner.close());
+ }
+ }
+
@Override
public void onUnLoad() {
- for (Perk perk : getPerkHandler().getActivePerks()) {
- getMcperksServerData().saveOfflinePerk(perk);
- perk.deactivatePerk(perk.getActivater());
- }
+ com.bencodez.mcperks.scheduling.OwnerTasks.shutdown(this);
+ // Native user storage is retired here; all user work belongs to the earlier stage.
+ if (getMcperksServerData() != null) getMcperksServerData().saveData();
HandlerList.unregisterAll(plugin);
plugin = null;
}
@Override
public void onPostLoad() {
+ com.bencodez.mcperks.scheduling.OwnerTasks.initialize(this);
+ shutdownPreparation = null;
+ shutdownFinalFiles = null;
+ shutdownCompletionHookDispatched = false;
+ int featureEpoch = ++featureLifecycle;
+ storage = new com.bencodez.mcperks.data.OrderedStorage(getDataFolder().toPath().resolve("perk-features.dat"));
+ var runtimeReady = mcperksServerData.initialize(storage);
+ runtimeReady.exceptionally(error -> { getLogger().severe("ServerData startup reconciliation failed; activations remain unavailable"); return null; });
loadListeners();
perkHandler = new PerkHandler(this);
effectHandler = new EffectHandler(this);
@@ -172,48 +253,16 @@ public void onPostLoad() {
versionHandle = new Post123VersionHandle();
}
- getBukkitScheduler().runTaskAsynchronously(this, new Runnable() {
-
- @Override
- public void run() {
- if (getMcperksServerData().getData().isConfigurationSection("OfflinePerk")) {
- for (String uuid : getMcperksServerData().getData().getConfigurationSection("OfflinePerk")
- .getKeys(false)) {
- if (getMcperksServerData().getData().isConfigurationSection("OfflinePerk." + uuid)) {
- for (String perk : getMcperksServerData().getData()
- .getConfigurationSection("OfflinePerk." + uuid).getKeys(false)) {
-
- long timeLast = getMcperksServerData().getData()
- .getLong("OfflinePerk." + uuid + "." + perk + ".Experiaton");
- debug(uuid + "/" + perk + "/" + timeLast);
- Timestamp timestamp = null;
- if (timeLast > 0) {
- timestamp = new Timestamp(timeLast);
- timeLast = 0;
- }
- boolean activate = true;
- if (timestamp != null) {
- if (timestamp.after(new Timestamp(Calendar.getInstance().getTime().getTime()))) {
- // not activating
- activate = false;
- }
- }
- if (activate) {
- debug("Activating perk from shutdown " + perk + "/" + uuid + "/" + timeLast);
- getPerkHandler().activePerk(getPerkHandler().getPerk(perk),
- getMcperksUserManager().getMCPerksUser(UUID.fromString(uuid)),
- (int) timeLast, timestamp);
- }
-
- }
- }
- }
- }
-
- getMcperksServerData().getData().set("OfflinePerk", null);
- getMcperksServerData().saveData();
- }
- });
+ runtimeReady.thenRun(() -> {
+ if (featureEpoch != featureLifecycle || !isEnabled()) return;
+ getBukkitScheduler().runTask(this, () -> {
+ if (featureEpoch != featureLifecycle || !isEnabled()) return;
+ perkHandler.loadActivePerksConfirmed(this::restoreOfflinePerks).exceptionally(error -> {
+ getLogger().severe("Legacy perk restoration failed; activations remain unavailable: " + error.getMessage());
+ return null;
+ });
+ });
+ });
getJavascriptEngineRequests().add(new JavascriptPlaceholderRequest("MCPerks") {
@@ -278,6 +327,44 @@ public void run() {
}
plugin.getRewardHandler().addInjectedReward(new RewardInjectInt("Activations") {
+ @Override public boolean supportsAsyncRequest() { return true; }
+ @Override public boolean requiresConfiguredDataForAsync() { return true; }
+ @Override public java.util.concurrent.CompletionStage onReplayCheckpointPersisted(Reward reward, AdvancedCoreUser user,
+ String occurrenceId, String injectionKey, boolean selectionPresent) {
+ if (!selectionPresent) return java.util.concurrent.CompletableFuture.completedFuture(null);
+ return onReplayCheckpointPersisted(reward, user, occurrenceId, injectionKey);
+ }
+ @Override public boolean hasPendingReplayWork(HashMap placeholders) {
+ return Reward.hasReplaySelection(placeholders);
+ }
+ @Override public java.util.concurrent.CompletionStage