diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationBinder.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationBinder.java
new file mode 100644
index 00000000..682ebedc
--- /dev/null
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationBinder.java
@@ -0,0 +1,309 @@
+package com.bencodez.simpleapi.file.annotation;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+import com.bencodez.simpleapi.file.config.ConfigView;
+
+import com.bencodez.simpleapi.time.ParsedDuration;
+
+/**
+ * Platform-neutral implementation of the existing configuration annotations.
+ *
+ *
This is a compatibility extraction, not a change to the annotation rules.
+ * Field fallback values, empty-list handling, declared-field-only traversal,
+ * annotation order and per-field exception isolation are intentionally retained.
+ * In particular, the legacy zero-default long reflection behavior is unchanged.
+ */
+public class AnnotationBinder {
+
+ private final Function sectionValue;
+
+ /** Creates a binder that assigns ConfigView values to section fields. */
+ public AnnotationBinder() {
+ this(view -> view);
+ }
+
+ /**
+ * Creates a binder with a platform-specific section projection. Only the
+ * compatibility adapter should unwrap a view to a native section. The
+ * projection is invoked for present sections, never for an absent section.
+ *
+ * @param sectionValue projection used for ConfigDataConfigurationSection
+ */
+ public AnnotationBinder(Function sectionValue) {
+ this.sectionValue = Objects.requireNonNull(sectionValue, "sectionValue");
+ }
+
+ @SuppressWarnings("unchecked")
+ public void load(ConfigView config, Object classToLoad) {
+ Class> clazz = classToLoad.getClass();
+
+ for (Field field : clazz.getDeclaredFields()) {
+ try {
+ field.setAccessible(true);
+
+ ConfigDataString stringAnnotation = field.getAnnotation(ConfigDataString.class);
+ if (stringAnnotation != null) {
+
+ String defaultValue = stringAnnotation.defaultValue();
+ if (defaultValue.isEmpty()) {
+ try {
+ String v = (String) field.get(classToLoad);
+ defaultValue = v;
+ } catch (Exception e) {
+
+ }
+ }
+ String value = "";
+ if (!stringAnnotation.secondPath().isEmpty()) {
+ value = config.getString(stringAnnotation.path(),
+ config.getString(stringAnnotation.secondPath(), defaultValue));
+ } else {
+ value = config.getString(stringAnnotation.path(), defaultValue);
+ }
+
+ field.set(classToLoad, value);
+
+ }
+
+ ConfigDataBoolean booleanAnnotation = field.getAnnotation(ConfigDataBoolean.class);
+ if (booleanAnnotation != null) {
+ boolean defaultValue = booleanAnnotation.defaultValue();
+ if (!defaultValue) {
+ try {
+ boolean v = field.getBoolean(classToLoad);
+ defaultValue = v;
+ } catch (Exception e) {
+
+ }
+
+ }
+
+ boolean value = defaultValue;
+ if (config.contains(booleanAnnotation.path())) {
+ value = config.getBoolean(booleanAnnotation.path(), defaultValue);
+ } else if (!booleanAnnotation.secondPath().isEmpty()
+ && config.contains(booleanAnnotation.secondPath())) {
+ value = config.getBoolean(booleanAnnotation.secondPath(), defaultValue);
+
+ if (booleanAnnotation.secondPathInvert()) {
+ value = !value;
+ }
+ } else {
+ value = config.getBoolean(booleanAnnotation.path(), defaultValue);
+ }
+
+ field.set(classToLoad, value);
+ }
+
+ ConfigDataInt intAnnotation = field.getAnnotation(ConfigDataInt.class);
+ if (intAnnotation != null) {
+ int defaultValue = intAnnotation.defaultValue();
+ if (defaultValue == 0) {
+ try {
+ int v = field.getInt(classToLoad);
+ defaultValue = v;
+ } catch (Exception e) {
+
+ }
+ }
+ int value = 0;
+ if (!intAnnotation.secondPath().isEmpty()) {
+ value = config.getInt(intAnnotation.path(),
+ config.getInt(intAnnotation.secondPath(), defaultValue));
+ } else {
+ value = config.getInt(intAnnotation.path(), defaultValue);
+ }
+
+ field.set(classToLoad, value);
+ }
+
+ ConfigDataLong longAnnotation = field.getAnnotation(ConfigDataLong.class);
+ if (longAnnotation != null) {
+ long defaultValue = longAnnotation.defaultValue();
+ if (defaultValue == 0) {
+ try {
+ int v = field.getInt(classToLoad);
+ defaultValue = v;
+ } catch (Exception e) {
+
+ }
+ }
+ long value = 0;
+ if (!longAnnotation.secondPath().isEmpty()) {
+ value = config.getLong(longAnnotation.path(),
+ config.getLong(longAnnotation.secondPath(), defaultValue));
+ } else {
+ value = config.getLong(longAnnotation.path(), defaultValue);
+ }
+
+ field.set(classToLoad, value);
+ }
+
+ ConfigDataDouble doubleAnnotation = field.getAnnotation(ConfigDataDouble.class);
+ if (doubleAnnotation != null) {
+ double defaultValue = doubleAnnotation.defaultValue();
+ if (defaultValue == 0) {
+ try {
+ double v = field.getDouble(classToLoad);
+ defaultValue = v;
+ } catch (Exception e) {
+
+ }
+ }
+ double value = 0;
+ if (!doubleAnnotation.secondPath().isEmpty()) {
+ value = config.getDouble(doubleAnnotation.path(),
+ config.getDouble(doubleAnnotation.secondPath(), defaultValue));
+ } else {
+ value = config.getDouble(doubleAnnotation.path(), defaultValue);
+ }
+
+ field.set(classToLoad, value);
+ }
+
+ ConfigDataListString listAnnotation = field.getAnnotation(ConfigDataListString.class);
+ if (listAnnotation != null) {
+ ArrayList defaultValue = new ArrayList<>();
+ try {
+ ArrayList v = (ArrayList) field.get(classToLoad);
+ defaultValue = v;
+ } catch (Exception e) {
+
+ }
+
+ List list = config.getStringList(listAnnotation.path());
+
+ if (list.isEmpty()) {
+ list = config.getStringList(listAnnotation.secondPath());
+ }
+
+ ArrayList list1 = new ArrayList<>(list);
+ // use default value
+ if (list.isEmpty()) {
+ list1 = defaultValue;
+ }
+
+ field.set(classToLoad, list1);
+
+ /*
+ * ArrayList value = null; if (!listAnnotation.secondPath().isEmpty()) {
+ * value = (ArrayList) config.getList(listAnnotation.path(),
+ * config.getList(listAnnotation.secondPath(), defaultValue)); } else { value =
+ * (ArrayList) config.getList(listAnnotation.path(), defaultValue); }
+ *
+ * field.set(classToLoad, value);
+ */
+ }
+
+ ConfigDataListInt intListAnnotation = field.getAnnotation(ConfigDataListInt.class);
+ if (intListAnnotation != null) {
+ ArrayList defaultValue = new ArrayList<>();
+ try {
+ ArrayList v = (ArrayList) field.get(classToLoad);
+ defaultValue = v;
+ } catch (Exception e) {
+
+ }
+
+ List list = config.getIntegerList(intListAnnotation.path());
+
+ if (list.isEmpty()) {
+ list = config.getIntegerList(intListAnnotation.secondPath());
+ }
+
+ ArrayList list1 = new ArrayList<>(list);
+ // use default value
+ if (list.isEmpty()) {
+ list1 = defaultValue;
+ }
+
+ field.set(classToLoad, list1);
+
+ /*
+ * ArrayList value = null; if (!listAnnotation.secondPath().isEmpty()) {
+ * value = (ArrayList) config.getList(listAnnotation.path(),
+ * config.getList(listAnnotation.secondPath(), defaultValue)); } else { value =
+ * (ArrayList) config.getList(listAnnotation.path(), defaultValue); }
+ *
+ * field.set(classToLoad, value);
+ */
+ }
+
+ ConfigDataKeys setAnnotation = field.getAnnotation(ConfigDataKeys.class);
+ if (setAnnotation != null) {
+ Set value = new HashSet<>();
+ if (config.isConfigurationSection(setAnnotation.path())) {
+ value = config.getConfigurationSection(setAnnotation.path()).getKeys(false);
+ } else if (config.isConfigurationSection(setAnnotation.secondPath())
+ && setAnnotation.secondPath().length() > 0) {
+ value = config.getConfigurationSection(setAnnotation.secondPath()).getKeys(false);
+ }
+ if (value != null) {
+ field.set(classToLoad, value);
+ }
+ }
+
+ ConfigDataConfigurationSection confAnnotation = field
+ .getAnnotation(ConfigDataConfigurationSection.class);
+ if (confAnnotation != null) {
+ ConfigView value = null;
+ if (config.isConfigurationSection(confAnnotation.path())) {
+ value = config.getConfigurationSection(confAnnotation.path());
+ } else if (config.isConfigurationSection(confAnnotation.secondPath())
+ && !confAnnotation.secondPath().isEmpty()) {
+ value = config.getConfigurationSection(confAnnotation.secondPath());
+ }
+
+ field.set(classToLoad, value == null ? null : sectionValue.apply(value));
+ }
+
+ ConfigDataParsedDuration durationAnnotation = field.getAnnotation(ConfigDataParsedDuration.class);
+ if (durationAnnotation != null) {
+
+ String defaultValue = durationAnnotation.defaultValue();
+
+ if (defaultValue.isEmpty()) {
+ try {
+ Object v = field.get(classToLoad);
+ if (v != null) {
+ defaultValue = v.toString();
+ }
+ } catch (Exception e) {
+
+ }
+ }
+
+ String value = "";
+
+ if (!durationAnnotation.secondPath().isEmpty()) {
+ value = config.getString(durationAnnotation.path(),
+ config.getString(durationAnnotation.secondPath(), defaultValue));
+ } else {
+ value = config.getString(durationAnnotation.path(), defaultValue);
+ }
+
+ try {
+ // Assumes ParsedDuration has a constructor or static parse method
+ Object parsedDuration = ParsedDuration.parse(value, durationAnnotation.defaultTimeUnit());
+ field.set(classToLoad, parsedDuration);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+}
diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationHandler.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationHandler.java
index 66591d8f..551b7dd5 100644
--- a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationHandler.java
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/annotation/AnnotationHandler.java
@@ -1,283 +1,27 @@
-package com.bencodez.simpleapi.file.annotation;
-
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import org.bukkit.configuration.ConfigurationSection;
-
-import com.bencodez.simpleapi.time.ParsedDuration;
-
-public class AnnotationHandler {
-
- public AnnotationHandler() {
- }
-
- @SuppressWarnings("unchecked")
- public void load(ConfigurationSection config, Object classToLoad) {
- Class> clazz = classToLoad.getClass();
-
- for (Field field : clazz.getDeclaredFields()) {
- try {
- field.setAccessible(true);
-
- ConfigDataString stringAnnotation = field.getAnnotation(ConfigDataString.class);
- if (stringAnnotation != null) {
-
- String defaultValue = stringAnnotation.defaultValue();
- if (defaultValue.isEmpty()) {
- try {
- String v = (String) field.get(classToLoad);
- defaultValue = v;
- } catch (Exception e) {
-
- }
- }
- String value = "";
- if (!stringAnnotation.secondPath().isEmpty()) {
- value = config.getString(stringAnnotation.path(),
- config.getString(stringAnnotation.secondPath(), defaultValue));
- } else {
- value = config.getString(stringAnnotation.path(), defaultValue);
- }
-
- field.set(classToLoad, value);
-
- }
-
- ConfigDataBoolean booleanAnnotation = field.getAnnotation(ConfigDataBoolean.class);
- if (booleanAnnotation != null) {
- boolean defaultValue = booleanAnnotation.defaultValue();
- if (!defaultValue) {
- try {
- boolean v = field.getBoolean(classToLoad);
- defaultValue = v;
- } catch (Exception e) {
-
- }
-
- }
-
- boolean value = defaultValue;
- if (config.contains(booleanAnnotation.path())) {
- value = config.getBoolean(booleanAnnotation.path(), defaultValue);
- } else if (!booleanAnnotation.secondPath().isEmpty()
- && config.contains(booleanAnnotation.secondPath())) {
- value = config.getBoolean(booleanAnnotation.secondPath(), defaultValue);
-
- if (booleanAnnotation.secondPathInvert()) {
- value = !value;
- }
- } else {
- value = config.getBoolean(booleanAnnotation.path(), defaultValue);
- }
-
- field.set(classToLoad, value);
- }
-
- ConfigDataInt intAnnotation = field.getAnnotation(ConfigDataInt.class);
- if (intAnnotation != null) {
- int defaultValue = intAnnotation.defaultValue();
- if (defaultValue == 0) {
- try {
- int v = field.getInt(classToLoad);
- defaultValue = v;
- } catch (Exception e) {
-
- }
- }
- int value = 0;
- if (!intAnnotation.secondPath().isEmpty()) {
- value = config.getInt(intAnnotation.path(),
- config.getInt(intAnnotation.secondPath(), defaultValue));
- } else {
- value = config.getInt(intAnnotation.path(), defaultValue);
- }
-
- field.set(classToLoad, value);
- }
-
- ConfigDataLong longAnnotation = field.getAnnotation(ConfigDataLong.class);
- if (longAnnotation != null) {
- long defaultValue = longAnnotation.defaultValue();
- if (defaultValue == 0) {
- try {
- int v = field.getInt(classToLoad);
- defaultValue = v;
- } catch (Exception e) {
-
- }
- }
- long value = 0;
- if (!longAnnotation.secondPath().isEmpty()) {
- value = config.getLong(longAnnotation.path(),
- config.getLong(longAnnotation.secondPath(), defaultValue));
- } else {
- value = config.getLong(longAnnotation.path(), defaultValue);
- }
-
- field.set(classToLoad, value);
- }
-
- ConfigDataDouble doubleAnnotation = field.getAnnotation(ConfigDataDouble.class);
- if (doubleAnnotation != null) {
- double defaultValue = doubleAnnotation.defaultValue();
- if (defaultValue == 0) {
- try {
- double v = field.getDouble(classToLoad);
- defaultValue = v;
- } catch (Exception e) {
-
- }
- }
- double value = 0;
- if (!doubleAnnotation.secondPath().isEmpty()) {
- value = config.getDouble(doubleAnnotation.path(),
- config.getDouble(doubleAnnotation.secondPath(), defaultValue));
- } else {
- value = config.getDouble(doubleAnnotation.path(), defaultValue);
- }
-
- field.set(classToLoad, value);
- }
-
- ConfigDataListString listAnnotation = field.getAnnotation(ConfigDataListString.class);
- if (listAnnotation != null) {
- ArrayList defaultValue = new ArrayList<>();
- try {
- ArrayList v = (ArrayList) field.get(classToLoad);
- defaultValue = v;
- } catch (Exception e) {
-
- }
-
- List list = config.getStringList(listAnnotation.path());
-
- if (list.isEmpty()) {
- list = config.getStringList(listAnnotation.secondPath());
- }
-
- ArrayList list1 = new ArrayList<>(list);
- // use default value
- if (list.isEmpty()) {
- list1 = defaultValue;
- }
-
- field.set(classToLoad, list1);
-
- /*
- * ArrayList value = null; if (!listAnnotation.secondPath().isEmpty()) {
- * value = (ArrayList) config.getList(listAnnotation.path(),
- * config.getList(listAnnotation.secondPath(), defaultValue)); } else { value =
- * (ArrayList) config.getList(listAnnotation.path(), defaultValue); }
- *
- * field.set(classToLoad, value);
- */
- }
-
- ConfigDataListInt intListAnnotation = field.getAnnotation(ConfigDataListInt.class);
- if (intListAnnotation != null) {
- ArrayList defaultValue = new ArrayList<>();
- try {
- ArrayList v = (ArrayList) field.get(classToLoad);
- defaultValue = v;
- } catch (Exception e) {
-
- }
-
- List list = config.getIntegerList(intListAnnotation.path());
-
- if (list.isEmpty()) {
- list = config.getIntegerList(intListAnnotation.secondPath());
- }
-
- ArrayList list1 = new ArrayList<>(list);
- // use default value
- if (list.isEmpty()) {
- list1 = defaultValue;
- }
-
- field.set(classToLoad, list1);
-
- /*
- * ArrayList value = null; if (!listAnnotation.secondPath().isEmpty()) {
- * value = (ArrayList) config.getList(listAnnotation.path(),
- * config.getList(listAnnotation.secondPath(), defaultValue)); } else { value =
- * (ArrayList) config.getList(listAnnotation.path(), defaultValue); }
- *
- * field.set(classToLoad, value);
- */
- }
-
- ConfigDataKeys setAnnotation = field.getAnnotation(ConfigDataKeys.class);
- if (setAnnotation != null) {
- Set value = new HashSet<>();
- if (config.isConfigurationSection(setAnnotation.path())) {
- value = config.getConfigurationSection(setAnnotation.path()).getKeys(false);
- } else if (config.isConfigurationSection(setAnnotation.secondPath())
- && setAnnotation.secondPath().length() > 0) {
- value = config.getConfigurationSection(setAnnotation.secondPath()).getKeys(false);
- }
- if (value != null) {
- field.set(classToLoad, value);
- }
- }
-
- ConfigDataConfigurationSection confAnnotation = field
- .getAnnotation(ConfigDataConfigurationSection.class);
- if (confAnnotation != null) {
- ConfigurationSection value = null;
- if (config.isConfigurationSection(confAnnotation.path())) {
- value = config.getConfigurationSection(confAnnotation.path());
- } else if (config.isConfigurationSection(confAnnotation.secondPath())
- && !confAnnotation.secondPath().isEmpty()) {
- value = config.getConfigurationSection(confAnnotation.secondPath());
- }
-
- field.set(classToLoad, value);
- }
-
- ConfigDataParsedDuration durationAnnotation = field.getAnnotation(ConfigDataParsedDuration.class);
- if (durationAnnotation != null) {
-
- String defaultValue = durationAnnotation.defaultValue();
-
- if (defaultValue.isEmpty()) {
- try {
- Object v = field.get(classToLoad);
- if (v != null) {
- defaultValue = v.toString();
- }
- } catch (Exception e) {
-
- }
- }
-
- String value = "";
-
- if (!durationAnnotation.secondPath().isEmpty()) {
- value = config.getString(durationAnnotation.path(),
- config.getString(durationAnnotation.secondPath(), defaultValue));
- } else {
- value = config.getString(durationAnnotation.path(), defaultValue);
- }
-
- try {
- // Assumes ParsedDuration has a constructor or static parse method
- Object parsedDuration = ParsedDuration.parse(value, durationAnnotation.defaultTimeUnit());
- field.set(classToLoad, parsedDuration);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- }
-
-}
+package com.bencodez.simpleapi.file.annotation;
+
+import org.bukkit.configuration.ConfigurationSection;
+
+import com.bencodez.simpleapi.file.config.bukkit.BukkitConfigView;
+
+/** Bukkit-compatible entry point for the shared annotation binder. */
+public class AnnotationHandler {
+
+ private final AnnotationBinder binder = new AnnotationBinder(
+ view -> ((BukkitConfigView) view).getSection());
+
+ public AnnotationHandler() {
+ }
+
+ /**
+ * Loads the existing annotations without changing their Bukkit behavior.
+ * This signature remains unchanged; a ConfigView overload is deliberately
+ * not added, so existing calls such as load(null, target) stay unambiguous.
+ *
+ * @param config Bukkit configuration (legacy null handling is preserved)
+ * @param classToLoad object whose declared fields should be populated
+ */
+ public void load(ConfigurationSection config, Object classToLoad) {
+ binder.load(config == null ? null : new BukkitConfigView(config), classToLoad);
+ }
+}
diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/ConfigView.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/ConfigView.java
new file mode 100644
index 00000000..9e512a75
--- /dev/null
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/ConfigView.java
@@ -0,0 +1,42 @@
+package com.bencodez.simpleapi.file.config;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Read-only access used by the annotation binder, independent of a server API.
+ *
+ * Implementations own path traversal, configured defaults and scalar/list
+ * conversion semantics. A platform adapter must delegate these operations rather
+ * than round-tripping a live configuration through a different parser. This
+ * interface does not imply a snapshot, thread safety, or permission to mutate the
+ * underlying configuration.
+ *
+ * List getters return an empty list when no compatible list exists. Section
+ * lookup returns {@code null} when absent. Paths and the {@code deep} key flag
+ * follow the backing configuration's rules.
+ */
+public interface ConfigView {
+
+ boolean contains(String path);
+
+ String getString(String path, String defaultValue);
+
+ boolean getBoolean(String path, boolean defaultValue);
+
+ int getInt(String path, int defaultValue);
+
+ long getLong(String path, long defaultValue);
+
+ double getDouble(String path, double defaultValue);
+
+ List getStringList(String path);
+
+ List getIntegerList(String path);
+
+ boolean isConfigurationSection(String path);
+
+ ConfigView getConfigurationSection(String path);
+
+ Set getKeys(boolean deep);
+}
diff --git a/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/bukkit/BukkitConfigView.java b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/bukkit/BukkitConfigView.java
new file mode 100644
index 00000000..0990bd23
--- /dev/null
+++ b/SimpleAPI/src/main/java/com/bencodez/simpleapi/file/config/bukkit/BukkitConfigView.java
@@ -0,0 +1,89 @@
+package com.bencodez.simpleapi.file.config.bukkit;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import org.bukkit.configuration.ConfigurationSection;
+
+import com.bencodez.simpleapi.file.config.ConfigView;
+
+/**
+ * Live, read-only facade over a Bukkit configuration section. Reads deliberately
+ * preserve the backing section's defaults, coercions, key order and path options.
+ */
+public final class BukkitConfigView implements ConfigView {
+
+ private final ConfigurationSection section;
+
+ public BukkitConfigView(ConfigurationSection section) {
+ this.section = Objects.requireNonNull(section, "section");
+ }
+
+ /**
+ * Returns the original section for legacy Bukkit-typed annotated fields.
+ * No copy is made: existing callers retain section identity and mutability.
+ * This method belongs to the Bukkit adapter, not the platform-neutral API.
+ *
+ * @return the backing Bukkit section
+ */
+ public ConfigurationSection getSection() {
+ return section;
+ }
+
+ @Override
+ public boolean contains(String path) {
+ return section.contains(path);
+ }
+
+ @Override
+ public String getString(String path, String defaultValue) {
+ return section.getString(path, defaultValue);
+ }
+
+ @Override
+ public boolean getBoolean(String path, boolean defaultValue) {
+ return section.getBoolean(path, defaultValue);
+ }
+
+ @Override
+ public int getInt(String path, int defaultValue) {
+ return section.getInt(path, defaultValue);
+ }
+
+ @Override
+ public long getLong(String path, long defaultValue) {
+ return section.getLong(path, defaultValue);
+ }
+
+ @Override
+ public double getDouble(String path, double defaultValue) {
+ return section.getDouble(path, defaultValue);
+ }
+
+ @Override
+ public List getStringList(String path) {
+ return section.getStringList(path);
+ }
+
+ @Override
+ public List getIntegerList(String path) {
+ return section.getIntegerList(path);
+ }
+
+ @Override
+ public boolean isConfigurationSection(String path) {
+ return section.isConfigurationSection(path);
+ }
+
+ @Override
+ public BukkitConfigView getConfigurationSection(String path) {
+ ConfigurationSection child = section.getConfigurationSection(path);
+ return child == null ? null : new BukkitConfigView(child);
+ }
+
+ @Override
+ public Set getKeys(boolean deep) {
+ return section.getKeys(deep);
+ }
+}
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/AnnotationBinderHeadlessTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/AnnotationBinderHeadlessTest.java
new file mode 100644
index 00000000..a3c6d377
--- /dev/null
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/AnnotationBinderHeadlessTest.java
@@ -0,0 +1,30 @@
+package com.bencodez.simpleapi.tests.file.config;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+
+import org.junit.jupiter.api.Test;
+
+import com.bencodez.simpleapi.file.annotation.AnnotationBinder;
+
+class AnnotationBinderHeadlessTest {
+
+ @Test
+ void bindsUsingOnlyProjectClassesAndTheJdk() throws Exception {
+ URL mainClasses = AnnotationBinder.class.getProtectionDomain().getCodeSource().getLocation();
+ URL testClasses = getClass().getProtectionDomain().getCodeSource().getLocation();
+ // The platform parent supplies only JDK classes, not Maven's dependency classpath.
+ try (URLClassLoader isolated = new URLClassLoader(new URL[] { mainClasses, testClasses },
+ ClassLoader.getPlatformClassLoader())) {
+ assertThrows(ClassNotFoundException.class,
+ () -> isolated.loadClass("org.bukkit.configuration.ConfigurationSection"));
+ assertThrows(ClassNotFoundException.class,
+ () -> isolated.loadClass("org.junit.jupiter.api.Test"));
+ Class> fixture = isolated.loadClass(HeadlessBindingFixture.class.getName());
+ assertSame(isolated, fixture.getClassLoader());
+ fixture.getMethod("run").invoke(null);
+ }
+ }
+}
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/AnnotationHandlerCompatibilityTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/AnnotationHandlerCompatibilityTest.java
new file mode 100644
index 00000000..8d805aac
--- /dev/null
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/AnnotationHandlerCompatibilityTest.java
@@ -0,0 +1,315 @@
+package com.bencodez.simpleapi.tests.file.config;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.MemoryConfiguration;
+import org.junit.jupiter.api.Test;
+
+import com.bencodez.simpleapi.file.annotation.*;
+import com.bencodez.simpleapi.file.config.bukkit.BukkitConfigView;
+import com.bencodez.simpleapi.time.ParsedDuration;
+
+class AnnotationHandlerCompatibilityTest {
+
+ private final AnnotationHandler handler = new AnnotationHandler();
+
+ static class Scalars {
+ @ConfigDataString(path = "string", secondPath = "old.string")
+ private String string = "initial";
+ @ConfigDataBoolean(path = "bool", secondPath = "old.bool", secondPathInvert = true)
+ private boolean bool = true;
+ @ConfigDataInt(path = "integer", secondPath = "old.integer")
+ private int integer = 7;
+ @ConfigDataLong(path = "long", secondPath = "old.long")
+ private long number = 99L;
+ @ConfigDataDouble(path = "double", secondPath = "old.double")
+ private double decimal = 1.5;
+ }
+
+ @Test
+ void preservesPublicConstructorAndUnambiguousLegacySignature() throws Exception {
+ assertNotNull(AnnotationHandler.class.getConstructor().newInstance());
+ Method load = AnnotationHandler.class.getMethod("load", ConfigurationSection.class, Object.class);
+ assertEquals(void.class, load.getReturnType());
+ assertEquals(1L, Arrays.stream(AnnotationHandler.class.getMethods())
+ .filter(method -> method.getName().equals("load")).count());
+ // Compile-time regression guard: a ConfigView overload would make this ambiguous.
+ assertDoesNotThrow(() -> handler.load(null, new Object()));
+ }
+
+ @Test
+ void preservesInitializedDefaultsIncludingHistoricalLongBehavior() {
+ Scalars target = new Scalars();
+ handler.load(new MemoryConfiguration(), target);
+ assertEquals("initial", target.string);
+ assertTrue(target.bool);
+ assertEquals(7, target.integer);
+ // Existing Field.getInt(longField) fails and leaves the annotation default at zero.
+ // Fixing this would be a separate behavior change, not part of the extraction.
+ assertEquals(0L, target.number);
+ assertEquals(1.5, target.decimal);
+ }
+
+ @Test
+ void honorsExplicitZeroFalseAndEmptyPrimaryValues() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ config.set("string", "");
+ config.set("bool", false);
+ config.set("integer", 0);
+ config.set("long", 0L);
+ config.set("double", 0.0);
+ config.set("old.string", "fallback");
+ config.set("old.bool", false);
+ config.set("old.integer", 100);
+ Scalars target = new Scalars();
+ handler.load(config, target);
+ assertEquals("", target.string);
+ assertFalse(target.bool);
+ assertEquals(0, target.integer);
+ assertEquals(0L, target.number);
+ assertEquals(0.0, target.decimal);
+ }
+
+ @Test
+ void usesAlternatePathsAndInvertsOnlyTheAlternateBoolean() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ config.set("old.string", "alternate");
+ config.set("old.bool", true);
+ config.set("old.integer", 12);
+ config.set("old.long", 4_000_000_000L);
+ config.set("old.double", 2.25);
+ Scalars target = new Scalars();
+ handler.load(config, target);
+ assertEquals("alternate", target.string);
+ assertFalse(target.bool);
+ assertEquals(12, target.integer);
+ assertEquals(4_000_000_000L, target.number);
+ assertEquals(2.25, target.decimal);
+ config.set("bool", true);
+ handler.load(config, target);
+ assertTrue(target.bool);
+ }
+
+ @Test
+ void retainsBukkitTypeCoercionAndPresentButInvalidBooleanPrecedence() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ config.set("string", 42);
+ config.set("bool", "not-a-boolean");
+ config.set("old.bool", true);
+ config.set("integer", "invalid");
+ config.set("old.integer", 18);
+ config.set("long", 123);
+ config.set("double", 4);
+ Scalars target = new Scalars();
+ handler.load(config, target);
+ assertEquals(config.getString("string", "initial"), target.string);
+ assertTrue(target.bool); // contains(primary) wins; no alternate inversion.
+ assertEquals(18, target.integer);
+ assertEquals(123L, target.number);
+ assertEquals(4.0, target.decimal);
+ }
+
+ static class ExplicitDefaults {
+ @ConfigDataString(path = "missing", defaultValue = "annotation")
+ String text = "field";
+ @ConfigDataInt(path = "missing", defaultValue = 12)
+ int integer = 7;
+ @ConfigDataLong(path = "missing", defaultValue = 123L)
+ long number = 99L;
+ @ConfigDataDouble(path = "missing", defaultValue = 2.5)
+ double decimal = 1.5;
+ @ConfigDataBoolean(path = "missing", defaultValue = true)
+ boolean bool;
+ }
+
+ @Test
+ void preservesExplicitAnnotationDefaults() {
+ ExplicitDefaults target = new ExplicitDefaults();
+ handler.load(new MemoryConfiguration(), target);
+ assertEquals("annotation", target.text);
+ assertEquals(12, target.integer);
+ assertEquals(123L, target.number);
+ assertEquals(2.5, target.decimal);
+ assertTrue(target.bool);
+ }
+
+ static class Lists {
+ @ConfigDataListString(path = "strings", secondPath = "old.strings")
+ ArrayList strings = new ArrayList<>(List.of("initial"));
+ @ConfigDataListInt(path = "integers", secondPath = "old.integers")
+ ArrayList integers = new ArrayList<>(List.of(7));
+ }
+
+ @Test
+ void preservesEmptyListFallbackAndInitializedListIdentity() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ config.set("strings", List.of());
+ config.set("integers", List.of());
+ Lists target = new Lists();
+ ArrayList strings = target.strings;
+ ArrayList integers = target.integers;
+ handler.load(config, target);
+ assertSame(strings, target.strings);
+ assertSame(integers, target.integers);
+ config.set("old.strings", List.of("alternate"));
+ config.set("old.integers", List.of(11));
+ handler.load(config, target);
+ assertEquals(List.of("alternate"), target.strings);
+ assertEquals(List.of(11), target.integers);
+ assertNotSame(strings, target.strings);
+ assertNotSame(integers, target.integers);
+ }
+
+ @Test
+ void delegatesMixedListConversionToBukkit() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ config.set("strings", Arrays.asList("one", 2, true, null, List.of("ignored")));
+ config.set("integers", Arrays.asList(1, "2", 3.5, 'A', false, null, "bad"));
+ Lists target = new Lists();
+ handler.load(config, target);
+ assertEquals(config.getStringList("strings"), target.strings);
+ assertEquals(config.getIntegerList("integers"), target.integers);
+ }
+
+ static class Sections {
+ @ConfigDataConfigurationSection(path = "rewards", secondPath = "old.rewards")
+ ConfigurationSection section;
+ @ConfigDataConfigurationSection(path = "rewards", secondPath = "old.rewards")
+ Object object;
+ @ConfigDataKeys(path = "rewards", secondPath = "old.rewards")
+ Set keys = Set.of("initial");
+ }
+
+ @Test
+ void keepsNativeSectionIdentityForTypedAndObjectFields() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ ConfigurationSection section = config.createSection("rewards");
+ section.set("first.command", "say first");
+ section.set("second", "say second");
+ Sections target = new Sections();
+ handler.load(config, target);
+ assertSame(section, target.section);
+ assertSame(section, target.object);
+ assertEquals(new ArrayList<>(section.getKeys(false)), new ArrayList<>(target.keys));
+ target.section.set("edited", true);
+ assertTrue(config.getBoolean("rewards.edited"));
+ }
+
+ @Test
+ void supportsAlternateSectionsAndClearsAbsentSectionsAndKeys() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ ConfigurationSection section = config.createSection("old.rewards");
+ section.set("command", "say alternate");
+ Sections target = new Sections();
+ handler.load(config, target);
+ assertSame(section, target.section);
+ assertSame(section, target.object);
+ assertEquals(Set.of("command"), target.keys);
+ config.set("old.rewards", null);
+ handler.load(config, target);
+ assertNull(target.section);
+ assertNull(target.object);
+ assertTrue(target.keys.isEmpty());
+ }
+
+ static class RootSection {
+ @ConfigDataConfigurationSection(path = "")
+ Object root;
+ }
+
+ @Test
+ void retainsRootSectionIdentity() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ RootSection target = new RootSection();
+ handler.load(config, target);
+ assertSame(config, target.root);
+ }
+
+ @Test
+ void retainsConfiguredDefaultsAndPathSeparatorThroughTheLiveAdapter() {
+ MemoryConfiguration defaults = new MemoryConfiguration();
+ defaults.set("bool", false);
+ MemoryConfiguration config = new MemoryConfiguration(defaults);
+ config.options().pathSeparator('/');
+ config.set("nested/value", 25);
+ BukkitConfigView view = new BukkitConfigView(config);
+ assertEquals(config.contains("bool"), view.contains("bool"));
+ assertEquals(config.getBoolean("bool", true), view.getBoolean("bool", true));
+ assertEquals(25, view.getInt("nested/value", 0));
+ assertEquals(config.getKeys(true), view.getKeys(true));
+ config.set("nested/value", 26);
+ assertEquals(26, view.getInt("nested/value", 0));
+ assertSame(config.getConfigurationSection("nested"), view.getConfigurationSection("nested").getSection());
+ assertNull(view.getConfigurationSection("absent"));
+ }
+
+ static class Parent {
+ @ConfigDataInt(path = "value")
+ int inherited = 3;
+ }
+
+ static class Child extends Parent {
+ @ConfigDataInt(path = "value")
+ private int declared = 4;
+ }
+
+ @Test
+ void retainsDeclaredFieldsOnlyTraversal() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ config.set("value", 20);
+ Child target = new Child();
+ handler.load(config, target);
+ assertEquals(3, target.inherited);
+ assertEquals(20, target.declared);
+ }
+
+ static class InvalidField {
+ @ConfigDataString(path = "text")
+ @ConfigDataInt(path = "number")
+ int invalid = 5;
+ @ConfigDataInt(path = "number")
+ int valid;
+ }
+
+ @Test
+ void isolatesFieldErrorsWithoutChangingAnnotationOrder() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ config.set("text", "not an integer");
+ config.set("number", 10);
+ InvalidField target = new InvalidField();
+ assertDoesNotThrow(() -> handler.load(config, target));
+ assertEquals(5, target.invalid); // String runs first; the field's remaining annotations are skipped.
+ assertEquals(10, target.valid);
+ }
+
+ static class Durations {
+ @ConfigDataParsedDuration(path = "delay", secondPath = "old.delay", defaultTimeUnit = TimeUnit.SECONDS)
+ ParsedDuration duration;
+ @ConfigDataParsedDuration(path = "missing", defaultValue = "2s", defaultTimeUnit = TimeUnit.SECONDS)
+ ParsedDuration fallback;
+ }
+
+ @Test
+ void keepsDurationParsingAndAlternatePaths() {
+ MemoryConfiguration config = new MemoryConfiguration();
+ config.set("old.delay", "3");
+ Durations target = new Durations();
+ handler.load(config, target);
+ assertEquals(3_000L, target.duration.getMillis());
+ assertEquals(2_000L, target.fallback.getMillis());
+ }
+
+ @Test
+ void retainsNullTargetFailure() {
+ assertThrows(NullPointerException.class, () -> handler.load(new MemoryConfiguration(), null));
+ assertThrows(NullPointerException.class, () -> handler.load(null, null));
+ }
+}
diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/HeadlessBindingFixture.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/HeadlessBindingFixture.java
new file mode 100644
index 00000000..38f5a963
--- /dev/null
+++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/HeadlessBindingFixture.java
@@ -0,0 +1,118 @@
+package com.bencodez.simpleapi.tests.file.config;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import com.bencodez.simpleapi.file.annotation.AnnotationBinder;
+import com.bencodez.simpleapi.file.annotation.ConfigDataConfigurationSection;
+import com.bencodez.simpleapi.file.annotation.ConfigDataInt;
+import com.bencodez.simpleapi.file.annotation.ConfigDataKeys;
+import com.bencodez.simpleapi.file.annotation.ConfigDataParsedDuration;
+import com.bencodez.simpleapi.file.annotation.ConfigDataString;
+import com.bencodez.simpleapi.file.config.ConfigView;
+import com.bencodez.simpleapi.time.ParsedDuration;
+
+/** Executed in a separate class loader that has no Bukkit or test libraries. */
+public final class HeadlessBindingFixture {
+
+ private HeadlessBindingFixture() {
+ }
+
+ public static final class Values {
+ @ConfigDataString(path = "message", secondPath = "legacy-message")
+ private String message;
+ @ConfigDataInt(path = "votes")
+ private int votes;
+ @ConfigDataConfigurationSection(path = "rewards")
+ private ConfigView rewards;
+ @ConfigDataKeys(path = "rewards")
+ private Set keys;
+ @ConfigDataParsedDuration(path = "delay", defaultTimeUnit = TimeUnit.SECONDS)
+ private ParsedDuration delay;
+ }
+
+ public static void run() {
+ ConfigView rewards = new TestView(Map.of("command", "say voted"));
+ ConfigView config = new TestView(Map.of("legacy-message", "thanks", "votes", 12,
+ "rewards", rewards, "delay", "3"));
+ Values values = new Values();
+ new AnnotationBinder().load(config, values);
+ if (!"thanks".equals(values.message) || values.votes != 12 || values.rewards != rewards
+ || !Set.of("command").equals(values.keys) || values.delay == null
+ || values.delay.getMillis() != 3_000L) {
+ throw new AssertionError("Headless annotation binding failed");
+ }
+ }
+
+ // A deliberately small in-memory test implementation, not a new YAML parser.
+ private static final class TestView implements ConfigView {
+ private final Map values;
+
+ private TestView(Map values) {
+ this.values = values;
+ }
+
+ @Override
+ public boolean contains(String path) {
+ return values.containsKey(path);
+ }
+
+ @Override
+ public String getString(String path, String fallback) {
+ Object value = values.get(path);
+ return value == null ? fallback : value.toString();
+ }
+
+ @Override
+ public boolean getBoolean(String path, boolean fallback) {
+ Object value = values.get(path);
+ return value instanceof Boolean ? (Boolean) value : fallback;
+ }
+
+ @Override
+ public int getInt(String path, int fallback) {
+ Object value = values.get(path);
+ return value instanceof Number ? ((Number) value).intValue() : fallback;
+ }
+
+ @Override
+ public long getLong(String path, long fallback) {
+ Object value = values.get(path);
+ return value instanceof Number ? ((Number) value).longValue() : fallback;
+ }
+
+ @Override
+ public double getDouble(String path, double fallback) {
+ Object value = values.get(path);
+ return value instanceof Number ? ((Number) value).doubleValue() : fallback;
+ }
+
+ @Override
+ public List getStringList(String path) {
+ return List.of();
+ }
+
+ @Override
+ public List getIntegerList(String path) {
+ return List.of();
+ }
+
+ @Override
+ public boolean isConfigurationSection(String path) {
+ return values.get(path) instanceof ConfigView;
+ }
+
+ @Override
+ public ConfigView getConfigurationSection(String path) {
+ Object value = values.get(path);
+ return value instanceof ConfigView ? (ConfigView) value : null;
+ }
+
+ @Override
+ public Set getKeys(boolean deep) {
+ return values.keySet();
+ }
+ }
+}
diff --git a/docs/platform-neutral-configuration.md b/docs/platform-neutral-configuration.md
new file mode 100644
index 00000000..f6800d26
--- /dev/null
+++ b/docs/platform-neutral-configuration.md
@@ -0,0 +1,81 @@
+# Platform-neutral configuration foundation
+
+This is the first compatibility-preserving step toward reusing SimpleAPI's
+configuration annotations outside Bukkit. It is not a Forge/Fabric implementation
+or a separately published `simpleapi-core` artifact yet. The existing Maven
+coordinates, shaded JAR, build command, configuration files and dependencies are
+unchanged. Do not put the entire current SimpleAPI JAR on a mod server and assume
+that its other classes are platform-neutral.
+
+## Existing Bukkit callers
+
+Keep using the existing API without changes:
+
+```java
+new AnnotationHandler().load(configurationSection, target);
+```
+
+`AnnotationHandler` delegates to `AnnotationBinder` through `BukkitConfigView`.
+The adapter forwards reads to the original section rather than serializing,
+copying or reparsing it. `@ConfigDataConfigurationSection` continues to assign the
+original Bukkit section, including when the annotated field is typed as `Object`.
+Section identity, edits through that section, Bukkit defaults and path options
+are preserved. There is no new `load` overload on `AnnotationHandler`, so existing
+`load(null, target)` call sites are still unambiguous.
+
+## New shared callers
+
+A platform supplies the small `ConfigView` read contract and uses:
+
+```java
+new AnnotationBinder().load(view, target);
+```
+
+The no-argument binder assigns `ConfigView` instances to section-annotated fields;
+shared models should declare those fields as `ConfigView`. The optional section
+projection constructor exists for compatibility adapters, not for leaking native
+server objects into new shared models. It is called only for present sections.
+
+The binder uses the existing annotation types and `ParsedDuration`. Its execution
+path has no Bukkit, proxy, mod-loader or Minecraft dependency. The headless test
+loads it with only project classes and the JDK and verifies that Bukkit cannot be
+loaded. An in-memory test view is included; production Configurate/YAML adapters
+and physical module extraction are separate follow-ups.
+
+## Behavior deliberately preserved
+
+This change moves the existing binding body rather than redesigning its rules:
+
+- Only declared fields are processed, in the existing annotation-check order.
+- Existing field/annotation defaults, alternate paths and boolean inversion apply.
+- Empty lists fall back to alternate paths and then the initialized list object.
+- Missing sections become null; missing key sets become empty.
+- Per-field exceptions are printed and isolated as before.
+- The historical zero-default `ConfigDataLong` reflection behavior is retained:
+ a long field initializer is not recovered through `Field.getInt`. Changing it
+ would alter existing configurations and is intentionally outside this PR.
+
+`ConfigView` is read-only access, not a promise that its backing data is immutable
+or safe to read from arbitrary threads. Each platform must preserve its existing
+configuration ownership and thread rules.
+
+## Verification
+
+Run the existing full build:
+
+```sh
+mvn -B -f SimpleAPI/pom.xml package
+```
+
+Focused regression tests:
+
+```sh
+mvn -B -f SimpleAPI/pom.xml \
+ -Dtest=AnnotationHandlerCompatibilityTest,AnnotationBinderHeadlessTest test
+```
+
+The compatibility tests exercise the real Bukkit configuration API. The headless
+fixture deliberately does not depend on JUnit or Bukkit within its isolated
+class loader. Before merging, also build AdvancedCore and VotingPlugin against
+the locally installed candidate SimpleAPI artifact; no consumer changes should
+be required.