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);