|
19 | 19 | import java.util.List; |
20 | 20 | import java.util.Map.Entry; |
21 | 21 | import java.util.Set; |
| 22 | +import java.util.concurrent.CompletableFuture; |
| 23 | +import java.util.concurrent.CompletionStage; |
22 | 24 | import java.util.concurrent.ThreadLocalRandom; |
| 25 | +import java.util.concurrent.TimeUnit; |
| 26 | +import java.util.concurrent.TimeoutException; |
| 27 | +import java.util.concurrent.atomic.AtomicBoolean; |
23 | 28 | import java.util.regex.Pattern; |
24 | 29 |
|
25 | 30 | import org.bukkit.Bukkit; |
|
38 | 43 | import org.bukkit.potion.PotionEffectType; |
39 | 44 |
|
40 | 45 | import com.bencodez.advancedcore.AdvancedCorePlugin; |
| 46 | +import com.bencodez.advancedcore.api.rewards.Reward; |
41 | 47 | import com.bencodez.advancedcore.api.item.ItemBuilder; |
42 | 48 | import com.bencodez.advancedcore.api.messages.PlaceholderUtils; |
43 | 49 | import com.bencodez.advancedcore.api.user.AdvancedCoreUser; |
@@ -237,28 +243,64 @@ public void executeConsoleCommands(final String playerName, final ArrayList<Stri |
237 | 243 | } |
238 | 244 |
|
239 | 245 | 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 | + } |
258 | 248 |
|
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); |
260 | 275 | } |
| 276 | + } |
261 | 277 |
|
| 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 | + } |
262 | 304 | } |
263 | 305 |
|
264 | 306 | public Object getBlockMeta(Block block, String str) { |
@@ -466,6 +508,42 @@ private void runConsoleCommand(String command, int delay, boolean hasDelay) { |
466 | 508 | consoleCommands.dispatch(command, delay, hasDelay); |
467 | 509 | } |
468 | 510 |
|
| 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 | + |
469 | 547 | private String stripLeadingSlash(String command) { |
470 | 548 | if (command != null && command.startsWith("/")) { |
471 | 549 | return command.substring(1); |
|
0 commit comments