From 3a85ec26ea7b9654b6368fbba443884c715b5723 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 20 Sep 2026 13:24:52 -0600 Subject: [PATCH 1/3] Remove unused prepared and keyed reward APIs Revert AdvancedCore #328 and #329 now that the VotingPlugin integration will use the live YAML reward behavior. Keep the atomic user transaction and earlier shared reward orchestration unchanged. --- .../api/rewards/PreparedRewardCatalog.java | 308 --------------- .../api/rewards/PreparedRewardDefinition.java | 211 ---------- .../PreparedRewardDefinitionException.java | 14 - .../api/rewards/RewardHandler.java | 57 +-- .../api/rewards/RewardLoader.java | 28 +- .../core/reward/SharedRewardActionClaim.java | 9 - .../core/reward/SharedRewardContext.java | 9 - .../SharedRewardIndeterminateException.java | 10 - .../reward/SharedRewardKeyedDurability.java | 30 -- .../core/reward/SharedRewardOrchestrator.java | 116 +----- .../core/reward/SharedRewardPlatform.java | 24 -- .../rewards/PreparedRewardCatalogTest.java | 321 ---------------- .../rewards/PreparedRewardDefinitionTest.java | 150 -------- .../SharedRewardKeyedRecoveryTest.java | 361 ------------------ 14 files changed, 30 insertions(+), 1618 deletions(-) delete mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardCatalog.java delete mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardDefinition.java delete mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardDefinitionException.java delete mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardActionClaim.java delete mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardIndeterminateException.java delete mode 100644 AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardKeyedDurability.java delete mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/api/rewards/PreparedRewardCatalogTest.java delete mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/api/rewards/PreparedRewardDefinitionTest.java delete mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/SharedRewardKeyedRecoveryTest.java diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardCatalog.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardCatalog.java deleted file mode 100644 index 32431e8cbb..0000000000 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardCatalog.java +++ /dev/null @@ -1,308 +0,0 @@ -package com.bencodez.advancedcore.api.rewards; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.TreeMap; - -/** - * Immutable, persistable lookup data for the named rewards available while a - * caller prepares one root reward plan. - * - *

