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 @@ -627,6 +627,12 @@ public class AdvancedCoreConfigOptions {
* @return the list of worlds where default rewards are disabled
* @param defaultRewardBlackListedWorlds the list of worlds to set
*/
/** Default gamemode allowlist; an explicit reward GameModes list overrides it. */
@Getter
@Setter
@ConfigDataListString(path = "DefaultRewardGameModes")
private ArrayList<String> defaultRewardGameModes = new ArrayList<>();

@Getter
@Setter
@ConfigDataListString(path = "DefaultRewardBlackListedWorlds")
Expand Down Expand Up @@ -679,6 +685,7 @@ public Sound getClickSoundSound() {
*/
public void load(AdvancedCorePlugin plugin) {
if (getYmlConfig() != null) {
defaultRewardGameModes = new ArrayList<>();
new AnnotationHandler().load(getYmlConfig().getData(), this);
debug = DebugLevel.getDebug(debugLevelStr);
storageType = UserStorage.value(userStorageString.toUpperCase());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,7 @@ public void loadAdvancedCoreEvents() {

Bukkit.getPluginManager().registerEvents(FireworkHandler.getInstance(), this);
Bukkit.getPluginManager().registerEvents(new BInventoryListener(this), this);
Bukkit.getPluginManager().registerEvents(new com.bencodez.advancedcore.listeners.RewardEligibilityListener(this), this);
}

private void loadConfig(boolean userStorage) {
Expand Down Expand Up @@ -1442,6 +1443,7 @@ public void onEnable() {
}
getRewardHandler().checkSubRewards();
getRewardHandler().checkDirectlyDefinedRewardFiles();
getRewardHandler().getRewardEligibility().bootstrapOnlinePlayers();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ public void run() {
plugin.getRewardHandler()
.addInjectedRequirements(new RequirementInjectString("VaultGroup", "") {

@Override
public java.util.concurrent.CompletionStage<Boolean> onRequirementRequestAsync(
Reward reward, AdvancedCoreUser user,
org.bukkit.configuration.ConfigurationSection data, RewardOptions options) {
if (!appliesTo(data)) return java.util.concurrent.CompletableFuture.completedFuture(true);
String type = data.getString(getPath(), getDefaultValue());
if (type.isEmpty()) return java.util.concurrent.CompletableFuture.completedFuture(true);
java.util.function.Supplier<java.util.concurrent.CompletionStage<Boolean>> offline =
() -> reward.evaluateAsyncRequirement(() -> {
String group = getPerms().getPrimaryGroup(null, user.getOfflinePlayer());
return group != null && group.equalsIgnoreCase(type);
});
if (options.isGiveOffline()) return offline.get();
return reward.evaluatePlayerRequirement(user, player -> {
if (player != null && user.isOnline()) {
if (!player.isOnline()) return java.util.concurrent.CompletableFuture.completedFuture(false);
String group = getPerms().getPrimaryGroup(player);
return java.util.concurrent.CompletableFuture.completedFuture(
group != null && group.equalsIgnoreCase(type));
}
return offline.get();
});
}

@Override
public boolean onRequirementsRequest(Reward reward, AdvancedCoreUser user,
String type, RewardOptions rewardOptions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;
Expand All @@ -26,6 +27,7 @@

import com.bencodez.advancedcore.AdvancedCorePlugin;
import com.bencodez.advancedcore.api.user.AdvancedCoreUser;
import com.bencodez.advancedcore.api.rewards.RewardEligibility;
import com.bencodez.advancedcore.data.ServerData;
import com.bencodez.simpleapi.messages.MessageAPI;

Expand Down Expand Up @@ -121,12 +123,22 @@ public void giveItem(Player player, ItemStack... item) {
* they never checkpoint a queued delivery as completed.
*/
public CompletionStage<Void> giveItemAsync(Player player, ItemStack... item) {
return giveItemWhenEligibleAsync(player, null, item);
}

/** Replay-aware delivery with an optional final owner-thread eligibility check. */
public CompletionStage<Void> giveItemWhenEligibleAsync(Player player, BooleanSupplier eligibility, ItemStack... item) {
CompletableFuture<Void> completion = new CompletableFuture<>();
scheduleItemDelivery(player, item, completion);
scheduleItemDelivery(player, item, completion, eligibility);
return completion;
}

private void scheduleItemDelivery(Player player, ItemStack[] item, CompletableFuture<Void> completion) {
scheduleItemDelivery(player, item, completion, null);
}

private void scheduleItemDelivery(Player player, ItemStack[] item, CompletableFuture<Void> completion,
BooleanSupplier eligibility) {
if (player == null || item == null || item.length == 0) {
if (completion != null) completion.complete(null);
return;
Expand Down Expand Up @@ -163,7 +175,14 @@ private void scheduleItemDelivery(Player player, ItemStack[] item, CompletableFu
deliveryLock.readLock().lock();
replayDeliveryLocked = true;
}
if (completion != null) validateReplayDeliveryTarget(player, deliveryPlayerId);
if (completion != null) validateReplayDeliveryTarget(player, deliveryPlayerId, eligibility != null);
// Check only before the initial mutation. Retained overflow is already
// committed and must remain deliverable if eligibility later changes.
if (eligibility != null && !eligibility.getAsBoolean()) {
throw AdvancedCoreUser.replayActionNotStarted(
"Player reward context became unavailable before item mutation",
com.bencodez.advancedcore.api.rewards.Reward.playerEligibilityDeferred());
}
if (completion != null) replayOverflowCompletions.put(reservationId, completion);
boolean pendingOverflow = giveItemOwnedPlayer(player, itemsToGive, reservationId);
if (completion != null) {
Expand All @@ -188,7 +207,18 @@ private void scheduleItemDelivery(Player player, ItemStack[] item, CompletableFu
}
};
try {
plugin.getBukkitScheduler().runTask(plugin, delivery, player);
if (completion != null && eligibility != null) {
RewardEligibility.runPlayerTask(plugin, player, delivery, () -> {
if (deliveryClaimed.compareAndSet(false, true)) {
completion.completeExceptionally(RewardEligibility.retiredPlayerActionFailure(plugin));
}
}, failure -> {
if (deliveryClaimed.compareAndSet(false, true)) {
completion.completeExceptionally(AdvancedCoreUser.replayActionNotStarted(
"Scheduler rejected item delivery before dispatch", failure));
}
});
} else plugin.getBukkitScheduler().runTask(plugin, delivery, player);
} catch (Throwable failure) {
deliveryClaimed.set(true);
if (completion != null) completion.completeExceptionally(AdvancedCoreUser.replayActionNotStarted(
Expand All @@ -215,13 +245,14 @@ protected long getItemDeliveryTimeoutMillis() {
* captured UUID and entity identity to remain current; a retry can then safely
* schedule delivery for the live entity.
*/
private void validateReplayDeliveryTarget(Player player, UUID playerId) {
private void validateReplayDeliveryTarget(Player player, UUID playerId, boolean eligibilityDeferral) {
if (!plugin.isEnabled()) {
throw AdvancedCoreUser.replayActionNotStarted("Plugin disabled before item delivery");
}
Player current = Bukkit.getPlayer(playerId);
if (current != player || !current.isOnline()) {
throw AdvancedCoreUser.replayActionNotStarted("Player became unavailable before item delivery");
throw AdvancedCoreUser.replayActionNotStarted("Player became unavailable before item delivery",
eligibilityDeferral ? com.bencodez.advancedcore.api.rewards.Reward.playerEligibilityDeferred() : null);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ public void executeConsoleCommands(String playerName, String command, HashMap<St
*/
public CompletionStage<Void> executeConsoleCommandsAsync(String playerName, String command,
HashMap<String, String> placeholders) {
return executeConsoleCommandsAsync(playerName, command, placeholders,
AdvancedCoreUser.captureCurrentPlayerEligibilityGuard());
}

/** Carries the originating reward's optional player policy into the actual dispatch. */
public CompletionStage<Void> executeConsoleCommandsAsync(String playerName, String command,
HashMap<String, String> placeholders, AdvancedCoreUser.PlayerEligibilityGuard eligibilityGuard) {
try {
ArrayList<String> templates = command == null || command.isEmpty()
? new ArrayList<>() : new ArrayList<>(java.util.List.of(command));
Expand All @@ -289,7 +296,7 @@ public CompletionStage<Void> executeConsoleCommandsAsync(String playerName, Stri
return Reward.replayCommandSequence(plugin, placeholders, "console", templates, commands,
(cmd, ignoredIndex) -> {
plugin.debug("Executing console command: " + cmd);
return runConsoleCommandAsync(stripLeadingSlash(cmd), 0, false, false);
return runConsoleCommandAsync(stripLeadingSlash(cmd), 0, false, false, eligibilityGuard);
});
} catch (Throwable failure) {
return CompletableFuture.failedFuture(failure);
Expand All @@ -301,9 +308,17 @@ public CompletionStage<Void> executeConsoleCommandsAsync(String playerName, Stri
* only after every dispatch has run. This keeps an async reward injector from
* checkpointing a command list while any member is still queued.
*/
@SuppressWarnings("deprecation")
public CompletionStage<Void> executeConsoleCommandsAsync(final String playerName, final ArrayList<String> cmds,
final HashMap<String, String> placeholders, final boolean stagger) {
return executeConsoleCommandsAsync(playerName, cmds, placeholders, stagger,
AdvancedCoreUser.captureCurrentPlayerEligibilityGuard());
}

/** Retains the legacy stagger schedule while checking a captured policy for every command. */
@SuppressWarnings("deprecation")
public CompletionStage<Void> executeConsoleCommandsAsync(final String playerName, final ArrayList<String> cmds,
final HashMap<String, String> placeholders, final boolean stagger,
AdvancedCoreUser.PlayerEligibilityGuard eligibilityGuard) {
try {
placeholders.put("player", playerName);
OfflinePlayer p = Bukkit.getOfflinePlayer(playerName);
Expand All @@ -317,7 +332,7 @@ public CompletionStage<Void> executeConsoleCommandsAsync(final String playerName
(command, index) -> {
plugin.debug("Executing console command: " + command);
return runConsoleCommandAsync(stripLeadingSlash(command), index > 0 ? 1 : 0,
stagger && index > 0, stagger || index == 0);
stagger && index > 0, stagger || index == 0, eligibilityGuard);
});
} catch (Throwable failure) {
return CompletableFuture.failedFuture(failure);
Expand Down Expand Up @@ -535,18 +550,45 @@ private void runConsoleCommand(String command, int delay, boolean hasDelay) {
* after a failed replay has already been retried.
*/
private CompletionStage<Void> runConsoleCommandAsync(String command, int delay, boolean hasDelay,
boolean scheduleAsTask) {
boolean scheduleAsTask, AdvancedCoreUser.PlayerEligibilityGuard eligibilityGuard) {
CompletableFuture<Void> completion = new CompletableFuture<>();
AtomicBoolean claimed = new AtomicBoolean();
Runnable dispatch = () -> {
Runnable finalDispatch = () -> {
if (!claimed.compareAndSet(false, true)) return;
try {
if (eligibilityGuard != null && !plugin.isEnabled()) {
throw new IllegalStateException("Plugin disabled before console command dispatch");
}
Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), command);
completion.complete(null);
} catch (Throwable failure) {
completion.completeExceptionally(failure);
}
};
AtomicBoolean started = new AtomicBoolean();
Runnable dispatch = eligibilityGuard == null ? finalDispatch : () -> {
if (!started.compareAndSet(false, true) || claimed.get()) return;
try {
eligibilityGuard.checkAsync().whenComplete((ignored, failure) -> {
if (failure != null) {
claimed.set(true);
completion.completeExceptionally(failure);
return;
}
try {
// Only the captured eligibility result crosses back to the global owner.
// Folia cannot make this handoff atomic with a player mode transition.
if (!claimed.get()) plugin.getBukkitScheduler().executeOrScheduleSync(plugin, finalDispatch);
} catch (Throwable schedulingFailure) {
claimed.set(true);
completion.completeExceptionally(schedulingFailure);
}
});
} catch (Throwable failure) {
claimed.set(true);
completion.completeExceptionally(failure);
}
};
try {
if (hasDelay && delay > 0) plugin.getBukkitScheduler().runTaskLater(plugin, dispatch, delay);
else if (scheduleAsTask) plugin.getBukkitScheduler().runTask(plugin, dispatch);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,19 @@ public boolean hasServerPermission(UUID playerUUID, String playername, String pe
return false;
}

/** Offline provider phase; callers must capture the world name on the server scheduler. */
public boolean hasOfflineServerPermission(UUID playerUUID, String world, String perm) {
if (playerUUID == null) return false;
if (plugin.getLuckPermsHandle() != null && plugin.getLuckPermsHandle().luckpermsApiLoaded()) {
if (plugin.getLuckPermsHandle().hasOfflinePermission(playerUUID, perm)) return true;
}
if (plugin.getOptions().isUseVaultPermissions() && plugin.getVaultHandler() != null
&& plugin.getVaultHandler().getPerms() != null && plugin.getVaultHandler().getPerms().isEnabled()) {
return plugin.getVaultHandler().getPerms().playerHas(world, Bukkit.getOfflinePlayer(playerUUID), perm);
}
return false;
}

/** @deprecated Use {@link #isValidUserAsync(String, boolean, Consumer, Consumer)} for persisted identity. */
@Deprecated
public boolean isValidUser(String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,30 @@ public boolean hasPermission(UUID uuid, String permission) {
return negate ? !has : has;
}

/**
* Worker-only static-context lookup. Unlike hasPermission, this never discovers
* an online player; a reconnect cannot move native permission reads onto a worker.
*/
public boolean hasOfflinePermission(UUID uuid, String permission) {
if (api == null) return false;
boolean negate = permission.startsWith("!");
if (negate) permission = permission.substring(1);
User user = api.getUserManager().getUser(uuid);
if (user == null) {
try {
user = api.getUserManager().loadUser(uuid).get(10, TimeUnit.SECONDS);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
return false;
} catch (Exception failure) {
return false;
}
}
if (user == null) return false;
boolean allowed = checkUserPermission(user, permission, null);
return negate ? !allowed : allowed;
}

/**
* Async permission check for offline users. Safe to call from the main thread.
*/
Expand Down
Loading
Loading