Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MCPerks/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<packaging>jar</packaging>
<name>MCPerks</name>
<properties>
<advancedcore.version>LATEST</advancedcore.version>
<advancedcore.version>3.8.2-SNAPSHOT</advancedcore.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
Expand Down
345 changes: 295 additions & 50 deletions MCPerks/src/main/java/com/bencodez/mcperks/MCPerksMain.java

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions MCPerks/src/main/java/com/bencodez/mcperks/VotingPluginHook.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,53 @@ public Object getObject(OfflinePlayer player) {
});

VotingPluginHooks.getInstance().addCustomReward(new RewardInjectInt("Activations") {
@Override public boolean supportsAsyncRequest() { return true; }
@Override public boolean requiresConfiguredDataForAsync() { return true; }
public java.util.concurrent.CompletionStage<Void> 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<String, String> placeholders) {
try { return Reward.hasReplaySelection(placeholders); }
catch (NoSuchMethodError unavailable) { return false; }
}
@Override public java.util.concurrent.CompletionStage<Object> onRewardRequestAsync(Reward reward, AdvancedCoreUser user,
org.bukkit.configuration.ConfigurationSection data, HashMap<String, String> placeholders) {
if (data.isInt(getPath()) || (isAlwaysForce() && data.contains(getPath(), true)) || isAlwaysForceNoData())
return super.onRewardRequestAsync(reward, user, data, placeholders);
if (!hasPendingReplayWork(placeholders)) return java.util.concurrent.CompletableFuture.completedFuture(null);
return onRewardRequestAsync(reward, user, 0, placeholders).thenApply(result -> (Object) result);
}

@Override
public java.util.concurrent.CompletionStage<String> onRewardRequestAsync(Reward reward, AdvancedCoreUser user, int value,
HashMap<String, String> placeholders) {
var plugin = MCPerksMain.plugin;
var target = plugin.getMcperksUserManager().getMCPerksUser(user.getPlayerName());
String identity; int selected; java.util.concurrent.CompletionStage<Void> metadata;
try {
identity = Reward.currentReplaySideEffectOccurrenceId();
selected = Integer.parseInt(Reward.replaySelection(placeholders, () -> Integer.toString(value)));
metadata = Reward.persistReplayMetadataAsync(VotingPluginMain.plugin, placeholders);
} catch (NoSuchMethodError unavailable) { return target.addActivationAsync(value).thenApply(ignored -> null); }
boolean shared = plugin.getUserManager().getDataManager().usesSharedSqlStorage(plugin.getStorageType());
return metadata.thenCompose(ignored ->
shared && identity != null ? target.addReplayActivationAsync(null, selected, "votingplugin:" + identity)
: target.addActivationAsync(selected)).thenApply(ignored -> null);
}
@Override
public java.util.concurrent.CompletionStage<Void> onReplayCheckpointPersisted(Reward reward, AdvancedCoreUser user,
String occurrenceId, String injectionKey) {
var plugin = MCPerksMain.plugin;
if (!plugin.getUserManager().getDataManager().usesSharedSqlStorage(plugin.getStorageType()))
return super.onReplayCheckpointPersisted(reward, user, occurrenceId, injectionKey);
String identity;
try { identity = Reward.replaySideEffectOccurrenceId(occurrenceId, injectionKey); }
catch (NoSuchMethodError unavailable) { return java.util.concurrent.CompletableFuture.completedFuture(null); }
if (identity == null) return super.onReplayCheckpointPersisted(reward, user, occurrenceId, injectionKey);
return plugin.getMcperksUserManager().getMCPerksUser(user.getPlayerName()).checkpointReplayActivationAsync("votingplugin:" + identity);
}

@Override
public String onRewardRequest(Reward reward, AdvancedCoreUser user, int value,
Expand All @@ -59,6 +106,53 @@ public void setVal(String key, Object value) {

for (final String perk : MCPerksMain.plugin.getPerkHandler().getLoadedPerks().keySet()) {
VotingPluginHooks.getInstance().addCustomReward(new RewardInjectInt("PerkActivations." + perk) {
@Override public boolean supportsAsyncRequest() { return true; }
@Override public boolean requiresConfiguredDataForAsync() { return true; }
public java.util.concurrent.CompletionStage<Void> 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<String, String> placeholders) {
try { return Reward.hasReplaySelection(placeholders); }
catch (NoSuchMethodError unavailable) { return false; }
}
@Override public java.util.concurrent.CompletionStage<Object> onRewardRequestAsync(Reward reward, AdvancedCoreUser user,
org.bukkit.configuration.ConfigurationSection data, HashMap<String, String> placeholders) {
if (data.isInt(getPath()) || (isAlwaysForce() && data.contains(getPath(), true)) || isAlwaysForceNoData())
return super.onRewardRequestAsync(reward, user, data, placeholders);
if (!hasPendingReplayWork(placeholders)) return java.util.concurrent.CompletableFuture.completedFuture(null);
return onRewardRequestAsync(reward, user, 0, placeholders).thenApply(result -> (Object) result);
}

@Override
public java.util.concurrent.CompletionStage<String> onRewardRequestAsync(Reward reward, AdvancedCoreUser user, int value,
HashMap<String, String> placeholders) {
var plugin = MCPerksMain.plugin;
var target = plugin.getMcperksUserManager().getMCPerksUser(user.getPlayerName());
String identity; int selected; java.util.concurrent.CompletionStage<Void> metadata;
try {
identity = Reward.currentReplaySideEffectOccurrenceId();
selected = Integer.parseInt(Reward.replaySelection(placeholders, () -> Integer.toString(value)));
metadata = Reward.persistReplayMetadataAsync(VotingPluginMain.plugin, placeholders);
} catch (NoSuchMethodError unavailable) { return target.addActivationAsync(perk, value).thenApply(ignored -> null); }
boolean shared = plugin.getUserManager().getDataManager().usesSharedSqlStorage(plugin.getStorageType());
return metadata.thenCompose(ignored ->
shared && identity != null ? target.addReplayActivationAsync(perk, selected, "votingplugin:" + identity)
: target.addActivationAsync(perk, selected)).thenApply(ignored -> null);
}
@Override
public java.util.concurrent.CompletionStage<Void> onReplayCheckpointPersisted(Reward reward, AdvancedCoreUser user,
String occurrenceId, String injectionKey) {
var plugin = MCPerksMain.plugin;
if (!plugin.getUserManager().getDataManager().usesSharedSqlStorage(plugin.getStorageType()))
return super.onReplayCheckpointPersisted(reward, user, occurrenceId, injectionKey);
String identity;
try { identity = Reward.replaySideEffectOccurrenceId(occurrenceId, injectionKey); }
catch (NoSuchMethodError unavailable) { return java.util.concurrent.CompletableFuture.completedFuture(null); }
if (identity == null) return super.onReplayCheckpointPersisted(reward, user, occurrenceId, injectionKey);
return plugin.getMcperksUserManager().getMCPerksUser(user.getPlayerName()).checkpointReplayActivationAsync("votingplugin:" + identity);
}

@Override
public String onRewardRequest(Reward reward, AdvancedCoreUser user, int value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ public class CommandLoader {
private HashMap<String, CommandHandler> commands;

private Object activationsLock = new Object();
private void reportActivationResult(CommandSender sender, java.util.concurrent.CompletionStage<Void> operation, String success) {
operation.whenComplete((ignored, failure) -> {
Runnable response = () -> sender.sendMessage(MessageAPI.colorize(failure == null ? success : "&cActivation update could not be confirmed; check the server log before retrying."));
if (failure != null) plugin.getLogger().warning("Activation update could not be confirmed");
if (sender instanceof Player player)
com.bencodez.mcperks.scheduling.OwnerTasks.run(plugin, player, () -> { if (player.isOnline()) response.run(); }, () -> {});
else plugin.getBukkitScheduler().runTask(plugin, response);
});
}
private void loadReloadCommand() {
plugin.getCommands().add(new CommandHandler(plugin, new String[] { "Reload" }, "MCPerks.Reload", "Reloads the plugin") {
@Override public void execute(CommandSender sender, String[] args) {
plugin.reloadAsync().whenComplete((ignored, error) -> {
Runnable response = () -> sender.sendMessage(MessageAPI.colorize(error == null
? "&c" + plugin.getName() + " v" + plugin.getDescription().getVersion() + " reloaded!"
: "&c" + plugin.getName() + " reload failed; check the server log."));
if (sender instanceof Player player)
com.bencodez.mcperks.scheduling.OwnerTasks.run(plugin, player,
() -> { if (player.isOnline()) response.run(); }, () -> {});
else plugin.getBukkitScheduler().runTask(plugin, response);
});
}
});
}



/**
* Instantiates a new command loader.
Expand Down Expand Up @@ -157,16 +183,7 @@ public void execute(CommandSender sender, String[] args) {
}
});

plugin.getCommands()
.add(new CommandHandler(plugin, new String[] { "Reload" }, "MCPerks.Reload", "Reloads the plugin") {

@Override
public void execute(CommandSender sender, String[] args) {
plugin.reload();
sender.sendMessage(MessageAPI.colorize(
"&c" + plugin.getName() + " v" + plugin.getDescription().getVersion() + " reloaded!"));
}
});
loadReloadCommand();

plugin.getCommands()
.add(new CommandHandler(plugin, new String[] { "Version" }, "MCPerks.Version", "List version info") {
Expand Down Expand Up @@ -588,8 +605,7 @@ public void execute(CommandSender sender, String[] args) {
public void execute(CommandSender sender, String[] args) {
synchronized (activationsLock) {
MCPerksUser user = plugin.getMcperksUserManager().getMCPerksUser(args[1]);
user.setActivations(Integer.parseInt(args[2]));
sendMessage(sender, "&cSet activations to " + args[2]);
reportActivationResult(sender, user.setActivationsAsync(Integer.parseInt(args[2])), "&cSet activations to " + args[2]);
}
}
});
Expand All @@ -601,15 +617,15 @@ public void execute(CommandSender sender, String[] args) {
public void execute(CommandSender sender, String[] args) {
synchronized (activationsLock) {
if (args[1].equals("*")) {
var pending = new java.util.ArrayList<java.util.concurrent.CompletableFuture<Void>>();
for (Player p : Bukkit.getOnlinePlayers()) {
MCPerksUser user = plugin.getMcperksUserManager().getMCPerksUser(p);
user.addActivation(Integer.parseInt(args[2]));
pending.add(user.addActivationAsync(Integer.parseInt(args[2])).toCompletableFuture());
}
sendMessage(sender, "&cAdded activations to " + args[2]);
reportActivationResult(sender, java.util.concurrent.CompletableFuture.allOf(pending.toArray(new java.util.concurrent.CompletableFuture<?>[0])), "&cAdded activations: " + args[2]);
} else {
MCPerksUser user = plugin.getMcperksUserManager().getMCPerksUser(args[1]);
user.addActivation(Integer.parseInt(args[2]));
sendMessage(sender, "&cSet activations to " + user.getActivations());
reportActivationResult(sender, user.addActivationAsync(Integer.parseInt(args[2])), "&cAdded activations: " + args[2]);
}
}
}
Expand Down Expand Up @@ -666,11 +682,7 @@ public void execute(CommandSender sender, String[] args) {
@Override
public void execute(CommandSender sender, String[] args) {
MCPerksUser user = plugin.getMcperksUserManager().getMCPerksUser(args[1]);
user.addActivation(-Integer.parseInt(args[2]));
if (user.getActivations() < 0) {
user.setActivations(0);
}
sendMessage(sender, "&cSet activations to " + user.getActivations());
reportActivationResult(sender, user.removeActivationClampedAsync(Integer.parseInt(args[2])), "&cRemoved activations: " + args[2]);
}
});

Expand All @@ -682,8 +694,7 @@ public void execute(CommandSender sender, String[] args) {
public void execute(CommandSender sender, String[] args) {
synchronized (activationsLock) {
MCPerksUser user = plugin.getMcperksUserManager().getMCPerksUser(args[1]);
user.setActivations(args[2], Integer.parseInt(args[3]));
sendMessage(sender, "&cSet activations to " + args[3]);
reportActivationResult(sender, user.setActivationsAsync(args[2], Integer.parseInt(args[3])), "&cSet activations to " + args[3]);
}
}
});
Expand All @@ -696,15 +707,15 @@ public void execute(CommandSender sender, String[] args) {
public void execute(CommandSender sender, String[] args) {
synchronized (activationsLock) {
if (args[1].equals("*")) {
var pending = new java.util.ArrayList<java.util.concurrent.CompletableFuture<Void>>();
for (Player p : Bukkit.getOnlinePlayers()) {
MCPerksUser user = plugin.getMcperksUserManager().getMCPerksUser(p);
user.addActivation(args[2], Integer.parseInt(args[3]));
pending.add(user.addActivationAsync(args[2], Integer.parseInt(args[3])).toCompletableFuture());
}
sendMessage(sender, "&cAdded activations to " + args[3]);
reportActivationResult(sender, java.util.concurrent.CompletableFuture.allOf(pending.toArray(new java.util.concurrent.CompletableFuture<?>[0])), "&cAdded activations: " + args[3]);
} else {
MCPerksUser user = plugin.getMcperksUserManager().getMCPerksUser(args[1]);
user.addActivation(args[2], Integer.parseInt(args[3]));
sendMessage(sender, "&cSet activations to " + user.getActivations(args[2]));
reportActivationResult(sender, user.addActivationAsync(args[2], Integer.parseInt(args[3])), "&cAdded activations: " + args[3]);
}
}
}
Expand All @@ -719,11 +730,7 @@ public void execute(CommandSender sender, String[] args) {
public void execute(CommandSender sender, String[] args) {
synchronized (activationsLock) {
MCPerksUser user = plugin.getMcperksUserManager().getMCPerksUser(args[1]);
user.addActivation(-Integer.parseInt(args[3]));
if (user.getActivations(args[2]) < 0) {
user.setActivations(args[2], 0);
}
sendMessage(sender, "&cSet activations to " + user.getActivations());
reportActivationResult(sender, user.removeActivationClampedAsync(args[2], Integer.parseInt(args[3])), "&cRemoved activations: " + args[3]);
}
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.bencodez.mcperks.data;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.*;
/** Forces staged contents and atomic publication. A post-move error has an uncertain outcome. */
public final class DurableFeatureFiles {
private DurableFeatureFiles() {}
@FunctionalInterface public interface Publisher { void publish(Path staged, Path file) throws IOException; }
/** Reconfirm an already replaced file without executing its mutation or moving it again. */
public static void settle(Path file) throws IOException {
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) { channel.force(true); }
forceDirectory(file);
}
private static void forceDirectory(Path file) throws IOException {
try (FileChannel channel = FileChannel.open(file.toAbsolutePath().getParent(), StandardOpenOption.READ)) { channel.force(true); }
catch (AccessDeniedException error) { if (!System.getProperty("os.name", "").startsWith("Windows")) throw error; }
catch (UnsupportedOperationException ignored) { /* Provider does not expose directory forcing. */ }
}
public static void publish(Path staged, Path file) throws IOException {
try (FileChannel channel = FileChannel.open(staged, StandardOpenOption.WRITE)) { channel.force(true); }
Files.move(staged, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
forceDirectory(file);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.bencodez.mcperks.data;
import java.nio.file.*;
import java.nio.channels.*;
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
/** Storage workers retain an OS lock through drain, including across plugin classloader replacement. */
public final class FeatureFileOwnership {
private static final Map<Path, CompletableFuture<Void>> owners = new HashMap<>();
private final Path path;
private final CompletableFuture<Void> predecessor, drained = new CompletableFuture<>();
private FileChannel channel;
private FileLock lock;
public FeatureFileOwnership(Path file) {
path = file.toAbsolutePath().normalize();
synchronized (owners) { predecessor = owners.put(path, drained); }
}
public synchronized void awaitPredecessor() {
if (predecessor != null) predecessor.join();
try {
Files.createDirectories(path.getParent());
Path guard = path.resolveSibling(path.getFileName() + ".lock");
channel = FileChannel.open(guard, StandardOpenOption.CREATE, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
while (lock == null) {
try { lock = channel.tryLock(); } catch (OverlappingFileLockException busy) { }
if (lock != null) return;
if (System.nanoTime() >= deadline) throw new IOException("Predecessor storage owner has not drained");
try { TimeUnit.MILLISECONDS.sleep(25); }
catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new IOException("Storage ownership interrupted", interrupted); }
}
} catch (IOException failure) {
try { if (channel != null) channel.close(); } catch (IOException cleanup) { failure.addSuppressed(cleanup); }
channel = null;
throw new UncheckedIOException(failure);
}
}
public synchronized void drained() {
if (drained.isDone()) return;
try {
if (channel != null) channel.close(); // Releases the lock before publishing drain completion.
lock = null; channel = null; drained.complete(null);
} catch (IOException failure) { drained.completeExceptionally(failure); }
synchronized (owners) { owners.remove(path, drained); }
// Keep the empty lock file: unlinking it could let another classloader lock a different inode.
}
}
Loading