The catalog preserves {@link RewardRegistry} lookup precedence: directly - * defined entries, then sub-direct entries, then ordinary reward files. It - * freezes only the definitions supplied at capture time; callers must capture - * a new catalog when they intentionally want a later registry state. The - * legacy reward dispatcher does not consume this catalog automatically: a - * caller must resolve nested names through {@link #instantiate(String)} while - * preparing its own execution plan.

- */ -public final class PreparedRewardCatalog { - - private static final String ENCODED_PREFIX = "AdvancedCorePreparedRewardCatalog/"; - private static final int FORMAT_VERSION = 2; - private static final int MAX_DEFINITIONS = 1024; - private static final int MAX_CATALOG_BYTES = 4 * 1024 * 1024; - private static final int MAX_ENCODED_BYTES = 8 * 1024 * 1024; - - private final PreparedRewardDefinition root; - private final Map directDefinitions; - private final Map fileDefinitions; - private final String records; - private final String versionHash; - - private PreparedRewardCatalog(PreparedRewardDefinition root, - Map directDefinitions, - Map fileDefinitions, - String records, String versionHash) { - this.root = root; - this.directDefinitions = Collections.unmodifiableMap(new TreeMap<>(directDefinitions)); - this.fileDefinitions = Collections.unmodifiableMap(new LinkedHashMap<>(fileDefinitions)); - this.records = records; - this.versionHash = versionHash; - } - - /** Captures the root and every currently registered named definition. */ - public static PreparedRewardCatalog capture(Reward root, Collection directlyDefined, - Collection subDirectlyDefined, Collection rewardFiles) { - PreparedRewardDefinition preparedRoot = PreparedRewardDefinition.capture(root); - TreeMap direct = new TreeMap<>(); - LinkedHashMap files = new LinkedHashMap<>(); - int count = 1; - int recordBytes = 0; - - for (DirectlyDefinedReward entry : directlyDefined) { - if (entry == null) continue; - count = checkCount(count + 1); - String key = directKey(entry.getPath()); - if (!direct.containsKey(key)) { - Reward reward = entry.getReward(); - PreparedRewardDefinition definition = reward == null ? null : definitionFor(reward, key); - recordBytes = addRecordBytes(recordBytes, "D", key, definition); - direct.put(key, definition); - } - } - for (SubDirectlyDefinedReward entry : subDirectlyDefined) { - if (entry == null) continue; - count = checkCount(count + 1); - String key = directKey(entry.getFullPath()); - if (!direct.containsKey(key)) { - Reward reward = entry.getReward(); - PreparedRewardDefinition definition = reward == null ? null : definitionFor(reward, key); - recordBytes = addRecordBytes(recordBytes, "D", key, definition); - direct.put(key, definition); - } - } - for (Reward entry : rewardFiles) { - count = checkCount(count + 1); - String key = fileKey(entry == null ? null : entry.getRewardName()); - if (!files.containsKey(key)) { - PreparedRewardDefinition definition = definitionFor(entry, key); - recordBytes = addRecordBytes(recordBytes, "F", key, definition); - files.put(key, definition); - } - } - - String records = encodeRecords(direct, files); - validateRecords(records); - String rootEncoded = preparedRoot.encode(); - return new PreparedRewardCatalog(preparedRoot, direct, files, records, hash(rootEncoded, records)); - } - - /** Restores a catalog written by {@link #encode()}, rejecting changed data. */ - public static PreparedRewardCatalog decode(String encoded) { - if (encoded == null || encoded.length() > MAX_ENCODED_BYTES || !encoded.startsWith(ENCODED_PREFIX)) { - throw new PreparedRewardDefinitionException("Malformed prepared reward catalog"); - } - String[] fields = encoded.split("/", -1); - if (fields.length != 5 || !"AdvancedCorePreparedRewardCatalog".equals(fields[0])) { - throw new PreparedRewardDefinitionException("Malformed prepared reward catalog"); - } - if (!String.valueOf(FORMAT_VERSION).equals(fields[1])) { - throw new PreparedRewardDefinitionException("Unsupported prepared reward catalog version: " + fields[1]); - } - String rootEncoded = decodeField(fields[2], "root"); - String records = decodeField(fields[3], "definitions"); - validateRecords(records); - String actualHash = hash(rootEncoded, records); - if (!MessageDigest.isEqual(fields[4].getBytes(StandardCharsets.US_ASCII), - actualHash.getBytes(StandardCharsets.US_ASCII))) { - throw new PreparedRewardDefinitionException("Prepared reward catalog hash does not match"); - } - - PreparedRewardDefinition root = PreparedRewardDefinition.decode(rootEncoded); - TreeMap direct = new TreeMap<>(); - LinkedHashMap files = new LinkedHashMap<>(); - if (!records.isEmpty()) { - long recordCount = 1L + records.chars().filter(value -> value == '\n').count(); - if (recordCount > MAX_DEFINITIONS - 1L) { - throw new PreparedRewardDefinitionException( - "Prepared reward catalog exceeds " + MAX_DEFINITIONS + " definitions"); - } - for (String record : records.split("\n", -1)) { - String[] parts = record.split("\\|", -1); - if (parts.length != 3) throw new PreparedRewardDefinitionException("Malformed prepared reward catalog record"); - String key = decodeField(parts[1], "lookup key"); - Map target; - PreparedRewardDefinition definition; - if ("D".equals(parts[0])) { - if (!key.equals(directKey(key))) { - throw new PreparedRewardDefinitionException("Malformed direct prepared reward lookup key"); - } - target = direct; - definition = parts[2].isEmpty() ? null - : PreparedRewardDefinition.decode(decodeField(parts[2], "definition")); - } else if ("F".equals(parts[0])) { - if (!key.equals(fileKey(key))) { - throw new PreparedRewardDefinitionException("Malformed file prepared reward lookup key"); - } - target = files; - definition = PreparedRewardDefinition.decode(decodeField(parts[2], "definition")); - } else { - throw new PreparedRewardDefinitionException("Malformed prepared reward catalog record type"); - } - if (target.containsKey(key)) { - throw new PreparedRewardDefinitionException("Duplicate prepared reward catalog lookup key"); - } - target.put(key, definition); - checkCount(1 + direct.size() + files.size()); - } - } - return new PreparedRewardCatalog(root, direct, files, records, actualHash); - } - - /** Encodes this bounded catalog for durable storage. */ - public String encode() { - String rootEncoded = root.encode(); - return ENCODED_PREFIX + FORMAT_VERSION + "/" + encodeField(rootEncoded) + "/" + encodeField(records) + "/" - + versionHash; - } - - /** Creates a fresh detached root reward for plan preparation. */ - public Reward instantiateRoot() { - return root.instantiate(); - } - - /** - * Resolves a named entry using the captured registry precedence and returns - * a fresh detached reward. Unknown names fail closed. - */ - public Reward instantiate(String rewardName) { - String lookupName = RewardRegistry.normalizeLookupName(rewardName); - if (lookupName.isEmpty()) lookupName = "EmptyName"; - String directKey = directKeyForLookup(lookupName); - if (directDefinitions.containsKey(directKey) && directDefinitions.get(directKey) == null) { - throw new PreparedRewardDefinitionException("Prepared reward catalog has no definition for: " + rewardName); - } - PreparedRewardDefinition definition = directDefinitions.get(directKey); - if (definition == null) { - String lookup = fileKeyForLookup(lookupName); - for (Map.Entry entry : fileDefinitions.entrySet()) { - if (entry.getKey().equalsIgnoreCase(lookup)) { - definition = entry.getValue(); - break; - } - } - } - if (definition == null) { - throw new PreparedRewardDefinitionException("Prepared reward catalog has no definition for: " + rewardName); - } - return definition.instantiate(); - } - - public String getVersionHash() { - return versionHash; - } - - public int getDefinitionCount() { - return 1 + (int) directDefinitions.values().stream().filter(java.util.Objects::nonNull).count() - + fileDefinitions.size(); - } - - private static PreparedRewardDefinition definitionFor(Reward reward, String lookupKey) { - if (reward == null) { - throw new PreparedRewardDefinitionException("Could not resolve prepared reward for " + lookupKey); - } - return PreparedRewardDefinition.capture(reward); - } - - private static int checkCount(int count) { - if (count > MAX_DEFINITIONS) { - throw new PreparedRewardDefinitionException("Prepared reward catalog exceeds " + MAX_DEFINITIONS + " definitions"); - } - return count; - } - - private static int addRecordBytes(int used, String type, String key, PreparedRewardDefinition definition) { - // Records are ASCII: Base64 URL fields separated by two pipes and newlines. - String record = type + "|" + encodeField(key) + "|" - + (definition == null ? "" : encodeField(definition.encode())); - long next = (long) used + (used == 0 ? 0 : 1) + record.length(); - if (next > MAX_CATALOG_BYTES) { - throw new PreparedRewardDefinitionException("Prepared reward catalog exceeds " + MAX_CATALOG_BYTES + " bytes"); - } - return (int) next; - } - - private static String encodeRecords(Map direct, - Map files) { - StringBuilder records = new StringBuilder(); - for (Map.Entry entry : direct.entrySet()) { - appendRecord(records, "D|" + encodeField(entry.getKey()) + "|" - + (entry.getValue() == null ? "" : encodeField(entry.getValue().encode()))); - } - for (Map.Entry entry : files.entrySet()) { - appendRecord(records, "F|" + encodeField(entry.getKey()) + "|" + encodeField(entry.getValue().encode())); - } - return records.toString(); - } - - private static void appendRecord(StringBuilder records, String record) { - // All record characters are ASCII (the fields use Base64 URL encoding). - int nextLength = records.length() + (records.length() == 0 ? 0 : 1) + record.length(); - if (nextLength > MAX_CATALOG_BYTES) { - throw new PreparedRewardDefinitionException("Prepared reward catalog exceeds " + MAX_CATALOG_BYTES + " bytes"); - } - if (records.length() != 0) records.append('\n'); - records.append(record); - } - - private static String directKeyForLookup(String name) { - return directKey(RewardRegistry.normalizeLookupName(name)); - } - - private static String fileKeyForLookup(String name) { - return fileKey(RewardRegistry.normalizeLookupName(name)); - } - - private static String directKey(String name) { - validateKey(name); - return RewardRegistry.normalizeDirectPath(name); - } - - private static String fileKey(String name) { - validateKey(name); - return name; - } - - private static void validateKey(String key) { - if (key == null || key.isEmpty() || key.getBytes(StandardCharsets.UTF_8).length > 256) { - throw new PreparedRewardDefinitionException("Invalid prepared reward catalog lookup key"); - } - } - - private static void validateRecords(String records) { - if (records == null || records.getBytes(StandardCharsets.UTF_8).length > MAX_CATALOG_BYTES) { - throw new PreparedRewardDefinitionException("Prepared reward catalog exceeds " + MAX_CATALOG_BYTES + " bytes"); - } - } - - private static String encodeField(String value) { - return Base64.getUrlEncoder().withoutPadding().encodeToString(value.getBytes(StandardCharsets.UTF_8)); - } - - private static String decodeField(String value, String field) { - try { - return new String(Base64.getUrlDecoder().decode(value), StandardCharsets.UTF_8); - } catch (IllegalArgumentException failure) { - throw new PreparedRewardDefinitionException("Malformed prepared reward catalog " + field, failure); - } - } - - private static String hash(String rootEncoded, String records) { - try { - byte[] digest = MessageDigest.getInstance("SHA-256") - .digest((FORMAT_VERSION + "\n" + rootEncoded + "\n" + records).getBytes(StandardCharsets.UTF_8)); - StringBuilder result = new StringBuilder(digest.length * 2); - for (byte value : digest) result.append(String.format("%02x", value)); - return result.toString(); - } catch (NoSuchAlgorithmException failure) { - throw new IllegalStateException("SHA-256 is unavailable", failure); - } - } -} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardDefinition.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardDefinition.java deleted file mode 100644 index 4737e279d8..0000000000 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardDefinition.java +++ /dev/null @@ -1,211 +0,0 @@ -package com.bencodez.advancedcore.api.rewards; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.configuration.serialization.ConfigurationSerializable; - -/** - * An immutable, persistable snapshot of one resolved reward definition. - * - *

It deliberately snapshots one reward at a time. Nested references must be - * resolved explicitly while building a prepared plan. Passing an instantiated - * reward to the legacy dispatcher can still resolve nested names from the live - * registry and does not provide a frozen graph execution contract.

- */ -public final class PreparedRewardDefinition { - - private static final String ENCODED_PREFIX = "AdvancedCorePreparedReward/"; - private static final int FORMAT_VERSION = 2; - private static final int MAX_REWARD_NAME_BYTES = 256; - private static final int MAX_YAML_BYTES = 1024 * 1024; - private static final int MAX_ENCODED_BYTES = 1400000; - - private final String rewardName; - private final String yamlPayload; - private final String versionHash; - - private PreparedRewardDefinition(String rewardName, String yamlPayload, String versionHash) { - this.rewardName = rewardName; - this.yamlPayload = yamlPayload; - this.versionHash = versionHash; - } - - /** Captures the current resolved reward configuration into a detached YAML payload. */ - public static PreparedRewardDefinition capture(Reward reward) { - if (reward == null) throw new IllegalArgumentException("Reward to prepare must not be null"); - return capture(reward.getRewardName(), reward.getConfig().getConfigData()); - } - - /** Captures a named, inline reward definition into a detached YAML payload. */ - public static PreparedRewardDefinition capture(String rewardName, ConfigurationSection section) { - validateRewardName(rewardName); - if (section == null) throw new IllegalArgumentException("Prepared reward configuration must not be null"); - - try { - YamlConfiguration detached = new YamlConfiguration(); - copySection(section, detached); - String payload = detached.saveToString(); - validatePayloadSize(payload); - // Validate now, while capture still has not admitted a definition for execution. - parsePayload(payload); - return new PreparedRewardDefinition(rewardName, payload, hash(rewardName, payload)); - } catch (PreparedRewardDefinitionException failure) { - throw failure; - } catch (RuntimeException failure) { - throw new PreparedRewardDefinitionException("Could not snapshot reward " + rewardName, failure); - } - } - - /** Decodes a value produced by {@link #encode()}, rejecting malformed or changed payloads. */ - public static PreparedRewardDefinition decode(String encoded) { - if (encoded == null || encoded.length() > MAX_ENCODED_BYTES || !encoded.startsWith(ENCODED_PREFIX)) { - throw new PreparedRewardDefinitionException("Malformed prepared reward payload"); - } - String[] fields = encoded.split("/", -1); - if (fields.length != 5 || !"AdvancedCorePreparedReward".equals(fields[0])) { - throw new PreparedRewardDefinitionException("Malformed prepared reward payload"); - } - int version; - try { - version = Integer.parseInt(fields[1]); - } catch (NumberFormatException failure) { - throw new PreparedRewardDefinitionException("Malformed prepared reward version", failure); - } - if (version != FORMAT_VERSION) { - throw new PreparedRewardDefinitionException("Unsupported prepared reward version: " + version); - } - String rewardName = decodeField(fields[2], "name"); - String payload = decodeField(fields[3], "configuration"); - validateRewardName(rewardName); - validatePayloadSize(payload); - String expectedHash = fields[4]; - String actualHash = hash(rewardName, payload); - if (!MessageDigest.isEqual(expectedHash.getBytes(StandardCharsets.US_ASCII), - actualHash.getBytes(StandardCharsets.US_ASCII))) { - throw new PreparedRewardDefinitionException("Prepared reward payload hash does not match"); - } - parsePayload(payload); - return new PreparedRewardDefinition(rewardName, payload, actualHash); - } - - /** Encodes this immutable snapshot for durable storage. */ - public String encode() { - return ENCODED_PREFIX + FORMAT_VERSION + "/" + encodeField(rewardName) + "/" + encodeField(yamlPayload) - + "/" + versionHash; - } - - /** - * Creates a detached reward for plan preparation. It cannot create a legacy - * generated reward file: the caller must persist this definition with its - * own durable plan instead of queueing a name that may change on reload. - */ - public Reward instantiate() { - return new Reward(rewardName, parsePayload(yamlPayload)).needsRewardFile(false); - } - - public String getRewardName() { - return rewardName; - } - - public String getVersionHash() { - return versionHash; - } - - private static void copySection(ConfigurationSection source, ConfigurationSection target) { - for (String key : source.getKeys(false)) { - Object value = source.get(key); - if (value instanceof ConfigurationSection child) { - ConfigurationSection targetChild = target.createSection(key); - copySection(child, targetChild); - } else { - target.set(key, copyValue(value, key)); - } - } - } - - private static Object copyValue(Object value, String path) { - if (value == null || value instanceof String || value instanceof Number || value instanceof Boolean - || value instanceof Character || value instanceof ConfigurationSerializable) { - return value; - } - if (value instanceof List list) { - List copy = new ArrayList<>(list.size()); - for (Object entry : list) copy.add(copyValue(entry, path)); - return copy; - } - if (value instanceof Map map) { - Map copy = new LinkedHashMap<>(); - for (Map.Entry entry : map.entrySet()) { - if (!(entry.getKey() instanceof String key)) { - throw new PreparedRewardDefinitionException( - "Unsupported non-string map key in prepared reward at " + path); - } - copy.put(key, copyValue(entry.getValue(), path + "." + key)); - } - return copy; - } - throw new PreparedRewardDefinitionException( - "Unsupported configuration value in prepared reward at " + path + ": " + value.getClass().getName()); - } - - private static YamlConfiguration parsePayload(String payload) { - try { - validatePayloadSize(payload); - YamlConfiguration parsed = new YamlConfiguration(); - parsed.loadFromString(payload); - return parsed; - } catch (InvalidConfigurationException | RuntimeException failure) { - throw new PreparedRewardDefinitionException("Malformed prepared reward configuration", failure); - } - } - - private static String encodeField(String value) { - return Base64.getUrlEncoder().withoutPadding().encodeToString(value.getBytes(StandardCharsets.UTF_8)); - } - - private static String decodeField(String value, String field) { - try { - return new String(Base64.getUrlDecoder().decode(value), StandardCharsets.UTF_8); - } catch (IllegalArgumentException failure) { - throw new PreparedRewardDefinitionException("Malformed prepared reward " + field, failure); - } - } - - private static String hash(String rewardName, String payload) { - try { - byte[] digest = MessageDigest.getInstance("SHA-256") - .digest((FORMAT_VERSION + "\n" + encodeField(rewardName) + "\n" + encodeField(payload)) - .getBytes(StandardCharsets.US_ASCII)); - StringBuilder result = new StringBuilder(digest.length * 2); - for (byte value : digest) result.append(String.format("%02x", value)); - return result.toString(); - } catch (NoSuchAlgorithmException failure) { - throw new IllegalStateException("SHA-256 is unavailable", failure); - } - } - - private static void validateRewardName(String rewardName) { - if (rewardName == null || rewardName.isEmpty()) { - throw new PreparedRewardDefinitionException("Prepared reward name must not be empty"); - } - if (rewardName.getBytes(StandardCharsets.UTF_8).length > MAX_REWARD_NAME_BYTES) { - throw new PreparedRewardDefinitionException("Prepared reward name exceeds " + MAX_REWARD_NAME_BYTES + " bytes"); - } - } - - private static void validatePayloadSize(String payload) { - if (payload == null || payload.getBytes(StandardCharsets.UTF_8).length > MAX_YAML_BYTES) { - throw new PreparedRewardDefinitionException("Prepared reward YAML exceeds " + MAX_YAML_BYTES + " bytes"); - } - } -} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardDefinitionException.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardDefinitionException.java deleted file mode 100644 index 5ca231dabe..0000000000 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/PreparedRewardDefinitionException.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.bencodez.advancedcore.api.rewards; - -/** Raised when a prepared reward definition cannot be safely captured or restored. */ -public final class PreparedRewardDefinitionException extends IllegalArgumentException { - private static final long serialVersionUID = 1L; - - public PreparedRewardDefinitionException(String message) { - super(message); - } - - public PreparedRewardDefinitionException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java index a6c5f59fc3..4312db52ea 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java @@ -102,15 +102,11 @@ public void checkDirectlyDefined() { } public void checkSubRewards() { - synchronized (rewardRegistry) { - subRewardResolver.checkSubRewards(); - } + subRewardResolver.checkSubRewards(); } public void checkSubRewards(DefinedReward direct) { - synchronized (rewardRegistry) { - subRewardResolver.checkSubRewards(direct); - } + subRewardResolver.checkSubRewards(direct); } public File getDefaultFolder() { @@ -141,50 +137,6 @@ public Reward getReward(ConfigurationSection data, String path, RewardOptions re return rewardExecutor.getReward(data, path, rewardOptions); } - /** - * Resolves and snapshots one named reward. The returned definition freezes - * only this reward; nested definitions are resolved individually by their - * owning injectors. - */ - public PreparedRewardDefinition prepareReward(String reward) { - synchronized (rewardRegistry) { - String lookup = RewardRegistry.normalizeLookupName(reward); - if (lookup.isEmpty()) lookup = "EmptyName"; - if (!rewardRegistry.rewardExist(lookup) && !rewardRegistry.hasDirectRewardHandle(lookup)) { - throw new IllegalArgumentException("Resolved reward does not exist: " + reward); - } - return prepareReward(getReward(lookup)); - } - } - - /** Resolves and snapshots one inline configuration-section reward. */ - public PreparedRewardDefinition prepareReward(ConfigurationSection data, String path, RewardOptions rewardOptions) { - return prepareReward(getReward(data, path, rewardOptions)); - } - - /** Snapshots an already resolved reward definition. */ - public PreparedRewardDefinition prepareReward(Reward reward) { - return PreparedRewardDefinition.capture(reward); - } - - /** - * Captures a bounded catalog for a resolved root and all current named - * registry entries. Dispatch remains unchanged; callers opt in explicitly. - */ - public PreparedRewardCatalog prepareCatalog(Reward root) { - List filesSnapshot; - // loadRewards holds the registry monitor from reset through publication. - // Also hold the synchronized list's monitor while copying its iterator. - synchronized (rewardRegistry) { - List rewardFiles = rewardRegistry.getRewards(); - synchronized (rewardFiles) { - filesSnapshot = new ArrayList<>(rewardFiles); - } - return PreparedRewardCatalog.capture(root, rewardRegistry.getDirectlyDefinedRewards(), - rewardRegistry.getSubDirectlyDefinedRewards(), filesSnapshot); - } - } - public Reward getReward(String reward) { return rewardRegistry.getReward(reward); } @@ -287,10 +239,7 @@ public void loadInjectedRewards() { } public void loadRewards() { - synchronized (rewardRegistry) { - rewardLoader.loadRewards(); - subRewardResolver.checkSubRewards(); - } + rewardLoader.loadRewards(); } public void openSubReward(Player player, String path, RewardEditData reward) { diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java index 709a4bafcc..0912da1b3c 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java @@ -198,22 +198,20 @@ public ArrayList getRewardNames(File file) { } public void loadRewards() { - synchronized (handler.getRewardRegistry()) { - suppressGeneratedDirectlyDefinedFiles(); - handler.getRewardRegistry().resetRewards(); - setupExample(); - handler.addValidPath("DirectlyDefinedReward"); - handler.addValidPath("Delayed"); - handler.addValidPath("Timed"); - handler.addValidPath("DisplayItem"); - handler.addValidPath("ForceOffline"); - for (File file : rewardFolders) { - loadRewards(file); - } - handler.sortInjectedRewards(); - handler.sortInjectedRequirements(); - plugin.debug("Loaded rewards"); + suppressGeneratedDirectlyDefinedFiles(); + handler.getRewardRegistry().resetRewards(); + setupExample(); + handler.addValidPath("DirectlyDefinedReward"); + handler.addValidPath("Delayed"); + handler.addValidPath("Timed"); + handler.addValidPath("DisplayItem"); + handler.addValidPath("ForceOffline"); + for (File file : rewardFolders) { + loadRewards(file); } + handler.sortInjectedRewards(); + handler.sortInjectedRequirements(); + plugin.debug("Loaded rewards"); } private void loadRewards(File file) { diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardActionClaim.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardActionClaim.java deleted file mode 100644 index 988bd3ba78..0000000000 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardActionClaim.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.bencodez.advancedcore.core.reward; - -/** Result of an atomic, durable attempt to claim one native reward action. */ -public enum SharedRewardActionClaim { - /** This caller persisted the pending action and may invoke it once. */ - STARTED, - /** A prior caller may have invoked the action; automatic replay is unsafe. */ - INDETERMINATE -} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardContext.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardContext.java index 12efb01e6f..6f24331aba 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardContext.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardContext.java @@ -10,7 +10,6 @@ public final class SharedRewardContext { private final UUID userId; private final String playerName; private final HashMap placeholders; - private volatile boolean keyedExecution; public SharedRewardContext(UUID userId, String playerName, Map placeholders) { this.userId = Objects.requireNonNull(userId, "userId"); @@ -33,12 +32,4 @@ public String playerName() { public HashMap placeholders() { return placeholders; } - - void markKeyedExecution() { - keyedExecution = true; - } - - boolean isKeyedExecution() { - return keyedExecution; - } } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardIndeterminateException.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardIndeterminateException.java deleted file mode 100644 index 3d848b68ff..0000000000 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardIndeterminateException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.bencodez.advancedcore.core.reward; - -/** A native action may have taken effect but has no durable completion acknowledgement. */ -public final class SharedRewardIndeterminateException extends IllegalStateException { - private static final long serialVersionUID = 1L; - - public SharedRewardIndeterminateException(String executionPath, int stepIndex) { - super("Native reward action requires reconciliation: " + executionPath + " step " + stepIndex); - } -} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardKeyedDurability.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardKeyedDurability.java deleted file mode 100644 index 74fc80fa56..0000000000 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardKeyedDurability.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.bencodez.advancedcore.core.reward; - -import java.util.concurrent.CompletionStage; - -/** - * Durable action admission for one logical reward occurrence. The injected owner - * serializes the occurrence across processes. Its database is authoritative. - * Plans used with this adapter must have flat native steps; nested composite - * execution has no durable parent/child admission contract yet. - */ -public interface SharedRewardKeyedDurability extends SharedRewardDurability { - /** - * Atomically persist a pending action before any native side effect. Return - * STARTED to exactly one caller. If a prior pending action or a stale cursor - * exists, return INDETERMINATE without granting execution. An acknowledgement - * lost after committing STARTED must also return INDETERMINATE on retry. - * Validate the fingerprint and step index against the stored occurrence. - */ - CompletionStage claimAction(String executionPath, String fingerprint, - int stepIndex, SharedRewardContext context); - - /** - * {@inheritDoc} For keyed execution this must atomically advance the cursor - * and clear the matching pending action. A failure after the native effect - * leaves the action pending for explicit reconciliation, never auto-replay. - */ - @Override - CompletionStage checkpoint(String executionPath, String fingerprint, int completedSteps, - SharedRewardContext context); -} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardOrchestrator.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardOrchestrator.java index be3b7320bd..cc3d7670fc 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardOrchestrator.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardOrchestrator.java @@ -27,27 +27,7 @@ public CompletionStage execute(SharedRewardPlan plan, Shared Objects.requireNonNull(plan, "plan"); Objects.requireNonNull(context, "context"); SharedRewardDurability replay = durability == null ? SharedRewardDurability.NONE : durability; - return execute(plan, context, replay, pathSegment(plan.id()), null); - } - - /** - * Execute a prepared plan for one stable logical occurrence. The caller-owned - * durability adapter must atomically claim each native action before it runs; - * an uncertain earlier attempt fails closed for reconciliation. Native steps - * must be flattened; keyed nested composite plans are not supported. - */ - public CompletionStage executeKeyed(SharedRewardPlan plan, SharedRewardContext context, - SharedRewardKeyedDurability durability, String occurrenceKey) { - Objects.requireNonNull(plan, "plan"); - Objects.requireNonNull(context, "context"); - Objects.requireNonNull(durability, "durability"); - Objects.requireNonNull(occurrenceKey, "occurrenceKey"); - if (occurrenceKey.isBlank() || occurrenceKey.length() > 256) { - throw new IllegalArgumentException("Occurrence key must contain 1..256 characters"); - } - if (!durability.durable()) throw new IllegalArgumentException("Keyed rewards require durable action admission"); - context.markKeyedExecution(); - return execute(plan, context, durability, pathSegment(occurrenceKey) + "/" + pathSegment(plan.id()), durability); + return execute(plan, context, replay, pathSegment(plan.id())); } public CompletionStage executeNested(SharedRewardPlan plan, SharedRewardContext context, @@ -57,22 +37,18 @@ public CompletionStage executeNested(SharedRewardPlan plan, Objects.requireNonNull(parentPath, "parentPath"); String segment = pathSegment(plan.id()); String path = parentPath.isBlank() ? segment : parentPath + "/" + segment; - SharedRewardDurability replay = durability == null ? SharedRewardDurability.NONE : durability; - if (context.isKeyedExecution()) { - return failed("Keyed nested plans require an explicit composite-step contract; flatten native steps instead"); - } - return execute(plan, context, replay, path, null); + return execute(plan, context, durability == null ? SharedRewardDurability.NONE : durability, path); } private CompletionStage execute(SharedRewardPlan plan, SharedRewardContext context, - SharedRewardDurability durability, String executionPath, SharedRewardKeyedDurability keyed) { + SharedRewardDurability durability, String executionPath) { try { if (platform.isShuttingDown()) return failed("Reward platform is shutting down"); String fingerprint = durability.durable() ? plan.fingerprint() : "non-durable"; SharedRewardProgress saved = durability.durable() ? durability.loadProgress(executionPath) : null; if (saved != null) { validateProgress(plan, fingerprint, saved, executionPath); - return continueFromProgress(plan, context, durability, executionPath, fingerprint, saved, keyed); + return continueFromProgress(plan, context, durability, executionPath, fingerprint, saved); } int legacyCursor = durability.completedSteps(executionPath); @@ -114,7 +90,7 @@ private CompletionStage execute(SharedRewardPlan plan, Share } return persisted.thenCompose(progress -> { validateProgress(plan, fingerprint, progress, executionPath); - return continueFromProgress(plan, context, durability, executionPath, fingerprint, progress, keyed); + return continueFromProgress(plan, context, durability, executionPath, fingerprint, progress); }); }); } catch (Throwable failure) { @@ -123,12 +99,11 @@ private CompletionStage execute(SharedRewardPlan plan, Share } private CompletionStage continueFromProgress(SharedRewardPlan plan, SharedRewardContext context, - SharedRewardDurability durability, String executionPath, String fingerprint, SharedRewardProgress progress, - SharedRewardKeyedDurability keyed) { + SharedRewardDurability durability, String executionPath, String fingerprint, SharedRewardProgress progress) { context.placeholders().clear(); context.placeholders().putAll(progress.placeholders()); if (!progress.eligible()) return CompletableFuture.completedFuture(SharedRewardResult.NOT_ELIGIBLE); - return executeEligible(plan, context, durability, executionPath, fingerprint, progress, keyed); + return executeEligible(plan, context, durability, executionPath, fingerprint, progress); } private void validateProgress(SharedRewardPlan plan, String fingerprint, SharedRewardProgress progress, @@ -146,11 +121,10 @@ private void validateProgress(SharedRewardPlan plan, String fingerprint, SharedR } private CompletionStage executeEligible(SharedRewardPlan plan, SharedRewardContext context, - SharedRewardDurability durability, String executionPath, String fingerprint, SharedRewardProgress progress, - SharedRewardKeyedDurability keyed) { + SharedRewardDurability durability, String executionPath, String fingerprint, SharedRewardProgress progress) { int resume = progress.completedSteps(); java.util.function.Supplier> work = - () -> executeSteps(plan, context, durability, executionPath, fingerprint, resume, keyed); + () -> executeSteps(plan, context, durability, executionPath, fingerprint, resume); Duration delay = Duration.ZERO; if (resume == 0 && !plan.delay().isZero()) { delay = durability.durable() ? Duration.between(platform.now(), progress.notBefore()) : plan.delay(); @@ -184,14 +158,13 @@ private CompletionStage evaluateRequirements(List executeSteps(SharedRewardPlan plan, SharedRewardContext context, - SharedRewardDurability durability, String executionPath, String fingerprint, int index, - SharedRewardKeyedDurability keyed) { + SharedRewardDurability durability, String executionPath, String fingerprint, int index) { CompletionStage chain = CompletableFuture.completedFuture(SharedRewardResult.COMPLETED); for (int current = index; current < plan.steps().size(); current++) { final int stepIndex = current; chain = chain.thenCompose(previous -> previous == SharedRewardResult.DEFERRED ? CompletableFuture.completedFuture(SharedRewardResult.DEFERRED) - : executeStep(plan.steps().get(stepIndex), context, durability, executionPath, fingerprint, stepIndex, keyed)); + : executeStep(plan.steps().get(stepIndex), context, durability, executionPath, fingerprint, stepIndex)); } return chain.thenCompose(result -> result != SharedRewardResult.DEFERRED && platform.isShuttingDown() ? failed("Reward platform shut down before execution completed") @@ -199,29 +172,9 @@ private CompletionStage executeSteps(SharedRewardPlan plan, } private CompletionStage executeStep(SharedRewardStep step, SharedRewardContext context, - SharedRewardDurability durability, String executionPath, String fingerprint, int index, - SharedRewardKeyedDurability keyed) { - if (keyed != null) { - if (!step.requiresOnlinePlayer()) { - return executeStepOnNative(step, context, durability, executionPath, fingerprint, index, keyed, false); - } - try { - CompletionStage availability = platform.checkActionAvailability(context.userId()); - if (availability == null) return failed("Reward platform returned null availability stage"); - return availability.thenCompose(online -> executeStepOnNative(step, context, durability, - executionPath, fingerprint, index, keyed, Boolean.TRUE.equals(online))); - } catch (Throwable failure) { - return CompletableFuture.failedFuture(failure); - } - } - return executeStepOnNative(step, context, durability, executionPath, fingerprint, index, null, false); - } - - private CompletionStage executeStepOnNative(SharedRewardStep step, SharedRewardContext context, - SharedRewardDurability durability, String executionPath, String fingerprint, int index, - SharedRewardKeyedDurability keyed, boolean onlineChecked) { + SharedRewardDurability durability, String executionPath, String fingerprint, int index) { if (platform.isShuttingDown()) return failed("Reward platform shut down before execution completed"); - if (step.requiresOnlinePlayer() && (keyed != null ? !onlineChecked : !platform.isOnline(context.userId()))) { + if (step.requiresOnlinePlayer() && !platform.isOnline(context.userId())) { if (!durability.durable()) return failed("Player became unavailable during non-durable reward step " + step.id()); CompletionStage deferred; try { @@ -235,42 +188,6 @@ private CompletionStage executeStepOnNative(SharedRewardStep } String stepPath = executionPath + "/" + pathSegment(step.id()) + ":" + index; - if (keyed == null) return executeClaimedStep(step, context, durability, executionPath, fingerprint, index, stepPath, false); - CompletionStage claim; - try { - claim = keyed.claimAction(executionPath, fingerprint, index, context); - if (claim == null) return failed("Keyed reward adapter returned null action claim stage"); - } catch (Throwable failure) { - return CompletableFuture.failedFuture(failure); - } - return claim.thenCompose(result -> { - if (result == SharedRewardActionClaim.INDETERMINATE) { - return CompletableFuture.failedFuture(new SharedRewardIndeterminateException(executionPath, index)); - } - if (result != SharedRewardActionClaim.STARTED) return failed("Invalid keyed reward action claim"); - try { - CompletionStage dispatched = platform.runClaimedAction(context.userId(), - step.requiresOnlinePlayer(), () -> { - // Admission may have awaited storage while the server disabled - // or the player disconnected. Leave the claim for reconciliation. - if (platform.isShuttingDown() - || (step.requiresOnlinePlayer() && !platform.isOnline(context.userId()))) { - return CompletableFuture.failedFuture( - new SharedRewardIndeterminateException(executionPath, index)); - } - return executeClaimedStep(step, context, durability, executionPath, fingerprint, index, - stepPath, true); - }); - return dispatched == null ? failed("Reward platform returned null claimed action stage") : dispatched; - } catch (Throwable failure) { - return CompletableFuture.failedFuture(failure); - } - }); - } - - private CompletionStage executeClaimedStep(SharedRewardStep step, SharedRewardContext context, - SharedRewardDurability durability, String executionPath, String fingerprint, int index, String stepPath, - boolean claimedStrict) { CompletionStage action; try { action = step.action().execute(context, stepPath); @@ -283,12 +200,7 @@ private CompletionStage executeClaimedStep(SharedRewardStep return action.thenCompose(result -> { if (result == null) return CompletableFuture.failedFuture( new IllegalStateException("Reward step returned null result: " + step.id())); - if (result == SharedRewardResult.DEFERRED) { - if (claimedStrict) { - return failed("Keyed reward action deferred after it was claimed: " + step.id()); - } - return CompletableFuture.completedFuture(SharedRewardResult.DEFERRED); - } + if (result == SharedRewardResult.DEFERRED) return CompletableFuture.completedFuture(SharedRewardResult.DEFERRED); CompletionStage checkpoint; try { checkpoint = durability.checkpoint(executionPath, fingerprint, index + 1, context); diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardPlatform.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardPlatform.java index cf63c383d6..f8bea6a17f 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardPlatform.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/reward/SharedRewardPlatform.java @@ -3,7 +3,6 @@ import java.time.Duration; import java.time.Instant; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Supplier; @@ -26,27 +25,4 @@ CompletionStage delay(Duration delay, Supplier> operation); boolean isShuttingDown(); - - /** - * Check online availability on the player's owner thread before claiming a - * durable action. The returned stage must finish without waiting for the - * action or its claim. A false result permits durable offline deferral. - */ - default CompletionStage checkActionAvailability(UUID userId) { - return CompletableFuture.failedFuture(new UnsupportedOperationException( - "Keyed online rewards require an owner-thread availability check")); - } - - /** - * Run the post-claim state check and native action on the platform's owner - * thread or scheduler. When {@code requiresOnlinePlayer} is true, route to - * that player's entity/region owner. Keyed execution fails closed until an - * adapter provides this hook; legacy execution does not use it. The returned - * stage must cover the whole supplied operation, not just its scheduling. - */ - default CompletionStage runClaimedAction( - UUID userId, boolean requiresOnlinePlayer, Supplier> operation) { - return CompletableFuture.failedFuture(new UnsupportedOperationException( - "Keyed reward execution requires a native action scheduler")); - } } diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/api/rewards/PreparedRewardCatalogTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/api/rewards/PreparedRewardCatalogTest.java deleted file mode 100644 index 54474066f2..0000000000 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/api/rewards/PreparedRewardCatalogTest.java +++ /dev/null @@ -1,321 +0,0 @@ -package com.bencodez.advancedcore.tests.api.rewards; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.Base64; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.logging.Logger; - -import org.bukkit.configuration.file.YamlConfiguration; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import com.bencodez.advancedcore.AdvancedCorePlugin; -import com.bencodez.advancedcore.api.rewards.DirectlyDefinedReward; -import com.bencodez.advancedcore.api.rewards.PreparedRewardCatalog; -import com.bencodez.advancedcore.api.rewards.PreparedRewardDefinition; -import com.bencodez.advancedcore.api.rewards.PreparedRewardDefinitionException; -import com.bencodez.advancedcore.api.rewards.Reward; -import com.bencodez.advancedcore.api.rewards.RewardHandler; -import com.bencodez.advancedcore.api.rewards.RewardRegistry; -import com.bencodez.advancedcore.api.rewards.SubDirectlyDefinedReward; - -class PreparedRewardCatalogTest { - - private AdvancedCorePlugin plugin; - private RewardHandler handler; - - @BeforeEach - void setUp() { - plugin = mock(AdvancedCorePlugin.class); - Logger logger = mock(Logger.class); - when(plugin.getLogger()).thenReturn(logger); - AdvancedCorePlugin.setInstance(plugin); - handler = new RewardHandler(plugin); - when(plugin.getRewardHandler()).thenReturn(handler); - } - - @AfterEach - void tearDown() { - AdvancedCorePlugin.setInstance(null); - } - - @Test - void catalogCapturesNamedDirectSubAndFileDefinitionsBeforeTheLiveRegistryChanges() { - YamlConfiguration rootData = rewardData("root before"); - YamlConfiguration directData = rewardData("direct before"); - YamlConfiguration subData = rewardData("sub before"); - YamlConfiguration fileData = rewardData("file before"); - YamlConfiguration collisionFileData = rewardData("file collision"); - - DirectlyDefinedReward direct = mock(DirectlyDefinedReward.class); - when(direct.getPath()).thenReturn("Direct.Child"); - Reward directReward = new Reward("Direct_Child", directData); - when(direct.getReward()).thenReturn(directReward); - handler.addDirectlyDefined(direct); - - SubDirectlyDefinedReward sub = mock(SubDirectlyDefinedReward.class); - when(sub.getFullPath()).thenReturn("Sub.Child"); - Reward subReward = new Reward("Sub_Child", subData); - when(sub.getReward()).thenReturn(subReward); - handler.addSubDirectlyDefined(sub); - - handler.getRewards().add(new Reward("File_Child", fileData)); - handler.getRewards().add(new Reward("Direct_Child", collisionFileData)); - - PreparedRewardCatalog catalog = handler.prepareCatalog(new Reward("Root", rootData)); - rootData.set("Messages", List.of("root after")); - directData.set("Messages", List.of("direct after")); - subData.set("Messages", List.of("sub after")); - fileData.set("Messages", List.of("file after")); - - assertEquals(5, catalog.getDefinitionCount()); - assertMessage("root before", catalog.instantiateRoot()); - assertMessage("direct before", catalog.instantiate("Direct Child")); - assertMessage("sub before", catalog.instantiate("Sub Child")); - assertMessage("file before", catalog.instantiate("File Child")); - assertMessage("direct before", catalog.instantiate("Direct_Child")); - Reward firstFileLookup = catalog.instantiate("File Child"); - firstFileLookup.getConfig().getConfigData().set("Messages", List.of("execution mutation")); - assertMessage("file before", catalog.instantiate("File Child")); - } - - @Test - void catalogRoundTripRetainsItsHashAndRejectsUnknownChangedAndOversizeInputs() { - YamlConfiguration child = rewardData("before"); - handler.getRewards().add(new Reward("Child", child)); - PreparedRewardCatalog captured = handler.prepareCatalog(new Reward("Root", rewardData("root"))); - PreparedRewardCatalog restored = PreparedRewardCatalog.decode(captured.encode()); - - assertEquals(captured.encode(), restored.encode()); - assertEquals(captured.getVersionHash(), restored.getVersionHash()); - assertMessage("before", restored.instantiate("Child")); - assertThrows(PreparedRewardDefinitionException.class, () -> restored.instantiate("missing")); - - String encoded = captured.encode(); - String lastCharacter = encoded.substring(encoded.length() - 1); - String changed = encoded.substring(0, encoded.length() - 1) + ("0".equals(lastCharacter) ? "1" : "0"); - assertThrows(PreparedRewardDefinitionException.class, () -> PreparedRewardCatalog.decode(changed)); - assertThrows(PreparedRewardDefinitionException.class, - () -> PreparedRewardCatalog.decode("AdvancedCorePreparedRewardCatalog/" + "x".repeat(8 * 1024 * 1024))); - } - - @Test - void nestedNameCanBeResolvedFromFrozenCatalogDuringPlanPreparation() { - YamlConfiguration root = rewardData("root"); - root.set("Rewards", List.of("Child")); - YamlConfiguration oldChild = rewardData("before reload"); - handler.getRewards().add(new Reward("Child", oldChild)); - PreparedRewardCatalog prepared = PreparedRewardCatalog.decode( - handler.prepareCatalog(new Reward("Root", root)).encode()); - - oldChild.set("Messages", List.of("after reload")); - List nestedNames = prepared.instantiateRoot().getConfig().getConfigData().getStringList("Rewards"); - assertEquals(List.of("Child"), nestedNames); - assertMessage("before reload", prepared.instantiate(nestedNames.get(0))); - } - - @Test - void optionalMissingDirectHandleDoesNotBlockValidRewards() { - DirectlyDefinedReward optional = mock(DirectlyDefinedReward.class); - when(optional.getPath()).thenReturn("Optional.Absent"); - handler.addDirectlyDefined(optional); - handler.getRewards().add(new Reward("Available", rewardData("available"))); - - PreparedRewardCatalog catalog = handler.prepareCatalog(new Reward("Root", rewardData("root"))); - - assertEquals(2, catalog.getDefinitionCount()); - assertMessage("available", catalog.instantiate("Available")); - assertThrows(PreparedRewardDefinitionException.class, () -> catalog.instantiate("Optional.Absent")); - } - - @Test - void missingDirectHandleShadowsSameNamedFileAfterRoundTrip() { - DirectlyDefinedReward optional = mock(DirectlyDefinedReward.class); - when(optional.getPath()).thenReturn("Optional.Absent"); - handler.addDirectlyDefined(optional); - handler.getRewards().add(new Reward("Optional_Absent", rewardData("file must stay shadowed"))); - handler.getRewards().add(new Reward("Available", rewardData("available"))); - - PreparedRewardCatalog catalog = PreparedRewardCatalog.decode( - handler.prepareCatalog(new Reward("Root", rewardData("root"))).encode()); - - assertThrows(PreparedRewardDefinitionException.class, () -> catalog.instantiate("Optional.Absent")); - assertMessage("available", catalog.instantiate("Available")); - } - - @Test - void fileLookupMatchesRegistryUnicodeCaseSemanticsAndFirstEntryWins() { - handler.getRewards().add(new Reward("İ", rewardData("first"))); - handler.getRewards().add(new Reward("i", rewardData("second"))); - PreparedRewardCatalog catalog = PreparedRewardCatalog.decode( - handler.prepareCatalog(new Reward("Root", rewardData("root"))).encode()); - - assertMessage("first", catalog.instantiate("i")); - assertMessage("first", catalog.instantiate("İ")); - } - - @Test - void fileLookupKeepsRawRegisteredNamesDistinctFromNormalizedRequests() { - handler.getRewards().add(new Reward("Foo Bar", rewardData("spaced file"))); - handler.getRewards().add(new Reward("Foo_Bar", rewardData("underscored file"))); - PreparedRewardCatalog catalog = PreparedRewardCatalog.decode( - handler.prepareCatalog(new Reward("Root", rewardData("root"))).encode()); - - assertMessage("underscored file", handler.getReward("Foo Bar")); - assertMessage("underscored file", catalog.instantiate("Foo Bar")); - } - - @Test - void emptyNameUsesTheRegistryFallbackWhenPrepared() { - handler.getRewards().add(new Reward("EmptyName", rewardData("fallback"))); - PreparedRewardCatalog catalog = handler.prepareCatalog(new Reward("Root", rewardData("root"))); - - assertMessage("fallback", handler.getReward("")); - assertMessage("fallback", handler.prepareReward("").instantiate()); - assertMessage("fallback", handler.prepareReward((String) null).instantiate()); - assertMessage("fallback", catalog.instantiate("")); - assertMessage("fallback", catalog.instantiate(null)); - } - - @Test - void combinedDefinitionBytesAreBoundedBeforeJoiningRecords() { - String largeValue = "x".repeat(800_000); - for (int index = 0; index < 5; index++) { - YamlConfiguration data = new YamlConfiguration(); - data.set("Payload", largeValue); - handler.getRewards().add(new Reward("Large" + index, data)); - } - Reward late = spy(new Reward("Late", rewardData("must not capture"))); - handler.getRewards().add(late); - - assertThrows(PreparedRewardDefinitionException.class, - () -> handler.prepareCatalog(new Reward("Root", rewardData("root")))); - verify(late, never()).getConfig(); - } - - @Test - void decodeRejectsExcessRecordsBeforeSplittingThem() throws Exception { - String[] parts = handler.prepareCatalog(new Reward("Root", rewardData("root"))).encode().split("/", -1); - String rootEncoded = new String(Base64.getUrlDecoder().decode(parts[2]), StandardCharsets.UTF_8); - String records = "\n".repeat(1024); - String hashInput = "2\n" + rootEncoded + "\n" + records; - byte[] digest = MessageDigest.getInstance("SHA-256").digest(hashInput.getBytes(StandardCharsets.UTF_8)); - StringBuilder hash = new StringBuilder(); - for (byte value : digest) hash.append(String.format("%02x", value)); - String encoded = "AdvancedCorePreparedRewardCatalog/2/" + parts[2] + "/" - + Base64.getUrlEncoder().withoutPadding().encodeToString(records.getBytes(StandardCharsets.UTF_8)) - + "/" + hash; - - PreparedRewardDefinitionException failure = assertThrows(PreparedRewardDefinitionException.class, - () -> PreparedRewardCatalog.decode(encoded)); - assertTrue(failure.getMessage().contains("definitions")); - } - - @Test - void oldCatalogFormatIsRejectedBeforeRestoringItsDefinitions() { - assertThrows(PreparedRewardDefinitionException.class, - () -> PreparedRewardCatalog.decode("AdvancedCorePreparedRewardCatalog/1/eA/eQ/oldhash")); - } - - @Test - void rewardFileCaptureWaitsForConcurrentRegistryReload() throws Exception { - List files = handler.getRewards(); - files.add(new Reward("Before", rewardData("before"))); - Reward root = new Reward("Root", rewardData("root")); - RewardRegistry registry = handler.getRewardRegistry(); - CountDownLatch lockHeld = new CountDownLatch(1); - CountDownLatch release = new CountDownLatch(1); - Thread reload = new Thread(() -> { - synchronized (registry) { - registry.resetRewards(); - lockHeld.countDown(); - try { - release.await(); - } catch (InterruptedException failure) { - Thread.currentThread().interrupt(); - } - registry.getRewards().add(new Reward("After", rewardData("after"))); - SubDirectlyDefinedReward sub = mock(SubDirectlyDefinedReward.class); - when(sub.getFullPath()).thenReturn("After.Sub"); - when(sub.getReward()).thenReturn(new Reward("After_Sub", rewardData("sub after"))); - registry.addSubDirectlyDefined(sub); - } - }); - reload.start(); - try { - org.junit.jupiter.api.Assertions.assertTrue(lockHeld.await(5, TimeUnit.SECONDS)); - CountDownLatch captureStarted = new CountDownLatch(1); - CompletableFuture capture = CompletableFuture.supplyAsync(() -> { - captureStarted.countDown(); - return handler.prepareCatalog(root); - }); - org.junit.jupiter.api.Assertions.assertTrue(captureStarted.await(5, TimeUnit.SECONDS)); - assertThrows(TimeoutException.class, () -> capture.get(100, TimeUnit.MILLISECONDS)); - release.countDown(); - PreparedRewardCatalog catalog = capture.get(5, TimeUnit.SECONDS); - assertMessage("after", catalog.instantiate("After")); - assertMessage("sub after", catalog.instantiate("After.Sub")); - assertThrows(PreparedRewardDefinitionException.class, () -> catalog.instantiate("Before")); - } finally { - release.countDown(); - reload.join(5000); - } - } - - @Test - void namedPreparationWaitsForRegistryReloadBeforeResolving() throws Exception { - RewardRegistry registry = handler.getRewardRegistry(); - CountDownLatch lockHeld = new CountDownLatch(1); - CountDownLatch release = new CountDownLatch(1); - Thread reload = new Thread(() -> { - synchronized (registry) { - registry.resetRewards(); - lockHeld.countDown(); - try { - release.await(); - } catch (InterruptedException failure) { - Thread.currentThread().interrupt(); - } - registry.getRewards().add(new Reward("After", rewardData("after"))); - } - }); - reload.start(); - try { - assertTrue(lockHeld.await(5, TimeUnit.SECONDS)); - CompletableFuture capture = CompletableFuture.supplyAsync( - () -> handler.prepareReward("After")); - assertThrows(TimeoutException.class, () -> capture.get(100, TimeUnit.MILLISECONDS)); - release.countDown(); - assertMessage("after", capture.get(5, TimeUnit.SECONDS).instantiate()); - } finally { - release.countDown(); - reload.join(5000); - } - } - - private static YamlConfiguration rewardData(String message) { - YamlConfiguration data = new YamlConfiguration(); - data.set("Messages", List.of(message)); - return data; - } - - private static void assertMessage(String expected, Reward reward) { - assertEquals(List.of(expected), reward.getConfig().getConfigData().getStringList("Messages")); - } -} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/api/rewards/PreparedRewardDefinitionTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/api/rewards/PreparedRewardDefinitionTest.java deleted file mode 100644 index 4bd69a005d..0000000000 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/api/rewards/PreparedRewardDefinitionTest.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.bencodez.advancedcore.tests.api.rewards; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.List; -import java.util.logging.Logger; - -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.YamlConfiguration; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import com.bencodez.advancedcore.AdvancedCorePlugin; -import com.bencodez.advancedcore.api.rewards.PreparedRewardDefinition; -import com.bencodez.advancedcore.api.rewards.PreparedRewardDefinitionException; -import com.bencodez.advancedcore.api.rewards.Reward; -import com.bencodez.advancedcore.api.rewards.RewardHandler; -import com.bencodez.advancedcore.api.rewards.RewardOptions; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class PreparedRewardDefinitionTest { - - private AdvancedCorePlugin plugin; - private RewardHandler handler; - - @BeforeEach - void setUp() { - plugin = mock(AdvancedCorePlugin.class); - Logger logger = mock(Logger.class); - when(plugin.getLogger()).thenReturn(logger); - AdvancedCorePlugin.setInstance(plugin); - handler = new RewardHandler(plugin); - when(plugin.getRewardHandler()).thenReturn(handler); - } - - @AfterEach - void tearDown() { - AdvancedCorePlugin.setInstance(null); - } - - @Test - void preparedInlineRewardSurvivesSourceMutationAndCreatesFreshConfigurations() { - YamlConfiguration source = new YamlConfiguration(); - ConfigurationSection inline = source.createSection("Inline"); - inline.set("Commands", List.of("say before")); - inline.createSection("Nested").set("Message", "before"); - - PreparedRewardDefinition prepared = handler.prepareReward(source, "Inline", - new RewardOptions().setPrefix("Parent")); - source.set("Inline.Commands", List.of("say after")); - source.set("Inline.Nested.Message", "after"); - - Reward first = prepared.instantiate(); - first.getConfig().getConfigData().set("Commands", List.of("execution mutation")); - Reward second = prepared.instantiate(); - - assertEquals("Parent_Inline", prepared.getRewardName()); - assertEquals(List.of("say before"), second.getConfig().getConfigData().getStringList("Commands")); - assertEquals("before", second.getConfig().getConfigData().getString("Nested.Message")); - } - - @Test - void encodedDefinitionRoundTripsWithAStableHash() { - YamlConfiguration source = new YamlConfiguration(); - source.set("Commands.Console", List.of("say one", "say two")); - source.set("Chance", 12.5D); - - PreparedRewardDefinition captured = PreparedRewardDefinition.capture("Named", source); - PreparedRewardDefinition restored = PreparedRewardDefinition.decode(captured.encode()); - - assertEquals(captured.encode(), restored.encode()); - assertEquals(captured.getVersionHash(), restored.getVersionHash()); - assertEquals(List.of("say one", "say two"), - restored.instantiate().getConfig().getConfigData().getStringList("Commands.Console")); - } - - @Test - void changedOrUnsupportedDefinitionsFailClosed() { - YamlConfiguration source = new YamlConfiguration(); - source.set("Commands", List.of("say one")); - String encoded = PreparedRewardDefinition.capture("Named", source).encode(); - String lastCharacter = encoded.substring(encoded.length() - 1); - String changed = encoded.substring(0, encoded.length() - 1) + ("0".equals(lastCharacter) ? "1" : "0"); - - assertThrows(PreparedRewardDefinitionException.class, () -> PreparedRewardDefinition.decode(changed)); - - source.set("Unsupported", new Object()); - assertThrows(PreparedRewardDefinitionException.class, - () -> PreparedRewardDefinition.capture("Unsupported", source)); - } - - @Test - void namedPreparationCapturesTheResolvedRewardRatherThanTheLiveRegistryEntry() { - YamlConfiguration source = new YamlConfiguration(); - source.set("Messages", List.of("before reload")); - Reward registered = new Reward("Named_Reward", source); - handler.getRewards().add(registered); - - PreparedRewardDefinition prepared = handler.prepareReward("Named Reward"); - source.set("Messages", List.of("after reload")); - - assertEquals("Named_Reward", prepared.getRewardName()); - assertEquals(List.of("before reload"), - prepared.instantiate().getConfig().getConfigData().getStringList("Messages")); - assertNotEquals(PreparedRewardDefinition.capture("Named_Reward", source).getVersionHash(), - prepared.getVersionHash()); - } - - @Test - void namedPreparationRejectsARewardThatWasNotResolvedByTheRegistry() { - assertThrows(IllegalArgumentException.class, () -> handler.prepareReward("missing")); - } - - @Test - void detachedDirectDefinitionDoesNotCreateALegacyGeneratedRewardFile() { - YamlConfiguration source = new YamlConfiguration(); - source.set("Delay.Seconds", 5); - Reward direct = new Reward("Direct", source).needsRewardFile(false); - Reward prepared = PreparedRewardDefinition.decode(PreparedRewardDefinition.capture(direct).encode()) - .instantiate(); - - assertDoesNotThrow(prepared::checkRewardFile); - } - - @Test - void hashFramesNameAndYamlWithoutDelimiterAmbiguity() { - YamlConfiguration shortPayload = new YamlConfiguration(); - shortPayload.set("other", "thing"); - YamlConfiguration longPayload = new YamlConfiguration(); - longPayload.set("key", "value"); - longPayload.set("other", "thing"); - - PreparedRewardDefinition first = PreparedRewardDefinition.capture("foo\nkey: value", shortPayload); - PreparedRewardDefinition second = PreparedRewardDefinition.capture("foo", longPayload); - - assertNotEquals(first.getVersionHash(), second.getVersionHash()); - } - - @Test - void oldHashFramingIsRejectedByItsVersionBeforeRestore() { - assertThrows(PreparedRewardDefinitionException.class, - () -> PreparedRewardDefinition.decode("AdvancedCorePreparedReward/1/Zm9v/eDogeQ/oldhash")); - } -} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/SharedRewardKeyedRecoveryTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/SharedRewardKeyedRecoveryTest.java deleted file mode 100644 index 89ae384b0e..0000000000 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/SharedRewardKeyedRecoveryTest.java +++ /dev/null @@ -1,361 +0,0 @@ -package com.bencodez.advancedcore.tests.rewards; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.time.Duration; -import java.time.Instant; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; - -import com.bencodez.advancedcore.core.reward.SharedRewardActionClaim; -import com.bencodez.advancedcore.core.reward.SharedRewardContext; -import com.bencodez.advancedcore.core.reward.SharedRewardIndeterminateException; -import com.bencodez.advancedcore.core.reward.SharedRewardKeyedDurability; -import com.bencodez.advancedcore.core.reward.SharedRewardOrchestrator; -import com.bencodez.advancedcore.core.reward.SharedRewardPlan; -import com.bencodez.advancedcore.core.reward.SharedRewardPlatform; -import com.bencodez.advancedcore.core.reward.SharedRewardProgress; -import com.bencodez.advancedcore.core.reward.SharedRewardResult; -import com.bencodez.advancedcore.core.reward.SharedRewardStep; - -class SharedRewardKeyedRecoveryTest { - private static final UUID USER = UUID.fromString("fef273b7-aa45-42f9-ac14-cb047533afde"); - - @Test - void pendingClaimPrecedesNativeActionAndCheckpointClearsIt() { - Store store = new Store(); - AtomicInteger actions = new AtomicInteger(); - SharedRewardPlan plan = plan(new SharedRewardStep("command", false, (context, path) -> { - assertEquals(0, store.cursor("vote-a/vote")); - assertEquals(0, store.pending.get("vote-a/vote")); - actions.incrementAndGet(); - return done(); - })); - - assertEquals(SharedRewardResult.COMPLETED, execute(store, plan, "vote-a").join()); - assertEquals(1, actions.get()); - assertEquals(1, store.cursor("vote-a/vote")); - assertNull(store.pending.get("vote-a/vote")); - assertEquals(SharedRewardResult.COMPLETED, execute(store, plan, "vote-a").join()); - assertEquals(1, actions.get()); - } - - @Test - void concurrentSubmissionCannotRunAnAlreadyClaimedAction() { - Store store = new Store(); - AtomicInteger actions = new AtomicInteger(); - CompletableFuture effect = new CompletableFuture<>(); - SharedRewardPlan plan = plan(new SharedRewardStep("command", false, (context, path) -> { - actions.incrementAndGet(); - return effect; - })); - - CompletableFuture first = execute(store, plan, "vote-a"); - assertEquals(1, actions.get()); - CompletionException failure = assertThrows(CompletionException.class, - () -> execute(store, plan, "vote-a").join()); - assertInstanceOf(SharedRewardIndeterminateException.class, failure.getCause()); - assertEquals(1, actions.get()); - - effect.complete(SharedRewardResult.COMPLETED); - assertEquals(SharedRewardResult.COMPLETED, first.join()); - assertEquals(SharedRewardResult.COMPLETED, execute(store, plan, "vote-a").join()); - assertEquals(1, actions.get()); - } - - @Test - void keyedNestedPlanIsRejectedBeforeAnyChildSideEffect() { - Store store = new Store(); - Platform platform = new Platform(); - SharedRewardOrchestrator orchestrator = new SharedRewardOrchestrator(platform); - AtomicInteger nestedActions = new AtomicInteger(); - SharedRewardPlan nested = new SharedRewardPlan("child", 1, Duration.ZERO, List.of(), - List.of(step("item", nestedActions)), "child-config-v1"); - SharedRewardPlan parent = plan(new SharedRewardStep("nested", false, - (context, path) -> orchestrator.executeNested(nested, context, store, path))); - - assertThrows(CompletionException.class, () -> execute(platform, store, parent, "vote-a").join()); - assertEquals(0, nestedActions.get()); - assertEquals(0, store.completedSteps("vote-a/vote/nested:0/child")); - assertNull(store.pending.get("vote-a/vote/nested:0/child")); - } - - @Test - void legacyNestedCallAcceptsAKeyedCapableAdapter() { - Store store = new Store(); - SharedRewardPlan empty = new SharedRewardPlan("child", 1, Duration.ZERO, List.of(), List.of(), "empty"); - - assertEquals(SharedRewardResult.COMPLETED, new SharedRewardOrchestrator(new Platform()) - .executeNested(empty, new SharedRewardContext(USER, "Ben", Map.of()), store, "legacy-parent") - .toCompletableFuture().join()); - } - - @Test - void disconnectWhileClaimIsPendingCannotStartOnlineAction() { - Store store = new Store(); - Platform platform = new Platform(); - AtomicInteger actions = new AtomicInteger(); - CompletableFuture claim = new CompletableFuture<>(); - store.delayedClaim = claim; - SharedRewardPlan plan = plan(new SharedRewardStep("item", true, (context, path) -> { - assertTrue(platform.nativeDispatch); - actions.incrementAndGet(); - return done(); - })); - - CompletableFuture execution = execute(platform, store, plan, "vote-a"); - platform.online = false; - claim.complete(SharedRewardActionClaim.STARTED); - - CompletionException failure = assertThrows(CompletionException.class, execution::join); - assertInstanceOf(SharedRewardIndeterminateException.class, failure.getCause()); - assertEquals(0, actions.get()); - assertEquals(0, store.pending.get("vote-a/vote")); - } - - @Test - void shutdownWhileClaimIsPendingCannotStartNativeAction() { - Store store = new Store(); - Platform platform = new Platform(); - AtomicInteger actions = new AtomicInteger(); - CompletableFuture claim = new CompletableFuture<>(); - store.delayedClaim = claim; - SharedRewardPlan plan = plan(step("command", actions)); - - CompletableFuture execution = execute(platform, store, plan, "vote-a"); - platform.shuttingDown = true; - claim.complete(SharedRewardActionClaim.STARTED); - - CompletionException failure = assertThrows(CompletionException.class, execution::join); - assertInstanceOf(SharedRewardIndeterminateException.class, failure.getCause()); - assertEquals(0, actions.get()); - assertEquals(0, store.pending.get("vote-a/vote")); - assertEquals(USER, platform.lastDispatchUser); - assertEquals(false, platform.lastDispatchRequiresOnline); - } - - @Test - void completedNativeEffectWithFailedCheckpointIsIndeterminateOnRetry() { - Store store = new Store(); - store.failCheckpoint = true; - AtomicInteger actions = new AtomicInteger(); - SharedRewardPlan plan = plan(step("money", actions)); - - assertThrows(CompletionException.class, () -> execute(store, plan, "vote-a").join()); - assertEquals(1, actions.get()); - assertEquals(0, store.cursor("vote-a/vote")); - assertEquals(0, store.pending.get("vote-a/vote")); - - Store restarted = store.reopen(); - CompletionException failure = assertThrows(CompletionException.class, - () -> execute(restarted, plan, "vote-a").join()); - assertInstanceOf(SharedRewardIndeterminateException.class, failure.getCause()); - assertEquals(1, actions.get()); - } - - @Test - void lostCheckpointAcknowledgementRecoversCommittedPrefixWithoutRepeatingAction() { - Store store = new Store(); - store.loseCheckpointAck = true; - AtomicInteger actions = new AtomicInteger(); - SharedRewardPlan plan = plan(step("command", actions)); - - assertThrows(CompletionException.class, () -> execute(store, plan, "vote-a").join()); - assertEquals(1, actions.get()); - assertEquals(1, store.cursor("vote-a/vote")); - assertNull(store.pending.get("vote-a/vote")); - assertEquals(SharedRewardResult.COMPLETED, execute(store.reopen(), plan, "vote-a").join()); - assertEquals(1, actions.get()); - } - - @Test - void lostClaimAcknowledgementFailsClosedWithoutRunningAction() { - Store store = new Store(); - store.loseClaimAck = true; - AtomicInteger actions = new AtomicInteger(); - SharedRewardPlan plan = plan(step("item", actions)); - - assertThrows(CompletionException.class, () -> execute(store, plan, "vote-a").join()); - assertEquals(0, actions.get()); - assertEquals(0, store.pending.get("vote-a/vote")); - CompletionException failure = assertThrows(CompletionException.class, - () -> execute(store.reopen(), plan, "vote-a").join()); - assertInstanceOf(SharedRewardIndeterminateException.class, failure.getCause()); - assertEquals(0, actions.get()); - } - - @Test - void offlineDeferralDoesNotClaimActionAndDistinctOccurrencesDoNotCollide() { - Store store = new Store(); - Platform platform = new Platform(); - platform.online = false; - AtomicInteger actions = new AtomicInteger(); - SharedRewardPlan plan = plan(new SharedRewardStep("item", true, (context, path) -> { - assertTrue(platform.nativeDispatch); - actions.incrementAndGet(); - return done(); - })); - - assertEquals(SharedRewardResult.DEFERRED, execute(platform, store, plan, "vote-a").join()); - assertNull(store.pending.get("vote-a/vote")); - platform.online = true; - assertEquals(SharedRewardResult.COMPLETED, execute(platform, store, plan, "vote-a").join()); - assertEquals(SharedRewardResult.COMPLETED, execute(platform, store, plan, "vote-b").join()); - assertEquals(2, actions.get()); - assertEquals(5, platform.nativeDispatches); - assertEquals(2, platform.claimedDispatches); - assertEquals(USER, platform.lastDispatchUser); - assertTrue(platform.lastDispatchRequiresOnline); - } - - @Test - void completedPrefixIsSkippedAndChangedPlanFailsBeforeSideEffects() { - Store store = new Store(); - AtomicInteger first = new AtomicInteger(), second = new AtomicInteger(); - SharedRewardPlan original = plan(step("first", first), step("second", second)); - store.failAtStep = 1; - assertThrows(CompletionException.class, () -> execute(store, original, "vote-a").join()); - assertEquals(1, first.get()); - assertEquals(0, second.get()); - assertEquals(1, store.cursor("vote-a/vote")); - store.failAtStep = -1; - SharedRewardPlan changed = new SharedRewardPlan("vote", 1, Duration.ZERO, List.of(), - List.of(step("first", first), step("second", second)), "changed-config"); - assertThrows(CompletionException.class, () -> execute(store, changed, "vote-a").join()); - assertEquals(0, second.get()); - assertEquals(SharedRewardResult.COMPLETED, execute(store, original, "vote-a").join()); - assertEquals(1, first.get()); - assertEquals(1, second.get()); - } - - private static SharedRewardPlan plan(SharedRewardStep... steps) { - return new SharedRewardPlan("vote", 1, Duration.ZERO, List.of(), List.of(steps), "config-v1"); - } - - private static SharedRewardStep step(String id, AtomicInteger calls) { - return new SharedRewardStep(id, false, (context, path) -> { - calls.incrementAndGet(); - return done(); - }); - } - - private static CompletionStage done() { - return CompletableFuture.completedFuture(SharedRewardResult.COMPLETED); - } - - private static CompletableFuture execute(Store store, SharedRewardPlan plan, String key) { - return execute(new Platform(), store, plan, key); - } - - private static CompletableFuture execute(Platform platform, Store store, - SharedRewardPlan plan, String key) { - return new SharedRewardOrchestrator(platform) - .executeKeyed(plan, new SharedRewardContext(USER, "Ben", Map.of()), store, key).toCompletableFuture(); - } - - private static final class Platform implements SharedRewardPlatform { - boolean online = true; - boolean shuttingDown; - boolean nativeDispatch; - int nativeDispatches; - int claimedDispatches; - UUID lastDispatchUser; - boolean lastDispatchRequiresOnline; - public Instant now() { return Instant.EPOCH; } - public boolean isOnline(UUID uuid) { - assertTrue(nativeDispatch); - return online; - } - public boolean isShuttingDown() { return shuttingDown; } - public double nextChanceRoll() { return 0; } - public CompletionStage delay(Duration delay, - Supplier> work) { return work.get(); } - public CompletionStage checkActionAvailability(UUID userId) { - assertFalse(nativeDispatch); - nativeDispatches++; - nativeDispatch = true; - try { return CompletableFuture.completedFuture(isOnline(userId)); } - finally { nativeDispatch = false; } - } - public CompletionStage runClaimedAction( - UUID userId, boolean requiresOnlinePlayer, Supplier> operation) { - assertFalse(nativeDispatch); - nativeDispatches++; - claimedDispatches++; - lastDispatchUser = userId; - lastDispatchRequiresOnline = requiresOnlinePlayer; - nativeDispatch = true; - try { return operation.get(); } - finally { nativeDispatch = false; } - } - } - - /** Simulates a caller-owned durable row that survives constructing a new adapter. */ - private static final class Store implements SharedRewardKeyedDurability { - final Map progress; - final Map pending; - boolean failCheckpoint, loseClaimAck, loseCheckpointAck; - CompletableFuture delayedClaim; - int failAtStep = -1; - - Store() { this(new HashMap<>(), new HashMap<>()); } - Store(Map progress, Map pending) { - this.progress = progress; - this.pending = pending; - } - Store reopen() { return new Store(progress, pending); } - synchronized int cursor(String path) { return progress.get(path).completedSteps(); } - public boolean durable() { return true; } - public synchronized int completedSteps(String path) { return progress.containsKey(path) ? cursor(path) : 0; } - public synchronized SharedRewardProgress loadProgress(String path) { return progress.get(path); } - public synchronized CompletionStage begin(String path, SharedRewardProgress proposed) { - progress.putIfAbsent(path, proposed); - return CompletableFuture.completedFuture(progress.get(path)); - } - public synchronized CompletionStage claimAction(String path, String fingerprint, - int index, SharedRewardContext context) { - SharedRewardProgress state = progress.get(path); - if (state == null || !fingerprint.equals(state.planFingerprint()) || state.completedSteps() != index) { - return CompletableFuture.completedFuture(SharedRewardActionClaim.INDETERMINATE); - } - if (failAtStep == index) return CompletableFuture.failedFuture(new IllegalStateException("claim failed")); - if (pending.putIfAbsent(path, index) != null) { - return CompletableFuture.completedFuture(SharedRewardActionClaim.INDETERMINATE); - } - if (delayedClaim != null) return delayedClaim; - if (loseClaimAck) return CompletableFuture.failedFuture(new IllegalStateException("lost claim acknowledgement")); - return CompletableFuture.completedFuture(SharedRewardActionClaim.STARTED); - } - public synchronized CompletionStage checkpoint(String path, String fingerprint, - int nextStep, SharedRewardContext context) { - if (failCheckpoint) return CompletableFuture.failedFuture(new IllegalStateException("checkpoint failed")); - if (pending.get(path) == null || pending.get(path) != nextStep - 1) { - return CompletableFuture.failedFuture(new IllegalStateException("missing pending action")); - } - progress.put(path, progress.get(path).advance(nextStep, context)); - pending.remove(path); - if (loseCheckpointAck) return CompletableFuture.failedFuture(new IllegalStateException("lost checkpoint acknowledgement")); - return CompletableFuture.completedFuture(null); - } - public CompletionStage checkpoint(String path, int nextStep, SharedRewardContext context) { - return checkpoint(path, progress.get(path).planFingerprint(), nextStep, context); - } - public CompletionStage defer(String path, int nextStep, SharedRewardContext context) { - return CompletableFuture.completedFuture(null); - } - } -} From 905fe0f4cbb7d10cb6dd0df7925b964856366765 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:08:05 +0000 Subject: [PATCH 2/3] Synchronize reward loading and subreward checks on the registry monitor --- .../api/rewards/RewardHandler.java | 12 ++++-- .../api/rewards/RewardLoader.java | 28 +++++++------ .../tests/rewards/RewardServicesTest.java | 40 +++++++++++++++++++ 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java index 4312db52ea..c1c48b5c71 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardHandler.java @@ -102,11 +102,15 @@ public void checkDirectlyDefined() { } public void checkSubRewards() { - subRewardResolver.checkSubRewards(); + synchronized (rewardRegistry) { + subRewardResolver.checkSubRewards(); + } } public void checkSubRewards(DefinedReward direct) { - subRewardResolver.checkSubRewards(direct); + synchronized (rewardRegistry) { + subRewardResolver.checkSubRewards(direct); + } } public File getDefaultFolder() { @@ -239,7 +243,9 @@ public void loadInjectedRewards() { } public void loadRewards() { - rewardLoader.loadRewards(); + synchronized (rewardRegistry) { + rewardLoader.loadRewards(); + } } public void openSubReward(Player player, String path, RewardEditData reward) { diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java index 0912da1b3c..709a4bafcc 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardLoader.java @@ -198,20 +198,22 @@ public ArrayList getRewardNames(File file) { } public void loadRewards() { - suppressGeneratedDirectlyDefinedFiles(); - handler.getRewardRegistry().resetRewards(); - setupExample(); - handler.addValidPath("DirectlyDefinedReward"); - handler.addValidPath("Delayed"); - handler.addValidPath("Timed"); - handler.addValidPath("DisplayItem"); - handler.addValidPath("ForceOffline"); - for (File file : rewardFolders) { - loadRewards(file); + synchronized (handler.getRewardRegistry()) { + suppressGeneratedDirectlyDefinedFiles(); + handler.getRewardRegistry().resetRewards(); + setupExample(); + handler.addValidPath("DirectlyDefinedReward"); + handler.addValidPath("Delayed"); + handler.addValidPath("Timed"); + handler.addValidPath("DisplayItem"); + handler.addValidPath("ForceOffline"); + for (File file : rewardFolders) { + loadRewards(file); + } + handler.sortInjectedRewards(); + handler.sortInjectedRequirements(); + plugin.debug("Loaded rewards"); } - handler.sortInjectedRewards(); - handler.sortInjectedRequirements(); - plugin.debug("Loaded rewards"); } private void loadRewards(File file) { diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java index a1755a4520..3a79005b06 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java @@ -4,9 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -16,6 +19,10 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.logging.Logger; import org.bukkit.configuration.file.YamlConfiguration; @@ -23,7 +30,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; +import com.bencodez.advancedcore.AdvancedCoreConfigOptions; import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.rewards.DefinedReward; import com.bencodez.advancedcore.api.rewards.DirectlyDefinedReward; @@ -47,8 +56,10 @@ public class RewardServicesTest { public void setUp() { plugin = mock(AdvancedCorePlugin.class); logger = mock(Logger.class); + AdvancedCoreConfigOptions options = mock(AdvancedCoreConfigOptions.class); when(plugin.getDataFolder()).thenReturn(tempDir); when(plugin.getLogger()).thenReturn(logger); + when(plugin.getOptions()).thenReturn(options); AdvancedCorePlugin.setInstance(plugin); handler = new RewardHandler(plugin); } @@ -179,4 +190,33 @@ public void fileBackedSubRewardsRemainSnapshotCapableForDeferredExecution() { assertTrue(fileBacked.isNeedsRewardFile()); assertFalse(directlyDefined.isNeedsRewardFile()); } + + @Test + public void registryRebuildAndSubRewardChecksUseTheSameMonitor() throws Exception { + DefinedReward direct = mock(DefinedReward.class); + when(direct.getFullPath()).thenReturn("Direct"); + + assertWaitsForRegistryMonitor(handler.getRewardLoader()::loadRewards); + assertWaitsForRegistryMonitor(handler::loadRewards); + assertWaitsForRegistryMonitor(handler::checkSubRewards); + assertWaitsForRegistryMonitor(() -> handler.checkSubRewards(direct)); + } + + private void assertWaitsForRegistryMonitor(Runnable operation) throws Exception { + CountDownLatch started = new CountDownLatch(1); + CompletableFuture invocation; + synchronized (handler.getRewardRegistry()) { + invocation = CompletableFuture.runAsync(() -> { + try (MockedStatic pluginClass = mockStatic(AdvancedCorePlugin.class, + CALLS_REAL_METHODS)) { + pluginClass.when(AdvancedCorePlugin::getInstance).thenReturn(plugin); + started.countDown(); + operation.run(); + } + }); + assertTrue(started.await(5, TimeUnit.SECONDS)); + assertThrows(TimeoutException.class, () -> invocation.get(100, TimeUnit.MILLISECONDS)); + } + invocation.get(5, TimeUnit.SECONDS); + } } From 0cb17a6675efb1ac2359d208fc30b659b8092d87 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 20:21:22 +0000 Subject: [PATCH 3/3] Assert worker thread blocking in reward registry monitor tests --- .../tests/rewards/RewardServicesTest.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java index 3a79005b06..68b3f0d0ee 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardServicesTest.java @@ -19,8 +19,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; @@ -203,18 +203,25 @@ public void registryRebuildAndSubRewardChecksUseTheSameMonitor() throws Exceptio } private void assertWaitsForRegistryMonitor(Runnable operation) throws Exception { - CountDownLatch started = new CountDownLatch(1); - CompletableFuture invocation; + CountDownLatch monitorAcquisitionAttempted = new CountDownLatch(1); + FutureTask invocation = new FutureTask<>(() -> { + try (MockedStatic pluginClass = mockStatic(AdvancedCorePlugin.class, + CALLS_REAL_METHODS)) { + pluginClass.when(AdvancedCorePlugin::getInstance).thenReturn(plugin); + monitorAcquisitionAttempted.countDown(); + operation.run(); + } + return null; + }); + Thread worker = new Thread(invocation, "reward-registry-monitor-test"); synchronized (handler.getRewardRegistry()) { - invocation = CompletableFuture.runAsync(() -> { - try (MockedStatic pluginClass = mockStatic(AdvancedCorePlugin.class, - CALLS_REAL_METHODS)) { - pluginClass.when(AdvancedCorePlugin::getInstance).thenReturn(plugin); - started.countDown(); - operation.run(); - } - }); - assertTrue(started.await(5, TimeUnit.SECONDS)); + worker.start(); + assertTrue(monitorAcquisitionAttempted.await(5, TimeUnit.SECONDS)); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (worker.getState() != Thread.State.BLOCKED && System.nanoTime() < deadline) { + Thread.yield(); + } + assertEquals(Thread.State.BLOCKED, worker.getState()); assertThrows(TimeoutException.class, () -> invocation.get(100, TimeUnit.MILLISECONDS)); } invocation.get(5, TimeUnit.SECONDS);