Skip to content

Commit 3de4b66

Browse files
authored
Merge pull request #317 from BenCodez/codex/async-reward-injections
Add ordered async reward injection support
2 parents 8b5b909 + f225cd4 commit 3de4b66

54 files changed

Lines changed: 10091 additions & 294 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AdvancedCore/pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
<version>3.6.1</version>
9292
<configuration>
9393
<shadedArtifactAttached>false</shadedArtifactAttached>
94-
<minimizeJar>true</minimizeJar>
94+
<minimizeJar>false</minimizeJar>
9595
</configuration>
9696
<executions>
9797
<execution>

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/api/item/FullInventoryHandler.java‎

Lines changed: 356 additions & 21 deletions
Large diffs are not rendered by default.

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/api/misc/MiscUtils.java‎

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@
1919
import java.util.List;
2020
import java.util.Map.Entry;
2121
import java.util.Set;
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.CompletionStage;
2224
import java.util.concurrent.ThreadLocalRandom;
25+
import java.util.concurrent.TimeUnit;
26+
import java.util.concurrent.TimeoutException;
27+
import java.util.concurrent.atomic.AtomicBoolean;
2328
import java.util.regex.Pattern;
2429

2530
import org.bukkit.Bukkit;
@@ -38,6 +43,7 @@
3843
import org.bukkit.potion.PotionEffectType;
3944

4045
import com.bencodez.advancedcore.AdvancedCorePlugin;
46+
import com.bencodez.advancedcore.api.rewards.Reward;
4147
import com.bencodez.advancedcore.api.item.ItemBuilder;
4248
import com.bencodez.advancedcore.api.messages.PlaceholderUtils;
4349
import com.bencodez.advancedcore.api.user.AdvancedCoreUser;
@@ -237,28 +243,64 @@ public void executeConsoleCommands(final String playerName, final ArrayList<Stri
237243
}
238244

239245
public void executeConsoleCommands(String playerName, String command, HashMap<String, String> placeholders) {
240-
if (command != null && !command.isEmpty()) {
241-
OfflinePlayer p = Bukkit.getOfflinePlayer(playerName);
242-
if (p != null) {
243-
command = PlaceholderUtils.replaceJavascriptOnly(p, command);
244-
}
245-
command = PlaceholderUtils.replacePlaceHolder(command, placeholders);
246-
if (p != null) {
247-
command = PlaceholderUtils.replacePlaceHolders(p, command);
248-
}
249-
final String cmd = stripLeadingSlash(command);
250-
251-
plugin.debug("Executing console command: " + command);
252-
plugin.getBukkitScheduler().executeOrScheduleSync(plugin, new Runnable() {
253-
254-
@Override
255-
public void run() {
256-
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), cmd);
257-
}
246+
executeConsoleCommandsAsync(playerName, command, placeholders);
247+
}
258248

259-
});
249+
/**
250+
* Schedules one console command and completes only after Bukkit has dispatched
251+
* it. Async reward replay uses this completion boundary so it never records a
252+
* durable checkpoint ahead of a queued command.
253+
*/
254+
public CompletionStage<Void> executeConsoleCommandsAsync(String playerName, String command,
255+
HashMap<String, String> placeholders) {
256+
try {
257+
ArrayList<String> templates = command == null || command.isEmpty()
258+
? new ArrayList<>() : new ArrayList<>(java.util.List.of(command));
259+
String expanded = command;
260+
OfflinePlayer p = Bukkit.getOfflinePlayer(playerName);
261+
if (expanded != null && !expanded.isEmpty()) {
262+
if (p != null) expanded = PlaceholderUtils.replaceJavascriptOnly(p, expanded);
263+
expanded = PlaceholderUtils.replacePlaceHolder(expanded, placeholders);
264+
if (p != null) expanded = PlaceholderUtils.replacePlaceHolders(p, expanded);
265+
}
266+
ArrayList<String> commands = command == null || command.isEmpty()
267+
? new ArrayList<>() : new ArrayList<>(java.util.List.of(expanded == null ? "" : expanded));
268+
return Reward.replayCommandSequence(plugin, placeholders, "console", templates, commands,
269+
(cmd, ignoredIndex) -> {
270+
plugin.debug("Executing console command: " + cmd);
271+
return runConsoleCommandAsync(stripLeadingSlash(cmd), 0, false, false);
272+
});
273+
} catch (Throwable failure) {
274+
return CompletableFuture.failedFuture(failure);
260275
}
276+
}
261277

