diff --git a/AdvancedCore/pom.xml b/AdvancedCore/pom.xml index 383ffe5bbe..d4da9904fc 100644 --- a/AdvancedCore/pom.xml +++ b/AdvancedCore/pom.xml @@ -208,7 +208,7 @@ com.bencodez simpleapi - 1.0.2-20260905.234759-10 + 1.0.2-SNAPSHOT compile @@ -513,4 +513,4 @@ - + \ No newline at end of file diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardFileData.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardFileData.java index 6844b1ea2d..3a521532ff 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardFileData.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/rewards/RewardFileData.java @@ -14,6 +14,8 @@ import com.bencodez.advancedcore.AdvancedCorePlugin; import com.bencodez.advancedcore.api.misc.files.FilesManager; +import com.bencodez.advancedcore.bukkit.rewards.BukkitRewardConfigReader; +import com.bencodez.advancedcore.core.rewards.RewardConfigReader; import com.bencodez.simpleapi.file.CaseInsensitiveSection; import lombok.Getter; @@ -26,6 +28,10 @@ public class RewardFileData { private ConfigurationSection configData; + // Resolve through the public getter on each read, including after reloads + // and for subclasses that supply their own configuration section. + private final RewardConfigReader rewardConfig = new BukkitRewardConfigReader(this::getConfigData); + /** The data file. */ private File dataFile; @@ -79,14 +85,11 @@ public void createSection(String path) { * @return the chance */ public double getChance() { - return getConfigData().getDouble("Chance"); + return rewardConfig.getChance(); } public Set getChoices() { - if (getConfigData().isConfigurationSection("Choices")) { - return getConfigData().getConfigurationSection("Choices").getKeys(false); - } - return new HashSet<>(); + return rewardConfig.getChoices(); } public ConfigurationSection getChoicesItem(String choice) { @@ -94,7 +97,7 @@ public ConfigurationSection getChoicesItem(String choice) { } public String getChoicesRewardsPath(String choice) { - return "Choices." + choice + ".Rewards"; + return rewardConfig.getChoicesRewardsPath(choice); } /** @@ -104,11 +107,7 @@ public String getChoicesRewardsPath(String choice) { */ @SuppressWarnings("unchecked") public ArrayList getCommandsConsole() { - if (getConfigData().isList("Commands")) { - return (ArrayList) getConfigData().getList("Commands", new ArrayList<>()); - } - return (ArrayList) getConfigData().getList("Commands.Console", new ArrayList<>()); - + return (ArrayList) rewardConfig.getCommandsConsole(); } /** @@ -118,7 +117,7 @@ public ArrayList getCommandsConsole() { */ @SuppressWarnings("unchecked") public ArrayList getCommandsPlayer() { - return (ArrayList) getConfigData().getList("Commands.Player", new ArrayList<>()); + return (ArrayList) rewardConfig.getCommandsPlayer(); } public ConfigurationSection getConfigData() { @@ -138,7 +137,7 @@ public File getDataFile() { * @return the delayed enabled */ public boolean getDelayedEnabled() { - return getConfigData().getBoolean("Delayed.Enabled"); + return rewardConfig.getDelayedEnabled(); } /** @@ -147,11 +146,11 @@ public boolean getDelayedEnabled() { * @return the delayed hours */ public int getDelayedHours() { - return getConfigData().getInt("Delayed.Hours"); + return rewardConfig.getDelayedHours(); } public int getDelayedMilliSeconds() { - return getConfigData().getInt("Delayed.MilliSeconds"); + return rewardConfig.getDelayedMilliSeconds(); } /** @@ -160,11 +159,11 @@ public int getDelayedMilliSeconds() { * @return the delayed minutes */ public int getDelayedMinutes() { - return getConfigData().getInt("Delayed.Minutes"); + return rewardConfig.getDelayedMinutes(); } public int getDelayedSeconds() { - return getConfigData().getInt("Delayed.Seconds"); + return rewardConfig.getDelayedSeconds(); } /** @@ -186,7 +185,7 @@ public ConfigurationSection getDisplayItem() { * @return the choice rewards enabled */ public boolean getEnableChoices() { - return getConfigData().getBoolean("EnableChoices"); + return rewardConfig.getEnableChoices(); } public FileConfiguration getFileData() { @@ -194,7 +193,7 @@ public FileConfiguration getFileData() { } public boolean getForceOffline() { - return getConfigData().getBoolean("ForceOffline"); + return rewardConfig.getForceOffline(); } /** @@ -352,7 +351,7 @@ public String getPermission() { @SuppressWarnings("unchecked") public ArrayList getPriority() { - return (ArrayList) getConfigData().getList("Priority", new ArrayList<>()); + return (ArrayList) rewardConfig.getPriority(); } /** @@ -361,7 +360,7 @@ public ArrayList getPriority() { * @return the require permission */ public boolean getRequirePermission() { - return getConfigData().getBoolean("RequirePermission"); + return rewardConfig.getRequirePermission(); } /** @@ -370,22 +369,11 @@ public boolean getRequirePermission() { * @return the reward type */ public String getRewardType() { - String str = getConfigData().getString("RewardType", "BOTH"); - if (str != null) { - if (str.equalsIgnoreCase("online")) { - return "ONLINE"; - } - if (str.equalsIgnoreCase("offline")) { - return "OFFLINE"; - } else { - return "BOTH"; - } - } - return "BOTH"; + return rewardConfig.getRewardType(); } public String getServer() { - return getConfigData().getString("Server", ""); + return rewardConfig.getServer(); } /** @@ -394,7 +382,7 @@ public String getServer() { * @return the timed enabled */ public boolean getTimedEnabled() { - return getConfigData().getBoolean("Timed.Enabled"); + return rewardConfig.getTimedEnabled(); } /** @@ -403,7 +391,7 @@ public boolean getTimedEnabled() { * @return the timed hour */ public int getTimedHour() { - return getConfigData().getInt("Timed.Hour"); + return rewardConfig.getTimedHour(); } /** @@ -412,7 +400,7 @@ public int getTimedHour() { * @return the timed minute */ public int getTimedMinute() { - return getConfigData().getInt("Timed.Minute"); + return rewardConfig.getTimedMinute(); } /** @@ -422,9 +410,7 @@ public int getTimedMinute() { */ @SuppressWarnings("unchecked") public ArrayList getWorlds() { - - return (ArrayList) getConfigData().getList("Worlds", new ArrayList<>()); - + return (ArrayList) rewardConfig.getWorlds(); } public boolean hasRewardFile() { @@ -432,7 +418,7 @@ public boolean hasRewardFile() { } public boolean isDirectlyDefinedReward() { - return getConfigData().getBoolean("DirectlyDefinedReward"); + return rewardConfig.isDirectlyDefinedReward(); } public boolean isRewardFile() { diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/rewards/BukkitRewardConfigReader.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/rewards/BukkitRewardConfigReader.java new file mode 100644 index 0000000000..5ca35dcc4e --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/rewards/BukkitRewardConfigReader.java @@ -0,0 +1,58 @@ +package com.bencodez.advancedcore.bukkit.rewards; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +import org.bukkit.configuration.ConfigurationSection; + +import com.bencodez.advancedcore.core.rewards.RewardConfigReader; +import com.bencodez.simpleapi.bukkit.config.BukkitStructuredConfigView; + +/** + * Keeps native Bukkit getter/default and raw-list behavior while the reward + * paths and interpretation live in the shared reader. It deliberately does not + * add a case-insensitive wrapper: the supplied section already owns that policy. + */ +public final class BukkitRewardConfigReader extends RewardConfigReader { + private final Supplier source; + + public BukkitRewardConfigReader(Supplier source) { + super(() -> new BukkitStructuredConfigView(source.get())); + this.source = Objects.requireNonNull(source, "source"); + } + + private ConfigurationSection section() { + return Objects.requireNonNull(source.get(), "configuration"); + } + + // Explicit fallback overloads do not have the same default-tree behavior + // as these legacy no-fallback Bukkit getters. + @Override + protected boolean readBoolean(String path) { + return section().getBoolean(path); + } + + @Override + protected int readInt(String path) { + return section().getInt(path); + } + + @Override + protected double readDouble(String path) { + return section().getDouble(path); + } + + @Override + protected boolean isList(String path) { + return section().isList(path); + } + + @Override + protected List readList(String path) { + // No copying, filtering, coercion or change to the legacy ArrayList + // cast in RewardFileData. The empty fallback is fresh on every read. + return section().getList(path, new ArrayList<>()); + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/rewards/RewardConfigReader.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/rewards/RewardConfigReader.java new file mode 100644 index 0000000000..a5ff9899e2 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/rewards/RewardConfigReader.java @@ -0,0 +1,185 @@ +package com.bencodez.advancedcore.core.rewards; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +import com.bencodez.simpleapi.core.config.StructuredConfigView; +import com.bencodez.simpleapi.core.config.StructuredConfigView.Kind; + +/** + * Platform-neutral interpretation of reward settings. This class neither loads + * files nor executes rewards. The supplied view owns casing, paths and typed + * conversion rules; wrap it in SimpleAPI's case-insensitive view when required. + * + *

The supplier is evaluated on reads, not construction, so replacing a + * configuration does not leave this reader bound to a stale section. A live + * view is not made thread-safe by this reader.

+ */ +public class RewardConfigReader { + private final Supplier source; + + public RewardConfigReader(Supplier source) { + this.source = Objects.requireNonNull(source, "source"); + } + + protected final StructuredConfigView config() { + return Objects.requireNonNull(source.get(), "configuration"); + } + + /** Adapter seam for native getters with implicit default-tree semantics. */ + protected boolean readBoolean(String path) { + return config().getBoolean(path, false); + } + + protected int readInt(String path) { + return config().getInt(path, 0); + } + + protected double readDouble(String path) { + return config().getDouble(path, 0); + } + + protected boolean isList(String path) { + return config().kind(path) == Kind.LIST; + } + + /** + * Plain-data lists retain order and value types, without string coercion or + * filtering. The default implementation returns a detached, unmodifiable + * list. A native compatibility adapter may preserve its existing live list. + */ + protected List readList(String path) { + StructuredConfigView view = config(); + if (view.kind(path) != Kind.LIST) { + return Collections.emptyList(); + } + return (List) view.value(path); + } + + public double getChance() { + return readDouble("Chance"); + } + + public Set getChoices() { + if (config().isConfigurationSection("Choices")) { + return config().getConfigurationSection("Choices").getKeys(false); + } + return new HashSet<>(); + } + + public String getChoicesRewardsPath(String choice) { + return "Choices." + choice + ".Rewards"; + } + + public List getCommandsConsole() { + if (isList("Commands")) { + return readList("Commands"); + } + return readList("Commands.Console"); + } + + public List getCommandsPlayer() { + return readList("Commands.Player"); + } + + public boolean getDelayedEnabled() { + return readBoolean("Delayed.Enabled"); + } + + public int getDelayedHours() { + return readInt("Delayed.Hours"); + } + + public int getDelayedMilliSeconds() { + return readInt("Delayed.MilliSeconds"); + } + + public int getDelayedMinutes() { + return readInt("Delayed.Minutes"); + } + + public int getDelayedSeconds() { + return readInt("Delayed.Seconds"); + } + + public boolean getEnableChoices() { + return readBoolean("EnableChoices"); + } + + public boolean getForceOffline() { + return readBoolean("ForceOffline"); + } + + public String getPermission(String rewardName) { + return config().getString("Permission", "AdvancedCore.Reward." + rewardName); + } + + public List getPriority() { + return readList("Priority"); + } + + public boolean getRequirePermission() { + return readBoolean("RequirePermission"); + } + + public String getRewardType() { + String type = config().getString("RewardType", "BOTH"); + if ("online".equalsIgnoreCase(type)) { + return "ONLINE"; + } + if ("offline".equalsIgnoreCase(type)) { + return "OFFLINE"; + } + return "BOTH"; + } + + public String getServer() { + return config().getString("Server", ""); + } + + public boolean getTimedEnabled() { + return readBoolean("Timed.Enabled"); + } + + public int getTimedHour() { + return readInt("Timed.Hour"); + } + + public int getTimedMinute() { + return readInt("Timed.Minute"); + } + + public List getWorlds() { + return readList("Worlds"); + } + + public boolean isDirectlyDefinedReward() { + return readBoolean("DirectlyDefinedReward"); + } + + /** Native objects remain in the adapter; these methods return read views. */ + public StructuredConfigView getChoicesItem(String choice) { + return config().getConfigurationSection("Choices." + choice + ".DisplayItem"); + } + + public StructuredConfigView getChoiceRewards(String choice) { + return config().getConfigurationSection(getChoicesRewardsPath(choice)); + } + + public StructuredConfigView getDisplayItem() { + return config().getConfigurationSection("DisplayItem"); + } + + public StructuredConfigView getItemSection(String item) { + return config().getConfigurationSection("Items." + item); + } + + /** Literal keys allow definition names containing the path separator. */ + public StructuredConfigView definitionAt(String... keys) { + return config().structuredAt(keys); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/build/BuildInputPinningTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/build/BuildInputPinningTest.java index c2fc3069f1..25ca5401a6 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/build/BuildInputPinningTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/build/BuildInputPinningTest.java @@ -24,15 +24,4 @@ void javadocPublisherUsesImmutableRevisionAndExplicitPermissions() throws IOExce assertTrue(workflow.contains("maven-javadoc-plugin:3.12.0:javadoc")); assertFalse(workflow.contains("Javadoc-publisher.yml@main")); } - - @Test - void simpleApiDependencyUsesImmutableSnapshotBuild() throws IOException { - String pom = Files.readString(Path.of("pom.xml")); - int dependency = pom.indexOf("simpleapi"); - - assertTrue(dependency >= 0); - String declaration = pom.substring(dependency, Math.min(pom.length(), dependency + 200)); - assertFalse(declaration.contains("SNAPSHOT")); - assertTrue(declaration.contains("1.0.2-20260905.234759-10")); - } } diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/CoreRewardConfigFixture.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/CoreRewardConfigFixture.java new file mode 100644 index 0000000000..938c46e538 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/CoreRewardConfigFixture.java @@ -0,0 +1,178 @@ +package com.bencodez.advancedcore.tests.rewards; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import com.bencodez.advancedcore.core.rewards.RewardConfigReader; +import com.bencodez.simpleapi.core.config.StructuredConfigView; +import com.bencodez.simpleapi.core.config.StructuredConfigView.Kind; + +/** JDK-only behavioral fixture; no Bukkit, Configurate, Mockito or JUnit linkage. */ +public final class CoreRewardConfigFixture { + private CoreRewardConfigFixture() { } + + public static void main(String[] args) { + run(); + System.out.println("CoreRewardConfigFixture: 10 scenarios passed"); + } + + public static void run() { + Map values = new LinkedHashMap<>(); + AtomicReference source = new AtomicReference<>(view(values)); + RewardConfigReader reader = new RewardConfigReader(source::get); + + equal(0.0, reader.getChance()); + equal("BOTH", reader.getRewardType()); + equal("", reader.getServer()); + equal("AdvancedCore.Reward.Daily", reader.getPermission("Daily")); + equal(List.of(), reader.getCommandsConsole()); + equal(false, reader.getDelayedEnabled()); + equal(0, reader.getTimedHour()); + + List commands = new ArrayList<>(List.of("first", 7, true, "second")); + values.put("Commands", commands); + List snapshot = reader.getCommandsConsole(); + equal(commands, snapshot); + commands.add("later"); + equal(4, snapshot.size()); + try { + snapshot.clear(); + throw new AssertionError("A plain-data list must be unmodifiable"); + } catch (UnsupportedOperationException expected) { } + + values.put("Commands", Map.of("Console", List.of("console"), "Player", List.of("player"))); + equal(List.of("console"), reader.getCommandsConsole()); + equal(List.of("player"), reader.getCommandsPlayer()); + values.put("Commands", "not a command list"); + equal(List.of(), reader.getCommandsConsole()); + equal(List.of(), reader.getCommandsPlayer()); + + values.put("Chance", 17.5); + values.put("Delayed", Map.of("Enabled", true, "Hours", 2, "Minutes", 3, + "Seconds", 4, "MilliSeconds", 5)); + values.put("Timed", Map.of("Enabled", true, "Hour", 6, "Minute", 7)); + equal(17.5, reader.getChance()); + equal(true, reader.getDelayedEnabled()); + equal(2, reader.getDelayedHours()); + equal(3, reader.getDelayedMinutes()); + equal(4, reader.getDelayedSeconds()); + equal(5, reader.getDelayedMilliSeconds()); + equal(true, reader.getTimedEnabled()); + equal(6, reader.getTimedHour()); + equal(7, reader.getTimedMinute()); + + for (String type : List.of("ONLINE", "online", "OnLiNe", "OFFLINE", "offline", "other", " online ")) { + values.put("RewardType", type); + String expected = type.equalsIgnoreCase("online") ? "ONLINE" + : type.equalsIgnoreCase("offline") ? "OFFLINE" : "BOTH"; + equal(expected, reader.getRewardType()); + } + + values.put("EnableChoices", true); + values.put("ForceOffline", true); + values.put("RequirePermission", true); + values.put("DirectlyDefinedReward", true); + values.put("Permission", "custom.reward"); + values.put("Priority", List.of("first", "second")); + values.put("Worlds", List.of("world")); + equal(true, reader.getEnableChoices()); + equal(true, reader.getForceOffline()); + equal(true, reader.getRequirePermission()); + equal(true, reader.isDirectlyDefinedReward()); + equal("custom.reward", reader.getPermission("Other")); + equal(List.of("first", "second"), reader.getPriority()); + equal(List.of("world"), reader.getWorlds()); + + Map reward = Map.of("Commands", List.of("nested")); + values.put("Choices", Map.of("one", Map.of("Rewards", reward), "with.dot", Map.of("Rewards", reward))); + equal(Set.of("one", "with.dot"), reader.getChoices()); + equal("Choices.one.Rewards", reader.getChoicesRewardsPath("one")); + equal(List.of("nested"), new RewardConfigReader(() -> reader.getChoiceRewards("one")).getCommandsConsole()); + StructuredConfigView literal = reader.definitionAt("Choices", "with.dot", "Rewards"); + equal(List.of("nested"), new RewardConfigReader(() -> literal).getCommandsConsole()); + + values.put("DisplayItem", Map.of("Material", "STONE")); + values.put("Items", Map.of("gift", Map.of("Amount", 3))); + equal("STONE", reader.getDisplayItem().getString("Material", null)); + equal(3, reader.getItemSection("gift").getInt("Amount", 0)); + equal(null, reader.getItemSection("missing")); + equal(null, reader.getChoicesItem("one")); + + source.set(view(new LinkedHashMap<>(Map.of("Chance", 91.0, "RewardType", "offline")))); + equal(91.0, reader.getChance()); + equal("OFFLINE", reader.getRewardType()); + equal(List.of(), reader.getPriority()); + + source.set(view(new LinkedHashMap<>(Map.of("Chance", "bad", "Delayed", Map.of("Enabled", "true"))))); + equal(0.0, reader.getChance()); + equal(false, reader.getDelayedEnabled()); + } + + private static void equal(Object expected, Object actual) { + if (!Objects.equals(expected, actual)) { + throw new AssertionError("Expected " + expected + " but got " + actual); + } + } + + private static Object resolve(Map values, String... keys) { + Object current = values; + for (String key : keys) { + if (!(current instanceof Map map)) return null; + current = map.get(key); + } + return current; + } + + private static Object detached(Object value) { + if (value instanceof List list) { + List copy = new ArrayList<>(); + for (Object entry : list) copy.add(detached(entry)); + return Collections.unmodifiableList(copy); + } + if (value instanceof Map map) { + Map copy = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) copy.put((String) entry.getKey(), detached(entry.getValue())); + return Collections.unmodifiableMap(copy); + } + return value; + } + + @SuppressWarnings("unchecked") + private static StructuredConfigView view(Map values) { + return (StructuredConfigView) Proxy.newProxyInstance(StructuredConfigView.class.getClassLoader(), + new Class[] { StructuredConfigView.class }, (proxy, method, args) -> { + String name = method.getName(); + if (name.equals("getKeys")) return new LinkedHashSet<>(values.keySet()); + if (name.equals("toString")) return "FixtureView"; + if (name.equals("hashCode")) return System.identityHashCode(proxy); + if (name.equals("equals")) return proxy == args[0]; + String[] keys = args[0] instanceof String[] array ? array + : ((String) args[0]).isEmpty() ? new String[0] : ((String) args[0]).split("\\."); + Object value = resolve(values, keys); + return switch (name) { + case "contains" -> value != null; + case "getString" -> value == null ? args[1] : String.valueOf(value); + case "getBoolean" -> value instanceof Boolean ? value : args[1]; + case "getInt" -> value instanceof Number number ? number.intValue() : args[1]; + case "getLong" -> value instanceof Number number ? number.longValue() : args[1]; + case "getDouble" -> value instanceof Number number ? number.doubleValue() : args[1]; + case "kind", "kindAt" -> value == null ? Kind.MISSING : value instanceof Map ? Kind.SECTION + : value instanceof List ? Kind.LIST : value instanceof Number ? Kind.NUMBER + : value instanceof Boolean ? Kind.BOOLEAN : value instanceof String ? Kind.STRING : Kind.OTHER; + case "value", "valueAt" -> detached(value); + case "isConfigurationSection" -> value instanceof Map; + case "getConfigurationSection", "at", "structuredAt" -> value instanceof Map + ? view((Map) value) : null; + default -> throw new AssertionError("Unexpected getter: " + name); + }; + }); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardConfigBukkitCompatibilityTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardConfigBukkitCompatibilityTest.java new file mode 100644 index 0000000000..f52539ebe6 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardConfigBukkitCompatibilityTest.java @@ -0,0 +1,263 @@ +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.spongepowered.configurate.BasicConfigurationNode; + +import com.bencodez.advancedcore.api.rewards.RewardFileData; +import com.bencodez.advancedcore.bukkit.rewards.BukkitRewardConfigReader; +import com.bencodez.advancedcore.core.rewards.RewardConfigReader; +import com.bencodez.simpleapi.core.config.ConfigurateStructuredConfigView; +import com.bencodez.simpleapi.core.config.StructuredConfigView; + +class RewardConfigBukkitCompatibilityTest { + @TempDir + Path tempDir; + + @Test + void defaultAndMalformedScalarReadsKeepNativeDefaultTreeRules() { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.addDefault("Chance", 12.5); + yaml.addDefault("Delayed.Enabled", true); + yaml.addDefault("Delayed.Hours", 3); + yaml.addDefault("Timed.Hour", 7); + yaml.addDefault("Server", "default-server"); + RewardFileData data = new RewardFileData(null, yaml); + assertScalars(data); + yaml.set("Chance", "not a number"); + yaml.set("Delayed.Enabled", "true"); + yaml.set("Delayed.Hours", 2.75); + yaml.set("Timed.Hour", "bad"); + assertScalars(data); + } + + @Test + void liveCaseInsensitiveReadsAndCasingCollisionsMatchExistingSection() { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("chance", 2.5); + yaml.set("Chance", 99); + yaml.set("dElAyEd.hOuRs", 3); + yaml.set("rEwArDtYpE", "oFfLiNe"); + RewardFileData data = new RewardFileData(null, yaml); + assertScalars(data); + assertEquals("OFFLINE", data.getRewardType()); + yaml.set("chance", 8.5); + assertScalars(data); + } + + @Test + void rawCommandListsKeepIdentityOrderAndUnsupportedNativeValues() { + YamlConfiguration yaml = new YamlConfiguration(); + Object nativeValue = new Object(); + ArrayList commands = new ArrayList<>(List.of("first", 7, nativeValue, "last")); + yaml.set("Commands", commands); + RewardFileData data = new RewardFileData(null, yaml); + assertSame(data.getConfigData().getList("Commands"), data.getCommandsConsole()); + assertSame(nativeValue, ((List) data.getCommandsConsole()).get(2)); + commands.add("later"); + assertEquals(5, data.getCommandsConsole().size()); + } + + @Test + void mappedCommandPriorityAndWorldListsStayLive() { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("Commands.Console", new ArrayList<>(List.of("console"))); + yaml.set("Commands.Player", new ArrayList<>(List.of("player"))); + yaml.set("Priority", new ArrayList<>(List.of("a", "b"))); + yaml.set("Worlds", new ArrayList<>(List.of("world"))); + RewardFileData data = new RewardFileData(null, yaml); + assertSame(data.getConfigData().getList("Commands.Console"), data.getCommandsConsole()); + assertSame(data.getConfigData().getList("Commands.Player"), data.getCommandsPlayer()); + assertSame(data.getConfigData().getList("Priority"), data.getPriority()); + assertSame(data.getConfigData().getList("Worlds"), data.getWorlds()); + } + + @Test + void missingListsStillReturnFreshMutableArrayLists() { + RewardFileData data = new RewardFileData(null, new YamlConfiguration()); + ArrayList first = data.getCommandsConsole(); + first.add("local edit"); + assertEquals(List.of(), data.getCommandsConsole()); + assertNotSame(first, data.getCommandsConsole()); + assertEquals(ArrayList.class, data.getPriority().getClass()); + assertEquals(ArrayList.class, data.getWorlds().getClass()); + assertEquals(ArrayList.class, data.getCommandsPlayer().getClass()); + } + + @Test + void nonArrayListNativeValuesRetainTheLegacyCastFailure() { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("Commands", new LinkedList<>(List.of("command"))); + RewardFileData data = new RewardFileData(null, yaml); + data.setConfigData(yaml); + assertThrows(ClassCastException.class, data::getCommandsConsole); + yaml.set("Commands", List.of("immutable")); + assertThrows(ClassCastException.class, data::getCommandsConsole); + } + + @Test + void replacementAndOverriddenGettersAreResolvedLazily() { + YamlConfiguration first = new YamlConfiguration(); + first.set("Chance", 1.0); + YamlConfiguration second = new YamlConfiguration(); + second.set("Chance", 2.0); + RewardFileData data = new RewardFileData(null, first); + data.setConfigData(second); + assertSame(second, data.getConfigData()); + assertEquals(2.0, data.getChance()); + + class SwitchingData extends RewardFileData { + private ConfigurationSection active; + SwitchingData() { + super(null, first); + active = first; + } + @Override + public ConfigurationSection getConfigData() { + if (active == null) throw new AssertionError("Getter invoked during construction"); + return active; + } + } + SwitchingData switching = new SwitchingData(); + assertEquals(1.0, switching.getChance()); + switching.active = second; + assertEquals(2.0, switching.getChance()); + } + + @Test + void fileReloadUsesTheNewSectionWithoutChangingExistingReloadCasing() throws Exception { + Path target = tempDir.resolve("reward.yml"); + YamlConfiguration initial = new YamlConfiguration(); + initial.set("chance", 3.0); + RewardFileData data = new RewardFileData(null, initial); + assertEquals(3.0, data.getChance()); + YamlConfiguration replacement = new YamlConfiguration(); + replacement.set("Chance", 8.0); + replacement.set("chance", 12.0); + replacement.save(target.toFile()); + data.setDataFile(target.toFile()); + data.reload(); + assertSame(data.getFileData().getConfigurationSection(""), data.getConfigData()); + assertEquals(data.getConfigData().getDouble("Chance"), data.getChance()); + assertEquals(8.0, data.getChance()); + } + + @Test + void legacySectionGettersRetainNativeSectionIdentity() { + YamlConfiguration yaml = new YamlConfiguration(); + ConfigurationSection display = yaml.createSection("DisplayItem"); + ConfigurationSection item = yaml.createSection("Items.gift"); + ConfigurationSection choiceDisplay = yaml.createSection("Choices.daily.DisplayItem"); + yaml.createSection("Choices.daily.Rewards"); + RewardFileData data = new RewardFileData(null, yaml); + data.setConfigData(yaml); + assertSame(display, data.getDisplayItem()); + assertSame(item, data.getItemSection("gift")); + assertSame(choiceDisplay, data.getChoicesItem("daily")); + assertEquals(Set.of("daily"), data.getChoices()); + assertEquals("Choices.daily.Rewards", data.getChoicesRewardsPath("daily")); + } + + @Test + void rawMapsAreNotReclassifiedAsLegacyNativeSections() { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("Choices", Map.of("daily", Map.of("Chance", 25))); + RewardFileData data = new RewardFileData(null, yaml); + data.setConfigData(yaml); + assertFalse(yaml.isConfigurationSection("Choices")); + assertEquals(Set.of(), data.getChoices()); + StructuredConfigView definition = new BukkitRewardConfigReader(() -> yaml).definitionAt("Choices", "daily"); + assertNotNull(definition); + assertEquals(25, definition.getInt("Chance", 0)); + } + + @Test + void actualConfigurateAndBukkitAdaptersAgreeOnPortableSettings() throws Exception { + Map settings = new LinkedHashMap<>(); + settings.put("Chance", 32.5); + settings.put("Commands.Console", new ArrayList<>(List.of("first", "second"))); + settings.put("Commands.Player", new ArrayList<>(List.of("player"))); + settings.put("Delayed.Enabled", true); + settings.put("Delayed.Hours", 1); + settings.put("Delayed.Minutes", 2); + settings.put("Delayed.Seconds", 3); + settings.put("Delayed.MilliSeconds", 4); + settings.put("Timed.Enabled", true); + settings.put("Timed.Hour", 7); + settings.put("Timed.Minute", 15); + settings.put("EnableChoices", true); + settings.put("ForceOffline", true); + settings.put("RequirePermission", true); + settings.put("RewardType", "oNlInE"); + settings.put("Server", "survival"); + settings.put("Worlds", new ArrayList<>(List.of("world"))); + settings.put("Priority", new ArrayList<>(List.of("one", "two"))); + settings.put("DirectlyDefinedReward", true); + YamlConfiguration yaml = new YamlConfiguration(); + BasicConfigurationNode node = BasicConfigurationNode.root(); + for (Map.Entry setting : settings.entrySet()) { + yaml.set(setting.getKey(), setting.getValue()); + node.node((Object[]) setting.getKey().split("\\.")).raw(setting.getValue()); + } + RewardFileData bukkit = new RewardFileData(null, yaml); + RewardConfigReader shared = new RewardConfigReader(() -> new ConfigurateStructuredConfigView(node)); + for (String method : List.of("getChance", "getCommandsConsole", "getCommandsPlayer", "getDelayedEnabled", + "getDelayedHours", "getDelayedMinutes", "getDelayedSeconds", "getDelayedMilliSeconds", + "getTimedEnabled", "getTimedHour", "getTimedMinute", "getEnableChoices", "getForceOffline", + "getRequirePermission", "getRewardType", "getServer", "getWorlds", "getPriority", "isDirectlyDefinedReward")) { + assertEquals(RewardFileData.class.getMethod(method).invoke(bukkit), + RewardConfigReader.class.getMethod(method).invoke(shared), method); + } + } + + @Test + void nativePlainDataListsStayDetachedAndLiteralNestedNamesWork() { + BasicConfigurationNode node = BasicConfigurationNode.root(); + node.node("Commands").raw(List.of("one", 7, true)); + node.node("Choices", "daily.bonus", "Rewards", "Commands").raw(List.of("nested")); + RewardConfigReader shared = new RewardConfigReader(() -> new ConfigurateStructuredConfigView(node)); + List list = shared.getCommandsConsole(); + assertEquals(List.of("one", 7, true), list); + assertThrows(UnsupportedOperationException.class, list::clear); + node.node("Commands").raw(List.of("changed")); + assertEquals(List.of("one", 7, true), list); + StructuredConfigView nested = shared.definitionAt("Choices", "daily.bonus", "Rewards"); + assertNotNull(nested); + assertEquals(List.of("nested"), new RewardConfigReader(() -> nested).getCommandsConsole()); + } + + private static void assertScalars(RewardFileData data) { + ConfigurationSection section = data.getConfigData(); + assertEquals(section.getDouble("Chance"), data.getChance()); + assertEquals(section.getBoolean("Delayed.Enabled"), data.getDelayedEnabled()); + assertEquals(section.getInt("Delayed.Hours"), data.getDelayedHours()); + assertEquals(section.getInt("Delayed.Minutes"), data.getDelayedMinutes()); + assertEquals(section.getInt("Delayed.Seconds"), data.getDelayedSeconds()); + assertEquals(section.getInt("Delayed.MilliSeconds"), data.getDelayedMilliSeconds()); + assertEquals(section.getBoolean("Timed.Enabled"), data.getTimedEnabled()); + assertEquals(section.getInt("Timed.Hour"), data.getTimedHour()); + assertEquals(section.getInt("Timed.Minute"), data.getTimedMinute()); + assertEquals(section.getBoolean("EnableChoices"), data.getEnableChoices()); + assertEquals(section.getBoolean("ForceOffline"), data.getForceOffline()); + assertEquals(section.getBoolean("RequirePermission"), data.getRequirePermission()); + assertEquals(section.getBoolean("DirectlyDefinedReward"), data.isDirectlyDefinedReward()); + assertEquals(section.getString("Server", ""), data.getServer()); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardConfigReaderTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardConfigReaderTest.java new file mode 100644 index 0000000000..a9aec3bca7 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/rewards/RewardConfigReaderTest.java @@ -0,0 +1,46 @@ +package com.bencodez.advancedcore.tests.rewards; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.jupiter.api.Test; + +import com.bencodez.advancedcore.core.rewards.RewardConfigReader; +import com.bencodez.simpleapi.core.config.StructuredConfigView; + +class RewardConfigReaderTest { + @Test + void sharedSettingsAndNestedDefinitions() { + CoreRewardConfigFixture.run(); + } + + @Test + void sharedReaderRunsWithoutServerOrTestFrameworkClasses() throws Exception { + URL[] classpath = { + RewardConfigReader.class.getProtectionDomain().getCodeSource().getLocation(), + StructuredConfigView.class.getProtectionDomain().getCodeSource().getLocation(), + CoreRewardConfigFixture.class.getProtectionDomain().getCodeSource().getLocation() + }; + try (URLClassLoader isolated = new URLClassLoader(classpath, ClassLoader.getPlatformClassLoader()) { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("org.bukkit.") || name.startsWith("net.md_5.") + || name.startsWith("org.junit.") || name.startsWith("org.mockito.") + || name.startsWith("org.spongepowered.") + || name.startsWith("com.bencodez.advancedcore.bukkit.") + || name.startsWith("com.bencodez.advancedcore.api.") + || name.equals("com.bencodez.advancedcore.AdvancedCorePlugin")) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name, resolve); + } + }) { + assertThrows(ClassNotFoundException.class, () -> isolated.loadClass("org.bukkit.Bukkit")); + assertThrows(ClassNotFoundException.class, () -> isolated.loadClass("org.junit.jupiter.api.Test")); + Class fixture = isolated.loadClass(CoreRewardConfigFixture.class.getName()); + fixture.getMethod("run").invoke(null); + } + } +} diff --git a/docs/shared-reward-configuration.md b/docs/shared-reward-configuration.md new file mode 100644 index 0000000000..7dd738ded3 --- /dev/null +++ b/docs/shared-reward-configuration.md @@ -0,0 +1,71 @@ +# Shared reward configuration reads + +`core.rewards.RewardConfigReader` interprets reward settings through SimpleAPI's +`StructuredConfigView`. It does not open files, create users, schedule work, or +execute rewards. The supplier is evaluated on each read so a replaced root can +be observed without rebuilding the reader; the caller owns synchronization. + +## Existing Bukkit callers + +`RewardFileData` retains its constructors, public method signatures, native +section getters, file handling, setters, and generated-snapshot behavior. Its +portable setting getters delegate to `BukkitRewardConfigReader`, which reads +through the existing virtual `getConfigData()` method. Nothing is resolved during +construction. Subclasses, `setConfigData`, and file reloads keep their current +behavior, including the existing distinction between constructor and reload +case handling. + +The Bukkit adapter preserves implicit default-tree getters and raw list identity. +The existing `ArrayList` casts remain, including their historical failure +for other list implementations. This extraction does not silently filter native +objects, coerce command entries, or change list mutability. Native display/item +sections stay in the existing Bukkit API. Reward-derived permission defaults also +remain in that facade; native readers can supply a reward name explicitly. + +## Native readers + +```java +StructuredConfigView view = new ConfigurateStructuredConfigView(node); +RewardConfigReader settings = new RewardConfigReader(() -> view); +List consoleCommands = settings.getCommandsConsole(); +boolean delayed = settings.getDelayedEnabled(); +StructuredConfigView nested = settings.definitionAt("Choices", "daily.bonus", "Rewards"); +``` + +Use imports from `com.bencodez.advancedcore.core.rewards` and +`com.bencodez.simpleapi.core.config`. The supplied view controls casing, paths, +and typed conversion rules; use the appropriate SimpleAPI case-insensitive view +when required. `definitionAt` uses literal keys, so names containing dots are not +split. The named choice/item helpers retain the existing dotted-path convention. + +Plain-data lists from the neutral reader are detached and unmodifiable, retain +entry order and value types, and use SimpleAPI's bounded exports. Unsupported +native objects are rejected by that export contract. This is intentionally not +an emulation of Bukkit's native object/list identity. No command is executed by +reading it. Empty/malformed non-list settings return an empty list. + +## Dependencies and validation + +This source requires SimpleAPI's merged structured configuration APIs (SimpleAPI +PR #78 and its follow-ups): `StructuredConfigView`, `BukkitStructuredConfigView`, +and `ConfigurateStructuredConfigView`. The dependency uses `1.0.2-SNAPSHOT` so +builds can consume newer published snapshots from Nexus. The existing repository +configuration enables snapshots with `updatePolicy` set to `always`. + +The SimpleAPI timestamp-pinning test has been removed at the maintainer's request. +The separate Javadoc workflow security test remains unchanged. When validating, +record the timestamped SimpleAPI artifact Maven actually resolves; a successful +build with one snapshot does not validate later snapshots automatically. + +Tests cover portable setting/nested-definition behavior, an isolated runtime +that rejects Bukkit and JUnit, native Bukkit defaults and list semantics, +replacement/reload/subclass compatibility, and real Bukkit/Configurate parity. +Use the repository's normal Maven package command and verify these tests actually +run; a successful invocation with zero discovered JUnit tests is not validation. +A downstream VotingPlugin build must resolve the exact candidate AdvancedCore +and SimpleAPI artifacts, not unrelated cached snapshots. + +This is one Maven project with `core` and `bukkit` packages. It does not add a +shared AdvancedCore artifact, a loader implementation, or a new workflow. +Storage/FLAT removal, user extraction, reward execution (including PR #317), and +native mod packaging remain separate work. PR #314 is not reopened.