Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
import com.bencodez.advancedcore.AdvancedCorePlugin;
import com.bencodez.advancedcore.api.rewards.Reward;
import com.bencodez.advancedcore.api.item.ItemBuilder;
import com.bencodez.advancedcore.api.javascript.JavascriptPlaceholderValue;
import com.bencodez.advancedcore.api.javascript.JavascriptTextTemplate;
import com.bencodez.advancedcore.api.messages.PlaceholderUtils;
import com.bencodez.advancedcore.api.user.AdvancedCoreUser;
import com.bencodez.advancedcore.bukkit.platform.BukkitPlatformServices;
Expand Down Expand Up @@ -244,16 +246,7 @@ public void run() {
public void executeConsoleCommands(final String playerName, final ArrayList<String> cmds,
final HashMap<String, String> placeholders, boolean stagger) {
if (cmds != null && !cmds.isEmpty()) {
placeholders.put("player", playerName);
OfflinePlayer p = Bukkit.getOfflinePlayer(playerName);
ArrayList<String> commands = cmds;
if (p != null) {
commands = PlaceholderUtils.replaceJavascriptOnly(p, commands);
}
commands = PlaceholderUtils.replacePlaceHolder(commands, placeholders);
if (p != null) {
commands = PlaceholderUtils.replacePlaceHolders(p, commands);
}
ArrayList<String> commands = expandConsoleCommands(playerName, cmds, placeholders, true);
int tick = 0;
for (final String cmd : commands) {
plugin.debug("Executing console command: " + cmd);
Expand All @@ -264,6 +257,10 @@ public void executeConsoleCommands(final String playerName, final ArrayList<Stri
}

public void executeConsoleCommands(String playerName, String command, HashMap<String, String> placeholders) {
if ((playerName == null || playerName.isBlank()) && !Reward.hasReplayCommandSnapshot(placeholders, "console")) {
expandConsoleCommands(playerName, command == null || command.isEmpty()
? new ArrayList<>() : new ArrayList<>(java.util.List.of(command)), placeholders, false);
}
executeConsoleCommandsAsync(playerName, command, placeholders);
}

Expand All @@ -284,15 +281,8 @@ public CompletionStage<Void> executeConsoleCommandsAsync(String playerName, Stri
try {
ArrayList<String> templates = command == null || command.isEmpty()
? new ArrayList<>() : new ArrayList<>(java.util.List.of(command));
String expanded = command;
OfflinePlayer p = Bukkit.getOfflinePlayer(playerName);
if (expanded != null && !expanded.isEmpty()) {
if (p != null) expanded = PlaceholderUtils.replaceJavascriptOnly(p, expanded);
expanded = PlaceholderUtils.replacePlaceHolder(expanded, placeholders);
if (p != null) expanded = PlaceholderUtils.replacePlaceHolders(p, expanded);
}
ArrayList<String> commands = command == null || command.isEmpty()
? new ArrayList<>() : new ArrayList<>(java.util.List.of(expanded == null ? "" : expanded));
ArrayList<String> commands = Reward.hasReplayCommandSnapshot(placeholders, "console")
? new ArrayList<>() : expandConsoleCommands(playerName, templates, placeholders, false);
return Reward.replayCommandSequence(plugin, placeholders, "console", templates, commands,
(cmd, ignoredIndex) -> {
plugin.debug("Executing console command: " + cmd);
Expand All @@ -319,16 +309,20 @@ public CompletionStage<Void> executeConsoleCommandsAsync(final String playerName
public CompletionStage<Void> executeConsoleCommandsAsync(final String playerName, final ArrayList<String> cmds,
final HashMap<String, String> placeholders, final boolean stagger,
AdvancedCoreUser.PlayerEligibilityGuard eligibilityGuard) {
return executeConsoleCommandsAsync(playerName, cmds, placeholders, stagger, eligibilityGuard,
Reward.currentReplayState(), Reward.currentReplayKey());
}

/** Carries the replay context across asynchronous availability checks. */
public CompletionStage<Void> executeConsoleCommandsAsync(final String playerName, final ArrayList<String> cmds,
final HashMap<String, String> placeholders, final boolean stagger,
AdvancedCoreUser.PlayerEligibilityGuard eligibilityGuard, Reward.ReplayState replayState, String replayKey) {
try {
placeholders.put("player", playerName);
OfflinePlayer p = Bukkit.getOfflinePlayer(playerName);
ArrayList<String> templates = cmds == null ? new ArrayList<>() : new ArrayList<>(cmds);
ArrayList<String> commands = new ArrayList<>(templates);
if (p != null) commands = PlaceholderUtils.replaceJavascriptOnly(p, commands);
commands = PlaceholderUtils.replacePlaceHolder(commands, placeholders);
if (p != null) commands = PlaceholderUtils.replacePlaceHolders(p, commands);
ArrayList<String> commands = Reward.hasReplayCommandSnapshot(placeholders, "console", replayState, replayKey)
? new ArrayList<>() : expandConsoleCommands(playerName, templates, placeholders, true);

return Reward.replayCommandSequence(plugin, placeholders, "console", templates, commands,
return Reward.replayCommandSequence(plugin, placeholders, "console", templates, commands, replayState, replayKey,
(command, index) -> {
plugin.debug("Executing console command: " + command);
return runConsoleCommandAsync(stripLeadingSlash(command), index > 0 ? 1 : 0,
Expand All @@ -339,6 +333,92 @@ public CompletionStage<Void> executeConsoleCommandsAsync(final String playerName
}
}

@SuppressWarnings("deprecation")
private ArrayList<String> expandConsoleCommands(String playerName, List<String> templates,
HashMap<String, String> placeholders, boolean addPlayer) {
ArrayList<String> commands = new ArrayList<>(templates);
if (commands.isEmpty()) return commands;
boolean hasName = playerName != null && !playerName.isBlank();
if (hasName) {
if (addPlayer) placeholders.put("player", playerName);
OfflinePlayer player = Bukkit.getOfflinePlayer(playerName);
if (player != null) commands = PlaceholderUtils.replaceJavascriptOnly(player, commands);
commands = PlaceholderUtils.replacePlaceHolder(commands, placeholders);
if (player != null) commands = PlaceholderUtils.replacePlaceHolders(player, commands);
return commands;
}
for (String command : commands) {
if (command != null && (containsPlayerNamePlaceholder(command)
|| (plugin.getOptions().isJavascriptEngineEnabled()
&& containsJavascriptSegment(command)))) {
throw new IllegalStateException("Console command requires an unavailable player identity");
}
}
// Trace identity aliases through map substitutions before an empty value can
// erase them. Other map entries may themselves introduce an alias.
String marker = "__advancedcore_missing_name_" + java.util.UUID.randomUUID() + "__";
HashMap<String, String> guarded = placeholders == null ? new LinkedHashMap<>() : new LinkedHashMap<>(placeholders);
guarded.replaceAll((key, value) -> key != null && (key.equalsIgnoreCase("player") || key.equalsIgnoreCase("@p"))
? marker : value);
guarded.put("player", marker);
guarded.put("@p", marker);
String encodedMarker = JavascriptPlaceholderValue.encode(marker);
for (String command : PlaceholderUtils.replacePlaceHolder(commands, guarded)) {
if (command != null && (command.contains(marker) || command.contains(encodedMarker)
|| containsPlayerNamePlaceholder(command))) {
throw new IllegalStateException("Console command requires an unavailable player identity");
}
}
commands = PlaceholderUtils.replacePlaceHolder(commands, placeholders);
for (String command : commands) {
if (command != null && (containsPlayerNamePlaceholder(command)
|| (plugin.isPlaceHolderAPIEnabled() && containsProcessedExternalPlaceholder(command)))) {
throw new IllegalStateException("Console command placeholders require an unavailable player identity");
}
}
return commands;
}

private static boolean containsPlayerNamePlaceholder(String command) {
return Pattern.compile("(?i)%(?:player|@p)%|\\{(?:player|@p)\\}").matcher(command).find();
}

private static boolean containsJavascriptSegment(String command) {
AtomicBoolean found = new AtomicBoolean();
JavascriptTextTemplate.parse(command).transform(Function.identity(), script -> {
found.set(true);
return script;
});
return found.get();
}

/** PlaceholderAPI processes literal segments, not authored JavaScript bodies. */
private static boolean containsProcessedExternalPlaceholder(String command) {
AtomicBoolean found = new AtomicBoolean();
JavascriptTextTemplate.parse(command).transform(text -> {
if (containsExternalPlaceholder(text)) found.set(true);
return text;
}, Function.identity());
return found.get();
}

/** Matches the percent-delimited expansion boundaries used by PlaceholderAPI. */
private static boolean containsExternalPlaceholder(String text) {
int start = text.indexOf('%');
while (start >= 0) {
int end = text.indexOf('%', start + 1);
if (end < 0) return false;
boolean abandoned = false;
for (int index = start + 1; index < end; index++) {
char c = text.charAt(index);
if (c == '_') return true;
if (c == ' ') { abandoned = true; break; }
}
start = text.indexOf('%', abandoned ? start + 1 : end + 1);
}
return false;
}

public Object getBlockMeta(Block block, String str) {
for (MetadataValue meta : block.getMetadata(str)) {
if (meta.getOwningPlugin().equals(plugin)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ public boolean hasPermission(Player player, String perm) {
* @return true, if successful
*/
public boolean hasPermission(String playerName, String perm) {
if (playerName == null) {
if (playerName == null || playerName.isBlank()) {
return false;
}
Player player = Bukkit.getPlayer(playerName);
Expand All @@ -233,7 +233,7 @@ public boolean hasPermission(String playerName, String perm) {
* @return true, if successful
*/
public boolean hasServerPermission(String playerName, String perm) {
if (playerName == null) {
if (playerName == null || playerName.isBlank()) {
return false;
}
Player player = Bukkit.getPlayer(playerName);
Expand All @@ -258,7 +258,7 @@ public boolean hasServerPermission(UUID playerUUID, String playername, String pe
Player player = Bukkit.getPlayer(playerUUID);
if (player != null) {
return player.hasPermission(perm);
} else {
} else if (playername != null && !playername.isBlank()) {
player = Bukkit.getPlayer(playername);
if (player != null && playerUUID.equals(player.getUniqueId())) {
return player.hasPermission(perm);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ public String getRewardName() {
public void giveInjectedRewards(AdvancedCoreUser user, HashMap<String, String> placeholders) {
if (RewardEligibility.isConfigured(plugin, getConfig().getConfigData())) {
giveRewardUserAsync(user, placeholders, new RewardOptions().setCheckTimed(false)).exceptionally(failure -> {
logRewardUserFailure(failure);
logRewardUserFailure(failure, null);
return null;
});
return;
Expand Down Expand Up @@ -858,8 +858,12 @@ private static String replaySequenceKey(String prefix, String activeKey, String

/** Returns whether the active replay already froze this logical command lane. */
public static boolean hasReplayCommandSnapshot(HashMap<String, String> placeholders, String lane) {
ReplayState replayState = currentReplayState();
String activeKey = currentReplayKey();
return hasReplayCommandSnapshot(placeholders, lane, currentReplayState(), currentReplayKey());
}

/** Checks a captured replay context after an asynchronous continuation. */
public static boolean hasReplayCommandSnapshot(HashMap<String, String> placeholders, String lane,
ReplayState replayState, String activeKey) {
if (replayState == null || activeKey == null) return false;
String storageKey = replaySequenceKey(REPLAY_COMMAND_PREFIX, activeKey,
lane == null ? "commands" : lane);
Expand Down Expand Up @@ -1481,8 +1485,10 @@ public void giveReward(AdvancedCoreUser user, RewardOptions rewardOptions) {
// player owner. Unconfigured legacy definitions retain their established path.
if (RewardEligibility.isConfigured(plugin, getConfig().getConfigData())
&& (rewardOptions == null || !rewardOptions.isIgnoreRequirements())) {
giveRewardAsync(user, rewardOptions).exceptionally(failure -> {
logRewardUserFailure(failure);
RewardOptions tracked = (rewardOptions == null ? new RewardOptions() : rewardOptions)
.captureLegacyFailureObserver();
giveRewardAsync(user, tracked).exceptionally(failure -> {
logRewardUserFailure(failure, tracked.legacyFailureObserverCapture().get());
return null;
});
return;
Expand Down Expand Up @@ -1612,6 +1618,11 @@ public void giveReward(AdvancedCoreUser user, RewardOptions rewardOptions) {
* @return completion stage for the complete reward injection chain
*/
public CompletionStage<Void> giveRewardAsync(AdvancedCoreUser user, RewardOptions rewardOptions) {
// A retained event options object can be used for a separate explicit dispatch.
// Only the original legacy invocation may update its terminal observer capture.
if (rewardOptions != null && !rewardOptions.claimLegacyFailureObserverCapture()) {
rewardOptions = rewardOptions.withoutLegacyFailureObserverCapture();
}
boolean primaryThread = false;
try {
primaryThread = Bukkit.isPrimaryThread();
Expand Down Expand Up @@ -1677,6 +1688,7 @@ private CompletionStage<Void> giveRewardAsyncOffPrimary(AdvancedCoreUser user, R

PlayerRewardEvent event = new PlayerRewardEvent(this, user, rewardOptions);
Bukkit.getPluginManager().callEvent(event);
rewardOptions.updateLegacyFailureObserverCapture();
if (event.isCancelled()) {
plugin.debug("Reward " + name + " was cancelled for " + user.getPlayerName());
return CompletableFuture.completedFuture(null);
Expand Down Expand Up @@ -1913,8 +1925,9 @@ private RewardReplayFailure replayFailureWithSnapshot(RewardOptions options, Thr
public void giveRewardUser(AdvancedCoreUser user, HashMap<String, String> phs, RewardOptions rewardOptions) {
RewardOptions effectiveOptions = rewardOptions == null ? new RewardOptions() : rewardOptions;
if (requiresAwaitedDelivery(effectiveOptions)) {
Consumer<Throwable> observer = effectiveOptions.getAsyncFailureObserver();
giveRewardUserAsync(user, phs, effectiveOptions).exceptionally(failure -> {
logRewardUserFailure(failure);
logRewardUserFailure(failure, observer);
return null;
});
return;
Expand Down Expand Up @@ -2033,7 +2046,8 @@ private HashMap<String, String> prepareRewardUser(AdvancedCoreUser user, HashMap

Player player = user.getPlayer();
if (player == null) {
player = Bukkit.getPlayer(user.getPlayerName());
String playerName = user.getPlayerName();
if (playerName != null && !playerName.isBlank()) player = Bukkit.getPlayer(playerName);
}
if (player != null || isForceOffline()) {

Expand Down Expand Up @@ -2066,7 +2080,14 @@ private HashMap<String, String> prepareRewardUser(AdvancedCoreUser user, HashMap
}
}

private void logRewardUserFailure(Throwable failure) {
private void logRewardUserFailure(Throwable failure, Consumer<Throwable> observer) {
if (observer != null) {
try {
observer.accept(failure);
} catch (Throwable ignored) {
// Best-effort diagnostics must not suppress or replace the original failure.
}
}
if (plugin.getLogger() != null) {
plugin.getLogger().log(Level.WARNING, "Failed to give reward " + getRewardName(), failure);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Consumer;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicBoolean;

import com.bencodez.advancedcore.AdvancedCorePlugin;
import com.bencodez.simpleapi.array.ArrayUtils;
Expand Down Expand Up @@ -100,6 +102,47 @@ public class RewardOptions {
@Setter
private Consumer<Reward.ReplayCheckpoint> asyncReplayCheckpointConsumer;

/** Optional best-effort observer for a legacy fire-and-forget terminal failure.
* Invoked on the completion thread; it must not block or assume Bukkit ownership. */
@Getter
@Setter
private Consumer<Throwable> asyncFailureObserver;

/** Per-legacy-dispatch handoff; never shared between separately requested rewards. */
private LegacyFailureObserverCapture legacyFailureObserverCapture;

private static final class LegacyFailureObserverCapture {
private final AtomicReference<Consumer<Throwable>> observer;
private final AtomicBoolean claimed = new AtomicBoolean();
private LegacyFailureObserverCapture(Consumer<Throwable> observer) {
this.observer = new AtomicReference<>(observer);
}
}

RewardOptions captureLegacyFailureObserver() {
RewardOptions copy = copyForDispatch();
copy.legacyFailureObserverCapture = new LegacyFailureObserverCapture(copy.asyncFailureObserver);
return copy;
}

AtomicReference<Consumer<Throwable>> legacyFailureObserverCapture() {
return legacyFailureObserverCapture == null ? null : legacyFailureObserverCapture.observer;
}

boolean claimLegacyFailureObserverCapture() {
return legacyFailureObserverCapture == null || legacyFailureObserverCapture.claimed.compareAndSet(false, true);
}

RewardOptions withoutLegacyFailureObserverCapture() {
RewardOptions copy = copyForDispatch();
copy.legacyFailureObserverCapture = null;
return copy;
}

void updateLegacyFailureObserverCapture() {
if (legacyFailureObserverCapture != null) legacyFailureObserverCapture.observer.set(asyncFailureObserver);
}

/** True while replaying an entry that must remain in the timed queue on deferral. */
@Getter
@Setter
Expand Down Expand Up @@ -273,6 +316,8 @@ RewardOptions copyForDispatch() {
copy.setAsyncReplayRegistryFingerprints(new HashMap<>(asyncReplayRegistryFingerprints));
copy.setLegacyAsyncReplayCheckpoint(legacyAsyncReplayCheckpoint);
copy.setAsyncReplayCheckpointConsumer(asyncReplayCheckpointConsumer);
copy.setAsyncFailureObserver(asyncFailureObserver);
copy.legacyFailureObserverCapture = legacyFailureObserverCapture;
copy.setAsyncReplayKey(asyncReplayKey);
copy.setAsyncReplayOccurrenceId(asyncReplayOccurrenceId);
copy.setTimedQueueReplay(timedQueueReplay);
Expand All @@ -286,6 +331,7 @@ RewardOptions copyForDispatch() {
*/
RewardOptions copyForNestedDispatch(String replayKey) {
RewardOptions copy = copyForDispatch();
copy.legacyFailureObserverCapture = null;
copy.setCompletedAsyncInjections(0);
copy.setAsyncReplayProgress(new HashMap<>());
// The checkpoint consumer belongs to the queued parent occurrence. Nested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,14 +261,14 @@ public CompletionStage<String> onRewardRequestedAsync(Reward reward, AdvancedCor
// Validate the player before any mixed-section side effect, then preserve
// the established console-before-player command order.
return availability.thenCompose(ignored -> consoleCommands.isEmpty()
&& !Reward.hasReplayCommandSnapshot(placeholders, "console")
&& !Reward.hasReplayCommandSnapshot(placeholders, "console", replayState, replayKey)
? CompletableFuture.completedFuture(null)
: eligibilityGuard == null
: replayState == null && eligibilityGuard == null
? MiscUtils.getInstance().executeConsoleCommandsAsync(user.getPlayerName(), consoleCommands,
placeholders, stagger)
: MiscUtils.getInstance().executeConsoleCommandsAsync(user.getPlayerName(), consoleCommands,
placeholders, stagger, eligibilityGuard))
.thenCompose(ignored -> eligibilityGuard == null
placeholders, stagger, eligibilityGuard, replayState, replayKey))
.thenCompose(ignored -> replayState == null && eligibilityGuard == null
? user.preformCommandAsync(userCommands, placeholders)
: user.preformCommandAsync(userCommands, placeholders, eligibilityGuard, replayState, replayKey))
.thenApply(ignored -> null);
Expand Down
Loading
Loading