278+
/**
279+
* Schedules every console command using the legacy stagger timing and completes
280+
* only after every dispatch has run. This keeps an async reward injector from
281+
* checkpointing a command list while any member is still queued.
282+
*/
283+
@SuppressWarnings("deprecation")
284+
public CompletionStage<Void> executeConsoleCommandsAsync(final String playerName, final ArrayList<String> cmds,
285+
final HashMap<String, String> placeholders, final boolean stagger) {
286+
try {
287+
placeholders.put("player", playerName);
288+
OfflinePlayer p = Bukkit.getOfflinePlayer(playerName);
289+
ArrayList<String> templates = cmds == null ? new ArrayList<>() : new ArrayList<>(cmds);
290+
ArrayList<String> commands = new ArrayList<>(templates);
291+
if (p != null) commands = PlaceholderUtils.replaceJavascriptOnly(p, commands);
292+
commands = PlaceholderUtils.replacePlaceHolder(commands, placeholders);
293+
if (p != null) commands = PlaceholderUtils.replacePlaceHolders(p, commands);
294+
295+
return Reward.replayCommandSequence(plugin, placeholders, "console", templates, commands,
296+
(command, index) -> {
297+
plugin.debug("Executing console command: " + command);
298+
return runConsoleCommandAsync(stripLeadingSlash(command), index > 0 ? 1 : 0,
299+
stagger && index > 0, stagger || index == 0);
300+
});
301+
} catch (Throwable failure) {
302+
return CompletableFuture.failedFuture(failure);
303+
}
262304
}
263305

264306
public Object getBlockMeta(Block block, String str) {
@@ -466,6 +508,42 @@ private void runConsoleCommand(String command, int delay, boolean hasDelay) {
466508
consoleCommands.dispatch(command, delay, hasDelay);
467509
}
468510

511+
/**
512+
* Schedules one command with a completion boundary. The timeout claims the
513+
* runnable before it can execute, so a task delayed past shutdown cannot run
514+
* after a failed replay has already been retried.
515+
*/
516+
private CompletionStage<Void> runConsoleCommandAsync(String command, int delay, boolean hasDelay,
517+
boolean scheduleAsTask) {
518+
CompletableFuture<Void> completion = new CompletableFuture<>();
519+
AtomicBoolean claimed = new AtomicBoolean();
520+
Runnable dispatch = () -> {
521+
if (!claimed.compareAndSet(false, true)) return;
522+
try {
523+
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), command);
524+
completion.complete(null);
525+
} catch (Throwable failure) {
526+
completion.completeExceptionally(failure);
527+
}
528+
};
529+
try {
530+
if (hasDelay && delay > 0) plugin.getBukkitScheduler().runTaskLater(plugin, dispatch, delay);
531+
else if (scheduleAsTask) plugin.getBukkitScheduler().runTask(plugin, dispatch);
532+
else plugin.getBukkitScheduler().executeOrScheduleSync(plugin, dispatch);
533+
} catch (Throwable failure) {
534+
claimed.set(true);
535+
completion.completeExceptionally(failure);
536+
return completion;
537+
}
538+
long timeoutSeconds = 30L + Math.max(0, delay);
539+
CompletableFuture.delayedExecutor(timeoutSeconds, TimeUnit.SECONDS).execute(() -> {
540+
if (claimed.compareAndSet(false, true)) {
541+
completion.completeExceptionally(new TimeoutException("Timed out waiting for scheduled console command"));
542+
}
543+
});
544+
return completion;
545+
}
546+
469547
private String stripLeadingSlash(String command) {
470548
if (command != null && command.startsWith("/")) {
471549
return command.substring(1);

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/QueuedGeneratedReward.java‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,25 @@ public final class QueuedGeneratedReward extends Reward {
2323

2424
@Override
2525
public void giveReward(AdvancedCoreUser user, RewardOptions rewardOptions) {
26-
if (user == null || user.getUUID() == null || !allowedUserUuids.contains(user.getUUID())) {
27-
plugin.getLogger().warning("Blocked generated queued reward " + getRewardName()
28-
+ " for a user without a matching persisted queue entry");
29-
return;
30-
}
26+
if (!isAllowed(user)) return;
3127
super.giveReward(user, rewardOptions);
3228
}
3329

30+
@Override
31+
public java.util.concurrent.CompletionStage<Void> giveRewardAsync(AdvancedCoreUser user,
32+
RewardOptions rewardOptions) {
33+
if (!isAllowed(user)) return java.util.concurrent.CompletableFuture.failedFuture(
34+
new IllegalStateException("Generated queued reward is not authorized for this user"));
35+
return super.giveRewardAsync(user, rewardOptions);
36+
}
37+
38+
private boolean isAllowed(AdvancedCoreUser user) {
39+
if (user != null && user.getUUID() != null && allowedUserUuids.contains(user.getUUID())) return true;
40+
plugin.getLogger().warning("Blocked generated queued reward " + getRewardName()
41+
+ " for a user without a matching persisted queue entry");
42+
return false;
43+
}
44+
3445
@Override
3546
public boolean isGeneratedSnapshotCreated() {
3647
// A loaded generated snapshot must retain snapshot provenance if execution is

0 commit comments

Comments
 (0)