diff --git a/.gitignore b/.gitignore
index 5d19ae2..8a57975 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.idea/
target/
-ConfigurationMaster.iml
\ No newline at end of file
+ConfigurationMaster.iml
+*.yml
\ No newline at end of file
diff --git a/API/pom.xml b/API/pom.xml
new file mode 100644
index 0000000..67594b8
--- /dev/null
+++ b/API/pom.xml
@@ -0,0 +1,30 @@
+
+
+
+ ConfigurationMaster
+ com.github.thatsmusic99
+ v2.0.0-rc.3
+
+ 4.0.0
+
+ ConfigurationMaster-API
+ v2.0.0-rc.3
+
+
+ 8
+ 8
+
+
+
+
+
+ org.yaml
+ snakeyaml
+ 2.0
+ provided
+
+
+
+
\ No newline at end of file
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/Example.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/Example.java
new file mode 100644
index 0000000..485743d
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/Example.java
@@ -0,0 +1,13 @@
+package io.github.thatsmusic99.configurationmaster.annotations;
+
+import java.lang.annotation.*;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(Examples.class)
+public @interface Example {
+
+ String key();
+
+ String value();
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/Examples.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/Examples.java
new file mode 100644
index 0000000..c923a8c
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/Examples.java
@@ -0,0 +1,12 @@
+package io.github.thatsmusic99.configurationmaster.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Examples {
+ Example[] value();
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/Option.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/Option.java
new file mode 100644
index 0000000..edd4092
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/Option.java
@@ -0,0 +1,24 @@
+package io.github.thatsmusic99.configurationmaster.annotations;
+
+
+import io.github.thatsmusic99.configurationmaster.annotations.handlers.DefaultOptionHandler;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Option {
+
+ String path() default "";
+
+ String comment() default "";
+
+ String section() default "";
+
+ boolean lenient() default false;
+
+ Class extends OptionHandler> optionHandler() default DefaultOptionHandler.class;
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/OptionHandler.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/OptionHandler.java
new file mode 100644
index 0000000..cbf2c13
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/OptionHandler.java
@@ -0,0 +1,27 @@
+package io.github.thatsmusic99.configurationmaster.annotations;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public interface OptionHandler {
+
+ Object get(
+ @NotNull ConfigFile file,
+ @NotNull String name
+ );
+
+ void set(
+ @NotNull ConfigFile file,
+ @NotNull String name,
+ @NotNull Object value
+ );
+
+ void addDefault(
+ @NotNull ConfigFile file,
+ @NotNull String name,
+ @NotNull Object value,
+ @Nullable String section,
+ @Nullable String comment
+ );
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/BooleanOptionHandler.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/BooleanOptionHandler.java
new file mode 100644
index 0000000..6d4374b
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/BooleanOptionHandler.java
@@ -0,0 +1,12 @@
+package io.github.thatsmusic99.configurationmaster.annotations.handlers;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import org.jetbrains.annotations.NotNull;
+
+public class BooleanOptionHandler extends DefaultOptionHandler {
+
+ @Override
+ public Object get(@NotNull ConfigFile file, @NotNull String name) {
+ return file.getBoolean(name);
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/DefaultOptionHandler.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/DefaultOptionHandler.java
new file mode 100644
index 0000000..4e5b36c
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/DefaultOptionHandler.java
@@ -0,0 +1,25 @@
+package io.github.thatsmusic99.configurationmaster.annotations.handlers;
+
+import io.github.thatsmusic99.configurationmaster.annotations.OptionHandler;
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class DefaultOptionHandler implements OptionHandler {
+
+
+ @Override
+ public Object get(@NotNull ConfigFile file, @NotNull String name) {
+ return file.get(name);
+ }
+
+ @Override
+ public void set(@NotNull ConfigFile file, @NotNull String name, @NotNull Object value) {
+ file.set(name, value);
+ }
+
+ @Override
+ public void addDefault(@NotNull ConfigFile file, @NotNull String name, @NotNull Object value, @Nullable String section, @Nullable String comment) {
+ file.addDefault(name, value, section, comment);
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/FloatOptionHandler.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/FloatOptionHandler.java
new file mode 100644
index 0000000..132d372
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/FloatOptionHandler.java
@@ -0,0 +1,12 @@
+package io.github.thatsmusic99.configurationmaster.annotations.handlers;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import org.jetbrains.annotations.NotNull;
+
+public class FloatOptionHandler extends DefaultOptionHandler {
+
+ @Override
+ public Object get(@NotNull ConfigFile file, @NotNull String name) {
+ return file.getFloat(name);
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/IntegerOptionHandler.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/IntegerOptionHandler.java
new file mode 100644
index 0000000..d2a6b7a
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/IntegerOptionHandler.java
@@ -0,0 +1,12 @@
+package io.github.thatsmusic99.configurationmaster.annotations.handlers;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import org.jetbrains.annotations.NotNull;
+
+public class IntegerOptionHandler extends DefaultOptionHandler {
+
+ @Override
+ public Object get(@NotNull ConfigFile file, @NotNull String name) {
+ return file.getInteger(name);
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/LongOptionHandler.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/LongOptionHandler.java
new file mode 100644
index 0000000..8e1d165
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/LongOptionHandler.java
@@ -0,0 +1,12 @@
+package io.github.thatsmusic99.configurationmaster.annotations.handlers;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import org.jetbrains.annotations.NotNull;
+
+public class LongOptionHandler extends DefaultOptionHandler {
+
+ @Override
+ public Object get(@NotNull ConfigFile file, @NotNull String name) {
+ return file.getLong(name);
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/StringOptionHandler.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/StringOptionHandler.java
new file mode 100644
index 0000000..a904736
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/annotations/handlers/StringOptionHandler.java
@@ -0,0 +1,12 @@
+package io.github.thatsmusic99.configurationmaster.annotations.handlers;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import org.jetbrains.annotations.NotNull;
+
+public class StringOptionHandler extends DefaultOptionHandler {
+
+ @Override
+ public Object get(@NotNull ConfigFile file, @NotNull String name) {
+ return file.getString(name);
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/CommentWriter.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/CommentWriter.java
new file mode 100644
index 0000000..787dbbd
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/CommentWriter.java
@@ -0,0 +1,163 @@
+package io.github.thatsmusic99.configurationmaster.api;
+
+import io.github.thatsmusic99.configurationmaster.api.comments.Comment;
+import io.github.thatsmusic99.configurationmaster.api.comments.Section;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class CommentWriter {
+
+ private final ConfigFile config;
+ // The currently written lines of the file.
+ private List currentLines;
+
+ protected CommentWriter(ConfigFile config) {
+ this.config = config;
+ this.currentLines = new ArrayList<>();
+ }
+
+ /**
+ * Initiates the comment writing process.
+ */
+ protected void writeComments(List currentLines) {
+ this.currentLines = currentLines;
+
+ // For each comment to be made...
+ for (String path : config.getComments().keySet()) {
+ // Write the comment at the specified path
+ writeComment(path, path.split("\\."), 0, 0);
+ }
+
+ // However, if there's any comments left, write them in.
+ for (Comment comment : config.getPendingComments()) {
+
+ String str = comment.getComment();
+ if (str.isEmpty()) {
+ currentLines.add("");
+ continue;
+ }
+ currentLines.add("");
+ String[] rawComment = str.split("\n");
+ for (String commentPart : rawComment) {
+ if (commentPart.isEmpty()) {
+ currentLines.add("");
+ continue;
+ }
+
+ // If we have a comment section here, use that instead
+ if (comment instanceof Section) {
+ String section = commentPart.split(": ")[1];
+ StringBuilder length = new StringBuilder();
+ length.append("###");
+ for (int j = 0; j < section.length(); j++) {
+ length.append("#");
+ }
+ length.append("###");
+ currentLines.add(length.toString());
+ currentLines.add("# " + section + " #");
+ currentLines.add(length.toString());
+ } else {
+ currentLines.add("# " + commentPart);
+ }
+ }
+ }
+ }
+
+ /**
+ * Method used to write a specified comment.
+ *
+ * @param path The path the comment must be written at.
+ * @param divisions The number of sections the part can be split up into.
+ * @param iteration How far we're down the pathway (in terms of different sections).
+ * @param startingLine The line we're starting from.
+ */
+ private void writeComment(String path, String[] divisions, int iteration, int startingLine) {
+ StringBuilder indent = new StringBuilder();
+ for (int j = 0; j < iteration; j++) indent.append(" ");
+
+ // Go through each line in the file
+ for (int i = startingLine; i < currentLines.size(); i++) {
+ String line = currentLines.get(i);
+
+ // If the line doesn't have an equal or larger indent, then the line could not be found.
+ if (!line.startsWith(indent.toString())) return;
+
+ // If it's already a comment, leave it be.
+ if (line.startsWith("#")) continue;
+
+ // If it's not an option (e.g. option: or 'option':), continue
+ if (!(line.startsWith(indent + divisions[iteration] + ":") ||
+ line.startsWith(indent + "'" + divisions[iteration] + "':"))) continue;
+
+ // Increment the iteration
+ iteration += 1;
+
+ // If the
+ if (iteration != divisions.length) {
+ writeComment(path, divisions, iteration, i + 1);
+ continue;
+ }
+
+ // Get the current line we're on
+ int currentLine = i;
+
+ //
+ List comments = config.getComments().get(path);
+ if (comments == null || comments.isEmpty()) continue;
+
+ for (Comment comment : comments) {
+
+ // Add an empty line before a single-
+ if (iteration == 1) {
+ currentLines.add(currentLine, "");
+ currentLine++;
+ }
+
+ if (comment == null || comment.getComment().isEmpty()) {
+ currentLines.add(currentLine, "");
+ } else {
+
+ String commentStr = comment.getComment();
+ String[] commentParts = commentStr.split("\n");
+
+ if (comment instanceof Section) {
+
+ // Get the maximum length
+ int max = 0;
+ for (String commentPart : commentParts) {
+ max = Math.max(commentPart.length(), max);
+ }
+
+ // Build the
+ StringBuilder length = new StringBuilder();
+ length.append("###");
+ for (int j = 0; j < max; j++) {
+ length.append("#");
+ }
+ length.append("###");
+ currentLines.add(currentLine, length.toString());
+ currentLine++;
+ for (String commentPart : commentParts) {
+ currentLines.add(currentLine, "# " + commentPart + " #");
+ currentLine++;
+ }
+ currentLines.add(currentLine, length.toString());
+ currentLine++;
+ continue;
+ }
+
+ for (String commentPart : commentParts) {
+ currentLines.add(currentLine, indent + "# " + commentPart);
+ currentLine++;
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ protected List getLines() {
+ return currentLines;
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/ConfigFile.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/ConfigFile.java
new file mode 100644
index 0000000..ffa017e
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/ConfigFile.java
@@ -0,0 +1,488 @@
+package io.github.thatsmusic99.configurationmaster.api;
+
+import io.github.thatsmusic99.configurationmaster.annotations.Example;
+import io.github.thatsmusic99.configurationmaster.annotations.Option;
+import io.github.thatsmusic99.configurationmaster.annotations.OptionHandler;
+import io.github.thatsmusic99.configurationmaster.annotations.handlers.*;
+import io.github.thatsmusic99.configurationmaster.api.comments.Comment;
+import io.github.thatsmusic99.configurationmaster.impl.CMConfigSection;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.error.YAMLException;
+import org.yaml.snakeyaml.representer.Representer;
+
+import java.io.*;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.*;
+import java.util.function.Function;
+import java.util.logging.Logger;
+
+/**
+ * Represents a specialised YAML file in ConfigurationMaster.
+ *
+ * It can be initialised using the following methods:
+ *
+ *
+ * - {@link ConfigFile#loadConfig(File)} - this loads a file with safety precautions.
+ * If the file contains a syntax error, the API will print an error,
+ * rename the file temporarily and load a new empty file.
+ *
+ * It is recommended to use this if you want your users to
+ * not lose their progress on a config file if they make a
+ * single mistake.
+ *
+ *
+ * - {@link io.github.thatsmusic99.configurationmaster.api.ConfigFile#ConfigFile(File)}
+ * - this loads a file without the safety precautions taken above.
+ * This is recommended if you want to handle YAMLExceptions.
+ *
+ * - Simply extend the class. This will not take any safety precautions, similarly to using the constructor.
+ *
+ *
+ */
+public class ConfigFile extends CMConfigSection {
+
+ @NotNull private static final HashMap, Class extends OptionHandler>> REGISTERED_HANDLERS = new HashMap<>();
+ @NotNull private final Yaml yaml;
+ @NotNull private final File file;
+ @NotNull private final CommentWriter writer;
+ @NotNull protected List pendingComments;
+ @NotNull protected HashMap> comments;
+ @NotNull protected HashSet examples;
+ @NotNull protected List lenientSections;
+ @NotNull protected Function optionNameTranslator;
+ @NotNull protected List permittedClasses;
+ @Nullable private Title title;
+ private boolean isNew;
+ protected boolean verbose;
+ protected boolean reloading;
+ protected static Logger logger = new CMLogger();
+
+ static {
+ REGISTERED_HANDLERS.put(boolean.class, BooleanOptionHandler.class);
+ REGISTERED_HANDLERS.put(Boolean.class, BooleanOptionHandler.class);
+ REGISTERED_HANDLERS.put(float.class, FloatOptionHandler.class);
+ REGISTERED_HANDLERS.put(Float.class, FloatOptionHandler.class);
+ REGISTERED_HANDLERS.put(int.class, IntegerOptionHandler.class);
+ REGISTERED_HANDLERS.put(Integer.class, IntegerOptionHandler.class);
+ REGISTERED_HANDLERS.put(long.class, LongOptionHandler.class);
+ REGISTERED_HANDLERS.put(Long.class, LongOptionHandler.class);
+ REGISTERED_HANDLERS.put(String.class, StringOptionHandler.class);
+ }
+
+ /**
+ * Used to initialise a config file without safety precautions taken by the API.
+ *
+ * @param file The config file to be loaded.
+ * @see ConfigFile#loadConfig(File)
+ * @throws YAMLException if the file being loaded contains syntax errors.
+ */
+ public ConfigFile(@NotNull File file) throws IOException, IllegalAccessException {
+ this(file, name -> name.replaceAll("_", "-").toLowerCase());
+ }
+
+ public ConfigFile(@NotNull File file, @NotNull Function optionNameTranslator) throws IOException, IllegalAccessException {
+
+ // Load the YAML configuration
+ yaml = getYaml();
+
+ // Set up the file itself
+ this.file = file;
+ this.optionNameTranslator = optionNameTranslator;
+ this.isNew = false;
+ this.reloading = false;
+ this.title = null;
+
+ // Set up internal variables
+ writer = new CommentWriter(this);
+ pendingComments = new ArrayList<>();
+ comments = new HashMap<>();
+ examples = new HashSet<>();
+ lenientSections = new ArrayList<>();
+ permittedClasses = new ArrayList<>();
+ }
+
+ /**
+ * Used to load a config file with safety precautions taken by the API.
+ * This safety precaution checks for syntax errors in the YAML file.
+ * If there is one, it is renamed and an empty file is loaded.
+ *
+ * @param file The file to be loaded.
+ * @return the ConfigFile instance of the file or backup file.
+ */
+ public static ConfigFile loadConfig(File file) throws Exception {
+ ConfigFile configFile = new ConfigFile(file);
+ configFile.createFile();
+ configFile.loadContent();
+ return configFile;
+ }
+
+ public void load() throws Exception {
+
+ // Before we do anything
+ preFileCreation();
+
+ // If the file doesn't already exist, create it
+ createFile();
+
+ // Read the file content
+ loadContent();
+
+ // Load the default options
+ addDefaults();
+
+ // Move everything to the new options
+ moveToNew();
+
+ // Then save!
+ save();
+
+ // Carry out any extra operations post-save
+ postSave();
+ }
+
+ public void preFileCreation() {
+ }
+
+ protected void loadFromString(String content) throws IOException {
+ try {
+
+ // Load everything
+ Map, ?> map = this.yaml.load(content);
+ if (map != null) {
+ mapToCM(map);
+ }
+
+ } catch (YAMLException exception) {
+
+ File tempFile = new File(file.getParentFile(),
+ file.getName().substring(0, file.getName().lastIndexOf("."))
+ + "-errored-" + System.currentTimeMillis() + ".yml");
+
+ Files.copy(file.toPath(), tempFile.toPath());
+
+ logger.severe("YAMLException caught - there is a syntax error in the config.");
+ exception.printStackTrace();
+ }
+ }
+
+ /**
+ * Used to load options
+ */
+ public void addDefaults() throws Exception {
+
+ handleAnnotations((field, option) -> {
+
+ if (!canProcessOption(field, option)) return;
+
+ // Also get the field value and treat it as the default option
+ final Object defaultOpt = field.get(this);
+
+ // Get the option metadata
+ final String name = option.path().isEmpty() ? optionNameTranslator.apply(field.getName()) : option.path();
+ final String comment = option.comment().isEmpty() ? null : option.comment();
+ final String section = option.section().isEmpty() ? null : option.section();
+ final boolean lenient = option.lenient();
+ Class extends OptionHandler> optionHandlerClass = option.optionHandler();
+ if (optionHandlerClass == DefaultOptionHandler.class) {
+ optionHandlerClass = REGISTERED_HANDLERS.getOrDefault(field.getType(), DefaultOptionHandler.class);
+ }
+ OptionHandler handler = optionHandlerClass.getConstructor().newInstance();
+
+ // If there's examples to add, add them
+ for (Example example : field.getAnnotationsByType(Example.class)) {
+ addExample(name + "." + example.key(), example.value());
+ }
+
+ // If it's a lenient field, then make it lenient
+ if (lenient) {
+ if (comment != null) addComment(name, comment);
+ makeSectionLenient(name);
+ }
+
+ // Add the default
+ if (defaultOpt != null) handler.addDefault(this, name, defaultOpt, section, comment);
+
+ // Set the result
+ field.set(this, handler.get(this, name));
+ });
+ }
+
+ public boolean canProcessOption(final @NotNull Field field, final @NotNull Option option) {
+ return true;
+ }
+
+ public void loadContent() throws IOException {
+ loadFromString(readFileContent());
+ }
+
+ public void moveToNew() {
+ }
+
+ public void postSave() {
+ }
+
+ /**
+ * Saves all changes made to the config to the actual file.
+ *
+ * @throws IOException if something went wrong writing to the file.
+ */
+ public void save() throws Exception {
+ try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(file.toPath()), StandardCharsets.UTF_8))) {
+ String saved = saveToString();
+ writer.write(saved);
+ isNew = false;
+ this.existingValues.clear();
+ }
+ }
+
+ protected String readFileContent() throws IOException {
+
+ StringBuilder content = new StringBuilder();
+
+ // Load the reader
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(Files.newInputStream(file.toPath()), StandardCharsets.UTF_8))) {
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ content.append(line).append("\n");
+ }
+ if (content.length() == 0) {
+ isNew = true;
+ }
+ }
+
+ return content.toString();
+ }
+
+ /**
+ * Reloads the configuration and updates all values stored inside it.
+ *
+ * @throws IOException if something went wrong saving the file.
+ */
+ public void reload() throws Exception {
+ debug("Reloading the configuration file " + file.getName() + "...");
+ reloading = true;
+
+ // Reset the defaults
+ HashMap allDefaults = new LinkedHashMap<>();
+ addDefaults(allDefaults);
+
+ // Create the file
+ createFile();
+
+ // Reset internal values
+ existingValues.clear();
+ clear();
+
+ // Load file content
+ loadContent();
+
+ // Add defaults
+ for (String path : allDefaults.keySet()) {
+
+ // Make sure it's not in a lenient section
+ String parentPath = getParentPath(path);
+ if (lenientSections.contains(parentPath)) {
+ makeSectionLenient(parentPath);
+ continue;
+ }
+
+ if (!examples.contains(path) || contains(path) || isNew) {
+ addDefault(path, allDefaults.get(path));
+ }
+ }
+ addDefaults();
+
+ moveToNew();
+ save();
+ postSave();
+
+ reloading = false;
+ }
+
+ /**
+ * Returns whether the loaded file is brand new or not.
+ *
+ * This is determined by whether a new file was created or if the file itself is empty.
+ *
+ * @return true if the file is newly loaded, false if not.
+ */
+ public boolean isNew() {
+ return isNew;
+ }
+
+ public boolean isReloading() {
+ return reloading;
+ }
+
+ protected void createFile() throws IOException {
+
+ // If the file doesn't already exist, create it
+ if (!file.exists()) {
+ if (!file.createNewFile()) {
+ throw new IOException("Failed to create " + file.getName() + "!");
+ }
+
+ // It's a new file
+ isNew = true;
+ }
+ }
+
+ public void updateAnnotations() throws Exception {
+
+ handleAnnotations((field, option) -> {
+
+ // Also get the field value and treat it as the default option
+ final Object value = field.get(this);
+
+ // Get the option metadata
+ final String name = option.path().isEmpty() ? optionNameTranslator.apply(field.getName()) : option.path();
+
+ // Update the values
+ set(name, value);
+ });
+ }
+
+ private void handleAnnotations(OptionConsumer consumer) throws Exception {
+ // Before doing anything else, check the fields
+ Field[] fields = getClass().getFields();
+ for (Field field : fields) {
+
+ // Check if a field has the option annotation, add it
+ if (!field.isAnnotationPresent(Option.class)) continue;
+ final Option option = field.getAnnotation(Option.class);
+
+ //
+ consumer.accept(field, option);
+ }
+ }
+
+ public String saveToString() throws Exception {
+
+ // Update the annotations
+ updateAnnotations();
+
+ // Convert the map
+ String dump = this.yaml.dump(convertToMap());
+ if (dump.equals("{}")) dump = "";
+
+ // Write the comments
+ writer.writeComments(new ArrayList<>(Arrays.asList(dump.split("\n"))));
+
+ // Write to the lines
+ StringBuilder result = new StringBuilder();
+ writer.getLines().forEach(line -> result.append(line).append("\n"));
+
+ // Write the title and result
+ return (title != null ? title + "\n" : "") + result;
+ }
+
+ public @NotNull HashMap> getComments() {
+ return comments;
+ }
+
+ /**
+ * Get all comments that have yet to be added. These will be added when the next default/example is set.
+ *
+ * @return Comments waiting to be added.
+ */
+ public @NotNull List getPendingComments() {
+ return pendingComments;
+ }
+
+ public @NotNull HashSet getExamples() {
+ return examples;
+ }
+
+ public @NotNull List getLenientSections() {
+ return lenientSections;
+ }
+
+ public @Nullable Title getTitle() {
+ return title;
+ }
+
+ public void setTitle(@Nullable Title title) {
+ this.title = title;
+ }
+
+ public @NotNull File getFile() {
+ return file;
+ }
+
+ /**
+ * Enables the debugging mode to track what is happening within the API.
+ *
+ * @return The ConfigFile object with debugging being enabled.
+ */
+ public ConfigFile enableDebugging() {
+ this.verbose = true;
+ return this;
+ }
+
+ public void debug(String message) {
+ if (verbose) logger.info(message);
+ }
+
+ private Yaml getYaml() {
+
+ // Initialise dump options
+ DumperOptions options = new DumperOptions();
+ options.setIndent(2);
+ options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+
+ // Initialise representer
+ Representer representerClone;
+ try {
+ representerClone = new Representer(options);
+ } catch (NoSuchMethodError ex) {
+ try {
+ representerClone = Representer.class.getConstructor().newInstance();
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
+ NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ representerClone.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+
+ // Initialise YAML
+ Yaml yaml;
+ try {
+ LoaderOptions loader = new LoaderOptions();
+ loader.setCodePointLimit(1024 * 1024 * 100);
+ loader.setTagInspector(tag -> this.permittedClasses.contains(tag.getClassName()));
+ yaml = new Yaml(new SafeConstructor(loader), representerClone, options, loader);
+ } catch (Exception | NoSuchMethodError | NoClassDefFoundError ex) {
+ // YOLO
+ try {
+ yaml = new Yaml(SafeConstructor.class.getConstructor().newInstance(), representerClone, options);
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
+ NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return yaml;
+ }
+
+ private static class CMLogger extends Logger {
+
+ // if you can't be 'em, join 'em
+ protected CMLogger() {
+ super("ConfigurationMaster", null);
+ }
+ }
+
+ private interface OptionConsumer {
+
+ void accept(T t, R r) throws Exception;
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/ConfigSection.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/ConfigSection.java
new file mode 100644
index 0000000..6b356c6
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/ConfigSection.java
@@ -0,0 +1,52 @@
+package io.github.thatsmusic99.configurationmaster.api;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public interface ConfigSection extends MemorySection {
+
+ /**
+ * Adds a default value to an option in the config file.
+ *
+ * If the option does not exist, it will be added with the provided default value.
+ *
+ * However, if the option does exist, it will only be adjusted to its correct position. The value inside does not change.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param value The default value to be used if the option doesn't already exist.
+ */
+ void addDefault(@NotNull String path, @Nullable Object value);
+
+ void addDefault(@NotNull String path, @Nullable Object value, @Nullable String comment);
+
+ void addDefault(@NotNull String path, @Nullable Object value, @Nullable String section, @Nullable String comment);
+
+ void addComment(@NotNull String path, @NotNull String comment);
+
+ void addComment(@NotNull String comment);
+
+ void addComments(@NotNull String path, @NotNull String... comments);
+
+ void moveTo(@NotNull String oldPath, @NotNull String newPath);
+
+ void moveTo(@NotNull String oldPath, @NotNull String newPath, @NotNull ConfigFile otherFile);
+
+ void addSection(@NotNull String section);
+
+ void addSection(@NotNull String path, @NotNull String section);
+
+ void addExample(@NotNull String path, @Nullable Object value);
+
+ void addExample(@NotNull String path, @Nullable Object value, @Nullable String comment);
+
+ void createExampleSection(@NotNull String path);
+
+ void forceExample(@NotNull String path, @Nullable Object value);
+
+ void forceExample(@NotNull String path, @Nullable Object value, @Nullable String comment);
+
+ void makeSectionLenient(@NotNull String path);
+
+ ConfigSection createConfigSection(@NotNull String path);
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/MemorySection.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/MemorySection.java
new file mode 100644
index 0000000..1e05a97
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/MemorySection.java
@@ -0,0 +1,327 @@
+package io.github.thatsmusic99.configurationmaster.api;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A MemorySection represents a section in the configuration that holds data.
+ */
+public interface MemorySection extends Map {
+
+ /**
+ * Returns a string at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The string stored at the path. If nothing is found, a value of null is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ default @Nullable String getString(@NotNull String path) {
+ return getString(path, null);
+ }
+
+ /**
+ * Returns a string at a specified path. If it is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @return The string stored at the path. If nothing is found, the default value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ String getString(@NotNull String path, @Nullable String defaultValue);
+
+ /**
+ * Returns an integer at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The integer stored at the path. If nothing is found, a value of 0 is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ default int getInteger(@NotNull String path) {
+ return getInteger(path, 0);
+ }
+
+ /**
+ * Returns an integer at a specified path. If it is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @return The integer stored at the path. If nothing is found, the default value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ int getInteger(@NotNull String path, int defaultValue);
+
+
+ /**
+ * Returns a double at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The double stored at the path. If nothing is found, a value of 0.0 is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ default double getDouble(@NotNull String path) {
+ return getDouble(path, 0.0);
+ }
+
+ /**
+ * Returns a double at a specified path. If it is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @return The double stored at the path. If nothing is found, the default value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ double getDouble(@NotNull String path, double defaultValue);
+
+ /**
+ * Returns an object at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The object stored at the path. If nothing is found, a null value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ default Object get(@NotNull String path) {
+ return get(path, null);
+ }
+
+ /**
+ * Returns an object at a specified path. If it is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @return The object stored at the path. If nothing is found, the default value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ Object get(@NotNull String path, @Nullable Object defaultValue);
+
+ /**
+ * Returns a boolean at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The boolean stored at the path. If nothing is found, false is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ default boolean getBoolean(@NotNull String path) {
+ return getBoolean(path, false);
+ }
+
+ /**
+ * Returns a boolean at a specified path. If it is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @return The boolean stored at the path. If nothing is found, the default value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ boolean getBoolean(@NotNull String path, boolean defaultValue);
+
+ /**
+ * Returns a long at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The long stored at the path. If nothing is found, a value of 0 is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ default long getLong(@NotNull String path) {
+ return getLong(path, 0);
+ }
+
+ /**
+ * Returns a long at a specified path. If it is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @return The long stored at the path. If nothing is found, the default value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ long getLong(@NotNull String path, long defaultValue);
+
+ /**
+ * Returns a short at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The short stored at the path. If nothing is found, a value of 0 is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ default short getShort(@NotNull String path) {
+ return getShort(path, (short) 0);
+ }
+
+ /**
+ * Returns a short at a specified path. If it is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @return The short stored at the path. If nothing is found, the default value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ short getShort(@NotNull String path, short defaultValue);
+
+ /**
+ * Returns a byte at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The byte stored at the path. If nothing is found, a value of 0 is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ default byte getByte(@NotNull String path) {
+ return getByte(path, (byte) 0);
+ }
+
+ /**
+ * Returns a byte at a specified path. If it is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @return The byte stored at the path. If nothing is found, the default value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ byte getByte(@NotNull String path, byte defaultValue);
+
+ /**
+ * Returns a float at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The float stored at the path. If nothing is found, a value of 0 is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ default float getFloat(@NotNull String path) {
+ return getFloat(path, 0f);
+ }
+
+ /**
+ * Returns a float at a specified path. If it is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @return The float stored at the path. If nothing is found, the default value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ float getFloat(@NotNull String path, float defaultValue);
+
+ /**
+ * Returns a configuration file at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The configuration section stored at the path. If nothing is found, a null value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ default ConfigSection getConfigSection(@NotNull String path) {
+ return getConfigSection(path, null);
+ }
+
+ /**
+ * Returns a configuration section at a specified path. If it is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @return The configuration section stored at the path. If nothing is found, the default value is returned.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ ConfigSection getConfigSection(@NotNull String path, @Nullable ConfigSection defaultValue);
+
+ /**
+ * Returns whether or not the memory section contains a specific path.
+ *
+ * @param path The path to search for.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return true if the memory section contains the path, false if not.
+ */
+ boolean contains(@NotNull String path);
+
+ /**
+ * Returns a list at a given path. If the provided path does not point to a list but a different data type,
+ * a new list is created containing that single element. If the path itself is not found, an empty list is created.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param The list type you want returned.
+ * @return The list stored at the path.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ @NotNull
+ default List getList(@NotNull String path) {
+ return getList(path, new ArrayList<>());
+ }
+
+ /**
+ * Returns a string list at a given path. If the provided path does not point to a list but a different data type,
+ * a new list is created containing that single element. If the path itself is not found, an empty list is created.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @return The string list stored at the path.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ @NotNull
+ default List getStringList(@NotNull String path) {
+ return getList(path, new ArrayList<>());
+ }
+
+ /**
+ * Returns a list at a given path. If the provided path does not point to a list but a different data type,
+ * a new list is created containing that single element. If the path itself is not found, the default value is returned.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param defaultValue The default value to be returned if nothing is found.
+ * @param The list type you want returned.
+ * @return The list stored at the path.
+ * @throws NullPointerException if the path is null (not if it wasn't found).
+ */
+ List getList(@NotNull String path, @Nullable List defaultValue);
+
+ /**
+ * Sets a value at a specified path.
+ *
+ * @param path The path of the option itself.
+ * To indicate for an option to be placed inside different sections, use a . delimiter, e.g. section.option
+ * @param object The object the option will be set to.
+ */
+ void set(@NotNull String path, @Nullable Object object);
+
+ /**
+ * Returns a list of option/config section keys stored within this memory section.
+ *
+ * @param deep true if options within configuration sections should be included, false if you just want to collect
+ * keys from this memory section only.
+ * @return A list of keys to the given paths.
+ */
+ default List getKeys(boolean deep) {
+ return getKeys(deep, false);
+ }
+
+ List getKeys(boolean deep, boolean useExisting);
+
+ /**
+ * The path of this section.
+ *
+ * @return The full path - periods included - of this section.
+ */
+ String getPath();
+
+
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/Title.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/Title.java
new file mode 100644
index 0000000..4b40e63
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/Title.java
@@ -0,0 +1,227 @@
+package io.github.thatsmusic99.configurationmaster.api;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * A subsection of the API which is used to make pretty
+ * titles easier to make and implement.
+ */
+public class Title {
+
+ private final List parts;
+ private int width;
+ private boolean addPadding;
+
+ /**
+ * The constructor used to initialise the title object.
+ */
+ public Title() {
+ width = 75;
+ parts = new ArrayList<>();
+ addPadding = true;
+ }
+
+ /**
+ * Changes the character width of the title object. Default value is 75.
+ *
+ * This method cannot be used after using {@link Title#addLine(String)},
+ * {@link Title#addLine(String, Pos)}, {@link Title#addSolidLine()} or
+ * {@link Title#addSolidLine(char)}.
+ *
+ * @param width The width to be set.
+ * @return the modified title object.
+ * @throws IllegalStateException when the width is adjusted after title parts have been added.
+ */
+ public Title withWidth(int width) {
+ if (!parts.isEmpty())
+ throw new IllegalStateException("Cannot adjust the title width after title content has been added!");
+ this.width = width;
+ return this;
+ }
+
+ /**
+ * Changes whether or not an extra # should be added at
+ * the end of each line. The API does this by default.
+ *
+ * This method cannot be used after using {@link Title#addLine(String)},
+ * {@link Title#addLine(String, Pos)}, {@link Title#addSolidLine()} or
+ * {@link Title#addSolidLine(char)}.
+ *
+ * @param padding Whether or not to add padding (#).
+ * @return the modified title object.
+ * @throws IllegalStateException when the padding is adjusted after title parts have been added.
+ */
+ public Title withPadding(boolean padding) {
+ if (!parts.isEmpty())
+ throw new IllegalStateException("Cannot adjust padding status after title content has been added!");
+ this.addPadding = padding;
+ return this;
+ }
+
+ /**
+ * Adds a solid line of # characters that match the title width.
+ *
+ * @return The modified title object.
+ */
+ public Title addSolidLine() {
+ return addSolidLine('#');
+ }
+
+ /**
+ * Adds a solid line of a specified character that matches the title width,
+ * but adjusted so padding can be included (or not).
+ *
+ * @param character The character to make a solid line of.
+ * @return The modified title object.
+ */
+ public Title addSolidLine(char character) {
+ parts.add(new LineTitlePart(character));
+ return this;
+ }
+
+ /**
+ * Adds a text line oriented to the left.
+ *
+ * @param content The text to be added to the title.
+ * @return The modified title object.
+ * @throws NullPointerException if the content is null.
+ * @see #addLine(String, Pos)
+ */
+ public Title addLine(@NotNull String content) {
+ return addLine(content, Pos.LEFT);
+ }
+
+ /**
+ * Adds a text line oriented to the right.
+ *
+ * @param content The text to be added to the title.
+ * @param position The position it is oriented to.
+ * @return The modified title object.
+ * @throws NullPointerException if the content or position is null.
+ * @throws IllegalArgumentException if a word in the content is longer than what the title can accept (width - 4)
+ * @see #addLine(String)
+ */
+ public Title addLine(@NotNull String content, @NotNull Pos position) {
+ // Null checks
+ Objects.requireNonNull(content, "Title content must not be null!");
+ Objects.requireNonNull(position, "Position must not be null!");
+ // If all the stuff added fits on one line, just dump it in.
+ if (content.length() < width - 3) {
+ parts.add(new TextTitlePart(content, position));
+ return this;
+ }
+ // However, if it doesn't, shorten it down.
+ StringJoiner joiner = new StringJoiner(" ");
+ for (String word : content.split(" ")) {
+ // If the word itself is waaaay too long, throw an error.
+ if (word.length() > width - 4) {
+ throw new IllegalArgumentException(String.format("Word %s of size %s is too long to be fit into the title (%s)!", word, word.length(), width - 4));
+ }
+ if ((joiner + " " + word).length() < width - 3) {
+ joiner.add(word);
+ continue;
+ }
+ parts.add(new TextTitlePart(joiner.toString(), position));
+ joiner = new StringJoiner(" ").add(word);
+ }
+ if (joiner.length() == 0) return this;
+ parts.add(new TextTitlePart(joiner.toString(), position));
+ return this;
+ }
+
+ /**
+ * Converts the title to a string.
+ *
+ * @return the string representation of the title.
+ */
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ for (TitlePart part : parts) {
+ if (builder.length() != 0) builder.append("\n");
+ builder.append(part.toString());
+ }
+ return builder.toString();
+ }
+
+ private String align(String content, Pos position) {
+ int remainder = width - 4 - content.length();
+ switch (position) {
+ case LEFT:
+ return "# " + content + repeat(" ", remainder);
+ case RIGHT:
+ return "# " + repeat(" ", remainder) + content;
+ case CENTER:
+ return "# " + repeat(" ", remainder / 2) + content + repeat(" ",
+ remainder % 2 == 1 ? remainder / 2 + 1 : remainder / 2);
+ }
+ return content;
+ }
+
+ /**
+ * Used to represent an alignment position in a title text part.
+ */
+ public enum Pos {
+ LEFT,
+ CENTER,
+ RIGHT
+ }
+
+ private class TextTitlePart extends TitlePart {
+ private final String content;
+
+ public TextTitlePart(String content, Pos pos) {
+ super(pos);
+ this.content = content;
+ }
+
+ @Override
+ public String toString() {
+ return align(content, position) + (addPadding ? " #" : "");
+ }
+ }
+
+ private class LineTitlePart extends TitlePart {
+
+ private final char character;
+
+ public LineTitlePart(char character) {
+ super(Pos.LEFT);
+ this.character = character;
+ }
+
+ @Override
+ public String toString() {
+ if (character == '#') {
+ return repeat("#", width);
+ } else {
+ return "# " + repeat(String.valueOf(character), width - 4) + (addPadding ? " #" : "");
+ }
+ }
+ }
+
+ /**
+ * Used to represent a title part to be added.
+ */
+ public abstract static class TitlePart {
+ protected Pos position;
+
+ public TitlePart(Pos pos) {
+ this.position = pos;
+ }
+
+ public abstract String toString();
+ }
+
+ private static String repeat(String str, int count) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < count; i++) {
+ builder.append(str);
+ }
+ return builder.toString();
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/comments/Comment.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/comments/Comment.java
new file mode 100644
index 0000000..f843b28
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/comments/Comment.java
@@ -0,0 +1,14 @@
+package io.github.thatsmusic99.configurationmaster.api.comments;
+
+public class Comment {
+
+ private final String comment;
+
+ public Comment(String comment) {
+ this.comment = comment;
+ }
+
+ public String getComment() {
+ return comment;
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/comments/Section.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/comments/Section.java
new file mode 100644
index 0000000..4037dfe
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/api/comments/Section.java
@@ -0,0 +1,8 @@
+package io.github.thatsmusic99.configurationmaster.api.comments;
+
+public class Section extends Comment {
+
+ public Section(String comment) {
+ super(comment);
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/impl/CMConfigSection.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/impl/CMConfigSection.java
new file mode 100644
index 0000000..e4da352
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/impl/CMConfigSection.java
@@ -0,0 +1,344 @@
+package io.github.thatsmusic99.configurationmaster.impl;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
+import io.github.thatsmusic99.configurationmaster.api.comments.Comment;
+import io.github.thatsmusic99.configurationmaster.api.comments.Section;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+public class CMConfigSection extends CMMemorySection implements ConfigSection {
+
+ public CMConfigSection() {
+ super();
+ }
+
+ public CMConfigSection(String path, ConfigFile file) {
+ super(path, file);
+ }
+
+ public void addDefault(@NotNull String path, @Nullable Object defaultOption) {
+ addDefault(path, defaultOption, null, null);
+ }
+
+ public void addDefault(@NotNull String path, @Nullable Object defaultOption, @Nullable String comment) {
+ addDefault(path, defaultOption, null, comment);
+ }
+
+ public void addDefault(@NotNull String path, @Nullable Object defaultOption, @Nullable String section, @Nullable String comment) {
+
+ // Null checks
+ Objects.requireNonNull(path, "The path cannot be null!");
+
+ // Get the full path of the key in question
+ String fullPath = getPathWithKey(path);
+
+ // Get the section to be created - if it's null, create it
+ CMMemorySection cmSection = getSectionInternal(path);
+ if (cmSection == null) cmSection = createSectionInternal(path);
+ String key = getKey(path);
+
+ if (!getParent().isReloading()) {
+
+ addPendingCommentsToPath(path);
+
+ final List comments = new ArrayList<>();
+
+ // Then handle the comments for the actual option
+ if (getParent().getComments().containsKey(fullPath)) comments.addAll(getParent().getComments().get(fullPath));
+
+ // Add the section
+ if (section != null) comments.add(new Section(section));
+
+ // Add the comment
+ if (comment != null) comments.add(new Comment(comment));
+
+ // Clear any pending comments
+ getParent().getPendingComments().clear();
+
+ // If there's comments to add,
+ if (comments.size() > 0) getParent().getComments().put(fullPath, comments);
+ }
+
+ // Add the defaults
+ cmSection.defaults.put(key, defaultOption);
+ cmSection.put(key, cmSection.existingValues.getOrDefault(key, defaultOption));
+ }
+
+ @Override
+ public void addComment(@NotNull String comment) {
+ if (getParent().isReloading()) return;
+ getParent().getPendingComments().add(new Comment(comment));
+ }
+
+ @Override
+ public void moveTo(@NotNull String oldPath, @NotNull String newPath) {
+ moveTo(oldPath, newPath, getParent());
+ }
+
+ @Override
+ public void moveTo(@NotNull String oldPath, @NotNull String newPath, @NotNull ConfigFile otherFile) {
+ Objects.requireNonNull(oldPath, "The old path cannot be null!");
+ Objects.requireNonNull(newPath, "The new path cannot be null!");
+ Objects.requireNonNull(otherFile, "The file being transferred to cannot be null!");
+
+ if (!contains(oldPath)) return;
+ CMMemorySection oldCmSection = getSectionInternal(oldPath, false);
+ if (oldCmSection == null) return;
+ CMMemorySection newCmSection = otherFile.getSectionInternal(newPath);
+ if (newCmSection == null) newCmSection = otherFile.createSectionInternal(newPath);
+ String oldKey = oldPath.substring(oldPath.lastIndexOf('.') + 1);
+ Object movingValue = oldCmSection.existingValues.get(oldKey);
+ String newKey = newPath.substring(newPath.lastIndexOf('.') + 1);
+ newCmSection.put(newKey, movingValue);
+ oldCmSection.set(oldKey, null);
+ }
+
+ @Override
+ public void addComment(@NotNull String path, @NotNull String comment) {
+ addComment(path, comment, true);
+ }
+
+ private void addComment(final @NotNull String path, final @NotNull String comment, final boolean addPending) {
+ Objects.requireNonNull(path, "The path cannot be null!");
+ Objects.requireNonNull(comment, "The comment cannot be null!");
+ if (getParent().isReloading()) return;
+ if (addPending) addPendingCommentsToPath(path);
+
+ // If a specified path already has comments, add this one onto the existing comment, otherwise just add it
+ if (getParent().getComments().containsKey(path)) {
+ getParent().getComments().get(path).add(new Comment(comment));
+ } else {
+ getParent().getComments().put(getPathWithKey(path), new ArrayList<>(Collections.singletonList(new Comment(comment))));
+ }
+ }
+
+ @Override
+ public void addComments(@NotNull String path, @NotNull String... comments) {
+ Objects.requireNonNull(path, "The path cannot be null!");
+ Objects.requireNonNull(comments, "The comments array cannot be null!");
+
+ for (String comment : comments) addComment(path, comment);
+ }
+
+ @Override
+ public void addExample(@NotNull String path, Object object) {
+ addExample(path, object, null);
+ }
+
+ @Override
+ public void addExample(@NotNull String path, Object object, @Nullable String comment) {
+ Objects.requireNonNull(path, "The path cannot be null!");
+
+ // Check if any of the sections are lenient
+ String[] sections = path.split("\\.");
+ List visited = new ArrayList<>();
+ for (String section : sections) {
+ visited.add(section);
+ String parentPath = String.join(".", visited);
+
+ // If it's lenient, check if it exists
+ if (getParent().getLenientSections().contains(parentPath)) {
+ CMConfigSection parentSection = (CMConfigSection) getSectionInternal(parentPath, false);
+ if (parentSection == null || parentSection.existingValues.get(section) == null) {
+ forceExample(path, object, comment);
+ return;
+ }
+ }
+ }
+
+ getParent().getExamples().add(getPathWithKey(path));
+ }
+
+ @Override
+ public void createExampleSection(@NotNull String path) {
+ Objects.requireNonNull(path, "The path cannot be null!");
+
+ getParent().getExamples().add(getPathWithKey(path));
+ // See if the base section exists - if not, force it
+ CMMemorySection section = getSectionInternal(path);
+ if (section == null) createConfigSection(path);
+ }
+
+ @Override
+ public void forceExample(@NotNull String path, @Nullable Object value) {
+ forceExample(path, value, null);
+ }
+
+ @Override
+ public void forceExample(@NotNull String path, @Nullable Object value, @Nullable String comment) {
+ Objects.requireNonNull(path, "The path cannot be null!");
+ getParent().getExamples().add(getPathWithKey(path));
+ addDefault(path, value, null, comment);
+ }
+
+ @Override
+ public void makeSectionLenient(@NotNull String path) {
+ // TODO - allow null/empty path to signify making the whole ass file lenient
+ Objects.requireNonNull(path, "The path cannot be null!");
+ // TODO - don't use internals here
+ CMConfigSection section = (CMConfigSection) getSectionInternal(path + ".haha");
+ if (section == null) section = createSectionInternal(path + ".haha");
+ section.forceExistingIntoActual();
+ if (getParent().getLenientSections().contains(getPathWithKey(path))) return;
+ getParent().getLenientSections().add(getPathWithKey(path));
+
+ // Add it as a default option to the parent section
+ CMConfigSection parent = (CMConfigSection) getSectionInternal(path);
+ String key = getKey(path);
+ if (parent != null) parent.defaults.put(key, new CMConfigSection(parent.getPathWithKey(key), getParent()));
+
+ // Check for any pending comments
+ if (getParent().isReloading()) return;
+ String fullPath = getPathWithKey(path);
+
+ addPendingCommentsToPath(path);
+
+ final List comments = new ArrayList<>();
+
+ // Then handle the comments for the actual option
+ if (getParent().getComments().containsKey(fullPath)) comments.addAll(getParent().getComments().get(fullPath));
+
+ // Clear any pending comments
+ getParent().getPendingComments().clear();
+
+ // If there's comments to add,
+ if (comments.size() > 0) getParent().getComments().put(fullPath, comments);
+ }
+
+ private void forceExistingIntoActual() {
+ if (!getParent().isNew()) {
+ clear();
+ }
+ for (String key : existingValues.keySet()) {
+ if (existingValues.get(key) instanceof CMConfigSection) {
+ ((CMConfigSection) existingValues.get(key)).forceExistingIntoActual();
+ }
+ put(key, existingValues.get(key));
+ }
+ }
+
+ @Override
+ public void addSection(@NotNull String section) {
+ if (getParent().isReloading()) return;
+ getParent().getPendingComments().add(new Section(section));
+ }
+
+ @Override
+ public void addSection(@NotNull String path, @NotNull String section) {
+ addSection(path, section, true);
+ }
+
+ private void addSection(final @NotNull String path, final @NotNull String section, final boolean addPending) {
+ Objects.requireNonNull(path, "The path cannot be null!");
+ Objects.requireNonNull(section, "The section cannot be null!");
+
+ if (getParent().isReloading()) return;
+ if (addPending) addPendingCommentsToPath(path);
+ // If a specified path already has comments, add this one onto the existing comment, otherwise just add it
+ if (getParent().getComments().containsKey(path)) {
+ getParent().getComments().get(path).add(new Section(section));
+ } else {
+ getParent().getComments().put(getPathWithKey(path), new ArrayList<>(Collections.singletonList(new Section(section))));
+ }
+ }
+
+ protected CMConfigSection createSectionInternal(@NotNull String path) {
+ return createConfigSection(path.substring(0, path.lastIndexOf('.')));
+ }
+
+ @Override
+ public CMConfigSection createConfigSection(@NotNull String path) {
+ Objects.requireNonNull(path, "The path must not be null!");
+ String[] sections = path.split("\\.");
+ CMConfigSection toEdit = this;
+ for (String section : sections) {
+ Object option = toEdit.get(section);
+ if (option == null) {
+ option = new CMConfigSection(
+ toEdit.getPath().length() == 0 ? section : toEdit.getPath() + "." + section,
+ toEdit.getParent());
+ toEdit.put(section, option);
+ toEdit = (CMConfigSection) option;
+ } else if (option instanceof CMConfigSection) {
+ toEdit = (CMConfigSection) option;
+ } else {
+ throw new IllegalStateException(path + " cannot be made into a configuration section due to already containing data!");
+ }
+ }
+ return toEdit;
+ }
+
+ protected Map convertToMap() {
+ LinkedHashMap map = new LinkedHashMap<>();
+ for (String path : keySet()) {
+ if (get(path) instanceof CMConfigSection) {
+ map.put(path, ((CMConfigSection) get(path)).convertToMap());
+ } else {
+ map.put(path, get(path));
+ }
+ }
+ return map;
+ }
+
+ protected void mapToCM(Map, ?> map) {
+ for (Object keyObj : map.keySet()) {
+ if (keyObj == null) keyObj = "null";
+ String key = keyObj.toString();
+ Object value = map.get(keyObj);
+ if (value instanceof Map) {
+ CMConfigSection section = new CMConfigSection(getPathWithKey(key), getParent());
+ section.mapToCM((Map, ?>) value);
+ existingValues.put(key, section);
+ } else {
+ existingValues.put(key, value);
+ }
+ }
+ }
+
+ protected String getParentPath(String path) {
+ int finalIndex = path.lastIndexOf('.');
+ if (finalIndex != -1) {
+ return path.substring(0, finalIndex);
+ }
+ return null;
+ }
+
+ private String getPathWithKey(String key) {
+ if (getPath().isEmpty()) return key;
+ return getPath() + "." + key;
+ }
+
+ protected void addDefaults(HashMap map) {
+ for (String key : keySet()) {
+ if (get(key) instanceof CMConfigSection) {
+ ((CMConfigSection) get(key)).addDefaults(map);
+ } else {
+ map.put(getPathWithKey(key), defaults.get(key));
+ }
+ }
+
+ // If the section is empty but lenient, keep it
+ if (getParent().getLenientSections().contains(getPath()) && keySet().size() == 0) {
+ map.put(getPath(), this);
+ }
+ }
+
+ private void addPendingCommentsToPath(final @NotNull String path) {
+
+ // Get the root path for pending commands
+ final int index = path.indexOf('.');
+ final String root = path.substring(0, index == -1 ? path.length() : index);
+
+ for (Comment pendingComment : getParent().getPendingComments()) {
+ if (pendingComment instanceof Section) {
+ addSection(root, pendingComment.getComment(), false);
+ } else {
+ addComment(root, pendingComment.getComment(), false);
+ }
+ }
+ getParent().getPendingComments().clear();
+ }
+}
diff --git a/API/src/main/java/io/github/thatsmusic99/configurationmaster/impl/CMMemorySection.java b/API/src/main/java/io/github/thatsmusic99/configurationmaster/impl/CMMemorySection.java
new file mode 100644
index 0000000..2411382
--- /dev/null
+++ b/API/src/main/java/io/github/thatsmusic99/configurationmaster/impl/CMMemorySection.java
@@ -0,0 +1,212 @@
+package io.github.thatsmusic99.configurationmaster.impl;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
+import io.github.thatsmusic99.configurationmaster.api.MemorySection;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+public class CMMemorySection extends LinkedHashMap implements MemorySection {
+
+ protected LinkedHashMap defaults;
+ protected LinkedHashMap existingValues;
+ protected LinkedHashMap actualValues;
+ protected String path;
+ protected ConfigFile parent;
+
+ CMMemorySection() {
+ if (this instanceof ConfigFile) {
+ this.path = "";
+ init();
+ } else {
+ throw new IllegalStateException("SHUT UP. SHUT UP");
+ }
+ }
+
+ CMMemorySection(String path, ConfigFile parent) {
+ this.path = path;
+ this.parent = parent;
+ init();
+ }
+
+ private void init() {
+ this.defaults = new LinkedHashMap<>();
+ this.existingValues = new LinkedHashMap<>();
+ }
+
+ @Override
+ public String getString(@NotNull String path, @Nullable String defaultValue) {
+ Object result = get(path, defaultValue);
+ if (result == null) return null;
+ return String.valueOf(result);
+ }
+
+ @Override
+ public int getInteger(@NotNull String path, int defaultValue) {
+ String result = getString(path);
+ try {
+ return result == null ? defaultValue : Integer.parseInt(result);
+ } catch (NumberFormatException ex) {
+ return defaultValue;
+ }
+ }
+
+ @Override
+ public double getDouble(@NotNull String path, double defaultValue) {
+ String result = getString(path);
+ try {
+ return result == null ? defaultValue : Double.parseDouble(result);
+ } catch (NumberFormatException ex) {
+ return defaultValue;
+ }
+ }
+
+ @Override
+ public Object get(@NotNull String path, @Nullable Object defaultValue) {
+ CMMemorySection section = getSectionInternal(path, false);
+ if (section == null) return defaultValue;
+ String key = getKey(path);
+ return section.getOrDefault(key, section.existingValues.getOrDefault(key, defaultValue));
+ }
+
+ @Override
+ public boolean getBoolean(@NotNull String path, boolean defaultValue) {
+ String result = getString(path);
+ if (result == null) return defaultValue;
+ if (!(result.equalsIgnoreCase("false") || result.equalsIgnoreCase("true"))) return defaultValue;
+ return result.equalsIgnoreCase("true");
+ }
+
+ @Override
+ public long getLong(@NotNull String path, long defaultValue) {
+ String result = getString(path);
+ try {
+ return result == null ? defaultValue : Long.parseLong(result);
+ } catch (NumberFormatException ex) {
+ return defaultValue;
+ }
+ }
+
+ @Override
+ public short getShort(@NotNull String path, short defaultValue) {
+ String result = getString(path);
+ try {
+ return result == null ? defaultValue : Short.parseShort(result);
+ } catch (NumberFormatException ex) {
+ return defaultValue;
+ }
+ }
+
+ @Override
+ public byte getByte(@NotNull String path, byte defaultValue) {
+ String result = getString(path);
+ try {
+ return result == null ? defaultValue : Byte.parseByte(result);
+ } catch (NumberFormatException ex) {
+ return defaultValue;
+ }
+ }
+
+ @Override
+ public float getFloat(@NotNull String path, float defaultValue) {
+ String result = getString(path);
+ try {
+ return result == null ? defaultValue : Float.parseFloat(result);
+ } catch (NumberFormatException ex) {
+ return defaultValue;
+ }
+ }
+
+ @Override
+ public ConfigSection getConfigSection(@NotNull String path, @Nullable ConfigSection defaultValue) {
+ Object value = get(path, defaultValue);
+ return value instanceof ConfigSection ? (ConfigSection) value : defaultValue;
+ }
+
+ @Override
+ public boolean contains(@NotNull String path) {
+ CMMemorySection section = getSectionInternal(path, false);
+ if (section == null) return false;
+ String key = getKey(path);
+ return section.existingValues.containsKey(key) || section.containsKey(key);
+ }
+
+ @Override
+ public List getList(@NotNull String path, @Nullable List defaultValue) {
+ Object value = get(path, defaultValue);
+ if (value == null) return defaultValue;
+ if (value.getClass().isArray()) {
+ value = Arrays.asList((Object[]) value);
+ } else if (!(value instanceof List)) {
+ value = new ArrayList<>(Collections.singletonList(value));
+ }
+ return (List) value;
+ }
+
+ @Override
+ public void set(@NotNull String path, @Nullable Object object) {
+ CMMemorySection section = getSectionInternal(path);
+ if (section == null) {
+ if (object == null) return;
+ section = getParent().createSectionInternal(path);
+ }
+ String key = getKey(path);
+ if (object == null) {
+ section.remove(key);
+ return;
+ }
+ section.put(key, object);
+ }
+
+ @Nullable
+ protected CMMemorySection getSectionInternal(@NotNull String path) {
+ return getSectionInternal(path, true);
+ }
+
+ protected CMMemorySection getSectionInternal(@NotNull String path, boolean add) {
+ Objects.requireNonNull(path, "Path must not be null!");
+ CMMemorySection section = this;
+ while (path.indexOf('.') != -1 && section != null) {
+ String key = path.substring(0, path.indexOf('.'));
+ path = path.substring(path.indexOf('.') + 1);
+ CMMemorySection tempSection;
+ if (section.existingValues.get(key) instanceof CMConfigSection) {
+ tempSection = (CMMemorySection) section.getConfigSection(key, (CMConfigSection) section.existingValues.get(key));
+ } else {
+ tempSection = (CMMemorySection) section.getConfigSection(key);
+ }
+ if (tempSection != null && add) section.putIfAbsent(key, tempSection);
+ section = tempSection;
+ }
+ return section;
+ }
+
+ @Override
+ public List getKeys(boolean deep, boolean useExisting) {
+ List keys = new ArrayList<>();
+ HashMap map = useExisting ? existingValues : this;
+ for (String path : map.keySet()) {
+ if (deep && map.get(path) instanceof CMConfigSection) {
+ keys.addAll(((CMConfigSection) map.get(path)).getKeys(true));
+ } else {
+ keys.add(path);
+ }
+ }
+ return keys;
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ protected ConfigFile getParent() {
+ return this instanceof ConfigFile ? (ConfigFile) this : parent;
+ }
+
+ protected String getKey(String path) {
+ return path.substring(path.lastIndexOf('.') + 1);
+ }
+}
diff --git a/API/src/test/java/io/github/thatsmusic99/configurationmaster/AdvancedTeleportTest.java b/API/src/test/java/io/github/thatsmusic99/configurationmaster/AdvancedTeleportTest.java
new file mode 100644
index 0000000..f5eafcb
--- /dev/null
+++ b/API/src/test/java/io/github/thatsmusic99/configurationmaster/AdvancedTeleportTest.java
@@ -0,0 +1,313 @@
+package io.github.thatsmusic99.configurationmaster;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import io.github.thatsmusic99.configurationmaster.api.Title;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class AdvancedTeleportTest {
+
+ @Test
+ public void initATConfig() throws Exception {
+ File file = new File("test-config.yml");
+ if (!file.exists()) {
+ file.createNewFile();
+ }
+ ConfigFile config = ConfigFile.loadConfig(file);
+ config.setTitle(new Title().withWidth(100).addSolidLine()
+ .addLine("-<( AdvancedTeleport )>-", Title.Pos.CENTER)
+ .addLine("Made by Niestrat99 and Thatsmusic99", Title.Pos.CENTER)
+ .addLine("")
+ .addSolidLine('-')
+ .addLine("A rapidly growing teleportation plugin looking to break the boundaries of traditional teleport plugins.")
+ .addLine("")
+ .addLine("SpigotMC - https://www.spigotmc.org/resources/advanced-teleport.64139/")
+ .addLine("Wiki - https://github.com/Niestrat99/AT-Rewritten/wiki")
+ .addLine("Discord - https://discord.gg/mgWbbN4")
+ .addSolidLine());
+
+ config.addComment("Another comment at the very top for all you lads :)");
+ config.addDefault("use-basic-teleport-features", true, "Features", "Whether basic teleportation features should be enabled or not." +
+ "\nThis includes /tpa, /tpahere, /tpblock, /tpunblock and /back." +
+ "\nThis does not disable the command for other plugins - if you want other plugins to use the provided commands, use Bukkit's commands.yml file." +
+ "\nPlease refer to https://bukkit.gamepedia.com/Commands.yml for this!");
+
+ config.addDefault("use-warps", true, "Whether warps should be enabled in the plugin.");
+ config.addDefault("use-spawn", true, "Whether the plugin should modify spawn/spawn properties.");
+ config.addDefault("use-randomtp", true, "Whether the plugin should allow random teleportation.");
+ config.addDefault("use-homes", true, "Whether homes should be enabled in the plugin.");
+ config.addDefault("disabled-commands", new ArrayList<>(), "The commands that AT should not register upon starting up.\n" +
+ "In other words, this gives up the command for other plugins to use.\n" +
+ "NOTE: If you are using Essentials with AT and want AT to give up its commands to Essentials, Essentials does NOT go down without a fight. Jesus Christ. You'll need to restart the server for anything to change.");
+
+ config.addSection("Teleport Requesting");
+ config.addDefault("request-lifetime", 60, "\u2770 How long tpa and tpahere requests last before expiring.");
+ config.addDefault("allow-multiple-requests", true, "Whether or not the plugin should enable the use of multiple requests.\n" +
+ "When enabled, user 1 may get TPA requests from user 2 and 3, but user 1 is prompted to select a specific request.\n" +
+ "When this is disabled and user 1 receives requests from user 2 and then 3, they will only have user 3's request to respond to.");
+ config.addDefault("notify-on-expire", true, "Let the player know when their request has timed out or been displaced by another user's request.\n" +
+ "Displacement only occurs when allow-multiple-requests is disabled.");
+ // addDefault("tpa-restrict-movement-on", "requester");
+ // addDefault("tpahere-restrict-movement-on", "requester");
+
+ config.addDefault("warm-up-timer-duration", 3, "Warm-Up Timers", "The number of seconds it takes for the teleportation to take place following confirmation.\n" +
+ "(i.e. \"You will teleport in 3 seconds!\")\n" +
+ "This acts as the default option for the per-command warm-ups.");
+ config.addDefault("cancel-warm-up-on-rotation", true, "Whether or not teleportation should be cancelled if the player rotates or moves.");
+ config.addDefault("cancel-warm-up-on-movement", true, "Whether or not teleportation should be cancelled upon movement only.");
+
+ config.addComment("per-command-warm-ups", "Command-specific warm-ups.");
+ config.addDefault("per-command-warm-ups.tpa", "default", "Warm-up timer for /tpa.");
+ config.addDefault("per-command-warm-ups.tpahere", "default", "Warm-up timer for /tpahere");
+ config.addDefault("per-command-warm-ups.tpr", "default", "Warm-up timer for /tpr, or /rtp.");
+ config.addDefault("per-command-warm-ups.warp", "default", "Warm-up timer for /warp");
+ config.addDefault("per-command-warm-ups.spawn", "default", "Warm-up timer for /spawn");
+ config.addDefault("per-command-warm-ups.home", "default", "Warm-up timer for /home");
+ config.addDefault("per-command-warm-ups.back", "default", "Warm-up timer for /back");
+
+ config.addDefault("cooldown-duration", 5, "Cooldowns", "How long before the user can use a command again.\n" +
+ "This stops users spamming commands repeatedly.\n" +
+ "This is also the default cooldown period for all commands.");
+ config.addDefault("add-cooldown-duration-to-warm-up", true, "Adds the warm-up duration to the cooldown duration.\n" +
+ "For example, if the cooldown duration was 5 seconds but the warm-up was 3, the cooldown becomes 8 seconds long.");
+ config.addDefault("apply-cooldown-to-all-commands", false, "Whether or not the cooldown of one command will stop a user from using all commands.\n" +
+ "For example, if a player used /tpa with a cooldown of 10 seconds but then used /tpahere with a cooldown of 5, the 10-second cooldown would still apply.\n" +
+ "On the other hand, if a player used /tpahere, the cooldown of 5 seconds would apply to /tpa and other commands.");
+ config.addDefault("apply-cooldown-after", "request", "When to apply the cooldown\n" +
+ "Options include:\n" +
+ "- request - Cooldown starts as soon as any teleport command is made and still applies even if no teleport takes place (i.e. cancelled by movement or not accepted).\n" +
+ "- accept - Cooldown starts only when the teleport request is accepted (with /tpyes) and still applies even if no teleport takes place (i.e. cancelled by movement).\n" +
+ "- teleport - Cooldown starts only when the teleport actually happens.\n" +
+ "Note:\n" +
+ "'request' and 'accept' behave the same for /rtp, /back, /spawn, /warp, and /home\n" +
+ "cooldown for /tpall always starts when the command is ran, regardless if any player accepts or teleports");
+
+ config.addComment("per-command-cooldowns", "Command-specific cooldowns.");
+ config.addDefault("per-command-cooldowns.tpa", "default", "Cooldown for /tpa.");
+ config.addDefault("per-command-cooldowns.tpahere", "default", "Cooldown for /tpahere");
+ config.addDefault("per-command-cooldowns.tpr", "default", "Cooldown for /tpr, or /rtp.");
+ config.addDefault("per-command-cooldowns.warp", "default", "Cooldown for /warp");
+ config.addDefault("per-command-cooldowns.spawn", "default", "Cooldown for /spawn");
+ config.addDefault("per-command-cooldowns.home", "default", "Cooldown for /home");
+ config.addDefault("per-command-cooldowns.back", "default", "Cooldown for /back");
+ // addDefault("per-command-cooldowns.sethome", "default", "Cooldown for /sethome");
+ // addDefault("per-command-cooldowns.setwarp", "default", "Cooldown for /setwarp");
+
+ config.addDefault("cost-amount", 100.0, "Teleportation Costs", "The amount it costs to teleport somewhere." +
+ "\nIf you want to use Vault Economy, use 100.0 to charge $100." +
+ "\nIf you want to use Minecraft EXP points, use 10EXP for 10 EXP Points." +
+ "\nIf you want to use Minecraft EXP levels, use 5LVL for 5 levels." +
+ "\nIf you want to use items, use the format MATERIAL:AMOUNT or MATERIAL:AMOUNT:BYTE." +
+ "\nFor example, on 1.13+, ORANGE_WOOL:3 for 3 orange wool, but on versions before 1.13, WOOL:3:1." +
+ "\nIf you're on a legacy version and unsure on what byte to use, see https://minecraftitemids.com/types" +
+ "\nTo use multiple methods of charging, use a ; - e.g. '100.0;10LVL' for $100 and 10 EXP levels." +
+ "\nTo disable, just put an empty string, i.e. ''");
+
+ config.addComment("per-command-cost", "Command-specific costs.");
+ config.addDefault("per-command-cost.tpa", "default", "Cost for /tpa.");
+ config.addDefault("per-command-cost.tpahere", "default", "Cost for /tpahere.");
+ config.addDefault("per-command-cost.tpr", "default", "Cost for /tpr, or /rtp.");
+ config.addDefault("per-command-cost.warp", "default", "Cost for /warp");
+ config.addDefault("per-command-cost.spawn", "default", "Cost for /spawn");
+ config.addDefault("per-command-cost.home", "default", "Cost for /home");
+ config.addDefault("per-command-cost.back", "default", "Cost for /back");
+ //addDefault("per-command-cost.sethome", "default", "Cost for /sethome");
+ //addDefault("pet-command-cost.setwarp", "default", "Cost for /setwarp");
+
+ config.addSection("SQL Storage");
+
+ config.addDefault("use-mysql", false, "Whether the plugin should use SQL storage or not.\n" +
+ "By default, AT uses SQLite storage, which stores data in a .db file locally.");
+ config.addDefault("mysql-host", "127.0.0.1", "The MySQL host to connect to.");
+ config.addDefault("mysql-port", 3306, "The port to connect to.");
+ config.addDefault("mysql-database", "database", "The database to connect to.");
+ config.addDefault("mysql-username", "username", "The username to use when connecting.");
+ config.addDefault("mysql-password", "password", "The password to use when connecting.");
+ config.addDefault("mysql-table-prefix", "advancedtp", "The prefix of all AT tables. \n" +
+ "If you're on Bungee, you may want to add your server's name to the end.");
+
+ config.addDefault("enable-distance-limitations", false, "Distance Limitations",
+ "Enables the distance limiter to stop players teleporting over a large distance.\n" +
+ "This is only applied when people are teleporting in the same world.");
+ config.addDefault("maximum-teleport-distance", 1000, "The maximum distance that a player can teleport.\n" +
+ "This is the default distance applied to all commands when specified.");
+ config.addDefault("monitor-all-teleports-distance", false, "Whether or not all teleportations - not just AT's - should be checked for distance.");
+
+ config.addComment("per-command-distance-limitations", "Determines the distance limit for each command.");
+ config.addDefault("per-command-distance-limitations.tpa", "default", "Distance limit for /tpa");
+ config.addDefault("per-command-distance-limitations.tpahere", "default", "Distance limit for /tpahere");
+ config.addDefault("per-command-distance-limitations.tpr", "default", "Distance limit for /tpr");
+ config.addDefault("per-command-distance-limitations.warp", "default", "Distance limit for /warp");
+ config.addDefault("per-command-distance-limitations.spawn", "default", "Distance limit for /spawn");
+ config.addDefault("per-command-distance-limitations.home", "default", "Distance limit for /home");
+ config.addDefault("per-command-distance-limitations.back", "default", "Distance limit for /back");
+
+ config.addSection("Teleportation Limitations");
+
+ config.addComment("WARNING: A lot of the options below are considered advanced and use special syntax that is not often accepted in YAML.\n" +
+ "When using such options, wrap them in quotes: ''\n" +
+ "As an example, 'stop-teleportation-out:world,world_nether'");
+
+ config.addDefault("enable-teleport-limitations", false,
+ "Enables teleport limitations. This means cross-world or even world teleportation can be limited within specific worlds.");
+ config.addDefault("monitor-all-teleports-limitations", false, "Whether or not all teleportation - not just AT's - should be checked to see if teleportation is allowed.");
+
+ config.addComment("world-rules", "The teleportation rules defined for each world.\n" +
+ "Rules include:\n" +
+ "- stop-teleportation-out - Stops players teleporting to another world when they are in this world.\n" +
+ "- stop-teleportation-within - Stops players teleporting within the world.\n" +
+ "- stop-teleportation-into - Stops players teleporting into this world.\n" +
+ "To combine multiple rules, use a ; - e.g. stop-teleportation-out;stop-teleportation-within\n" +
+ "For out and into rules, you can make it so that rules only initiate when in or going to a specific world using :, e.g. stop-teleportation-out:world stops players teleporting to \"world\" in the world they're currently in.\n" +
+ "To do the opposite (i.e. initiates the rule when users are not in the specified world), use !, e.g. stop-teleportation-into!world stops teleportation into a specific world if they are not in \"world\". If ! and : are used in the same rule, then : is given top priority." +
+ "To make this rule work with multiple worlds, use a comma (,), e.g. stop-teleportation-into:world,world_nether");
+
+ config.makeSectionLenient("world-rules");
+ config.addDefault("world-rules.default", "stop-teleportation-within");
+ config.addExample("world-rules.world", "default");
+ config.addExample("world-rules.world_nether", "stop-teleportation-into!world", "Stops people teleporting into the Nether if they're not coming from \"world\"");
+
+ config.addComment("command-rules", "The teleportation rules defined for each AT command.\n" +
+ "Rules include:\n" +
+ "- override - The command will override world rules and run regardless.\n" +
+ "- ignore - The command will refuse to run regardless of world rules.\n" +
+ "To combine multiple rules, use a ;.\n" +
+ "To make rules behave differently in different worlds, use : to initiate the rule in a specific world (e.g. override:world to make the command override \"world\"'s rules.)\n" +
+ "To initiate rules outside of a specific world, use ! (e.g. override!world to make the command override world rules everywhere but in world)\n" +
+ "To use multiple worlds, use a comma (,).\n" +
+ "By default, all commands will comply with the world rules. If no rules are specified, they will comply.\n" +
+ "All worlds specified will be considered the world in which the player is currently in. For worlds being teleported to, add > to the start of the world name.\n" +
+ "For example, ignore:world,>world_nether will not run if the player is in \"world\" or if the player is going into the Nether.");
+ config.addDefault("command-rules.tpa", "");
+ config.addDefault("command-rules.tpahere", "");
+ config.addDefault("command-rules.tpr", "");
+ config.addDefault("command-rules.warp", "");
+ config.addDefault("command-rules.spawn", "");
+ config.addDefault("command-rules.home", "");
+ config.addDefault("command-rules.back", "");
+
+ config.addDefault("maximum-x", 5000, "RandomTP", "The maximum X coordinate to go up to when selecting a random location.");
+ config.addDefault("maximum-z", 5000, "The maximum Z coordinate to go up to when selecting a random location.");
+ config.addDefault("minimum-x", -5000, "The minimum X coordinate to go down to when selecting a random location.");
+ config.addDefault("minimum-z", -5000, "The minimum Z coordinate to go down to when selecting a random location.");
+ config.addDefault("use-world-border", true, "When WorldBorder is installed, AT will check the border of each world instead rather than using the minimum and maximum coordinates.");
+ config.addDefault("use-rapid-response", true, "Use the new rapid response system for RTP.\n" +
+ "This means valid locations are prepared before a user chooses to use /tpr or interact with a sign, meaning they are ready for use and can instantly TP a player.\n" +
+ "This feature allows you to use the \"tpr\" death option in the death management section further down.\n" +
+ "IMPORTANT NOTE - this feature only works on the Paper server type and any of its forks. It is not considered safe to use on Spigot or Bukkit.");
+ config.addDefault("prepared-locations-limit", 3, "How many locations can be prepared per world when using AT's Rapid Response system.\n" +
+ "These are immediately prepared upon startup and when a world is loaded.");
+ config.addDefault("ignore-world-generators", new ArrayList<>(Arrays.asList(
+ "us.talabrek.ultimateskyblock.world.SkyBlockChunkGenerator",
+ "us.talabrek.ultimateskyblock.world.SkyBlockNetherChunkGenerator",
+ "world.bentobox.bskyblock.generators.ChunkGeneratorWorld",
+ "world.bentobox.acidisland.world.ChunkGeneratorWorld",
+ "world.bentobox.oneblock.generators.ChunkGeneratorWorld",
+ "com.wasteofplastic.askyblock.generators.ChunkGeneratorWorld",
+ "com.wasteofplastic.acidisland.generators.ChunkGeneratorWorld",
+ "b.a",
+ "com.chaseoes.voidworld.VoidWorld.VoidWorldGenerator",
+ "club.bastonbolado.voidgenerator.EmptyChunkGenerator")), "AT's Rapid Response system automatically loads locations for each world, but can be problematic on some worlds, mostly SkyBlock worlds.\n" +
+ "In response, this list acts as pro-active protection and ignores worlds generated using the following generators.\n" +
+ "This is provided as an option so you can have control over which worlds have locations load.");
+ config.addDefault("avoid-blocks", new ArrayList<>(Arrays.asList("WATER", "LAVA", "STATIONARY_WATER", "STATIONARY_LAVA")),
+ "Blocks that people must not be able to land in when using /tpr.");
+ config.addDefault("avoid-biomes", new ArrayList<>(Arrays.asList("OCEAN", "DEEP_OCEAN")), "Biomes that the plugin should avoid when searching for a location.");
+ config.addDefault("whitelist-worlds", false, "Whether or not /tpr should only be used in the worlds listed below.");
+ config.addDefault("redirect-to-whitelisted-worlds", true, "Whether or not players should be directed to a whitelisted world when using /tpr.\n" +
+ "When this option is disabled and the player tries to use /tpr in a non-whitelisted world, the command simply won't work.");
+ config.addDefault("allowed-worlds", new ArrayList<>(Arrays.asList("world", "world_nether")), "Worlds you can use /tpr in.\n" +
+ "If a player uses /tpr in a world that doesn't allow it, they will be teleported in the first world on the list instead.\n" +
+ "To make this feature effective, turn on \"whitelist-worlds\" above.");
+
+
+ config.addDefault("default-homes-limit", -1, "Homes", "The default maximum of homes people can have.\n" +
+ "This can be overridden by giving people permissions such as at.member.homes.10.\n" +
+ "To disable this, use -1 as provided by default.");
+ config.addDefault("add-bed-to-homes", true, "Whether or not the bed home should be added to /homes.");
+ config.addDefault("deny-homes-if-over-limit", false, "Whether or not players should be denied access to some of their homes if they exceed their homes limit.\n" +
+ "The homes denied access to will end up being their most recently set homes.\n" +
+ "For example, having homes A, B, C, D and E with a limit of 3 will deny access to D and E.");
+ config.addDefault("hide-homes-if-denied", false, "If homes should be hidden from /homes should they be denied access.\n" +
+ "If this is false, they will be greyed out in the /homes list.");
+
+ config.addDefault("tpa-request-received", "none", "Notifications/Sounds",
+ "The sound played when a player receives a teleportation (tpa) request.\n" +
+ "For 1.16+, check https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html for a list of sounds you can use\n" +
+ "For 1.15 and below, check https://www.spigotmc.org/threads/sounds-spigot-1-7-1-14-4-sound-enums.340452/ for a list of sounds down to 1.7.\n" +
+ "(Friendly reminder that 1.7.x is not supported though!)\n" +
+ "Set to \"none\" if you want no sound playing.");
+ config.addDefault("tpa-request-sent", "none", "The sound played when a player sends a teleportation (tpa) request.");
+ config.addDefault("tpahere-request-received", "none", "The sound played when a player receives a teleportation (tpahere) request.");
+ config.addDefault("tpahere-request-sent", "none", "The sound played when a player sends a teleportation (tpahere) request.");
+
+ config.addDefault("used-teleport-causes", new ArrayList<>(Arrays.asList("COMMAND", "PLUGIN", "SPECTATE")), "Back",
+ "The teleport causes that the plugin must listen to allow players to teleport back to the previous location.\n" +
+ "You can see a full list of these causes at https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/player/PlayerTeleportEvent.TeleportCause.html");
+ config.addDefault("back-search-radius", 5, "The cubic radius to search for a safe block when using /back.\n" +
+ "If a player teleports from an unsafe location and uses /back to return to it, the plugin will search all blocks within this radius to see if it is a safe place for the player to be moved to.\n" +
+ "It is recommend to avoid setting this option too high as this can have a worst case execution time of O(n^3) (e.g. run 27 times, 64, 125, 216 and so on).\n" +
+ "To disable, either set to 0 or -1.");
+
+
+ config.addDefault("teleport-to-spawn-on-first-join", true, "Spawn Management",
+ "Whether the player should be teleported to the spawnpoint when they join for the first time.");
+ config.addDefault("teleport-to-spawn-on-every-join", false,
+ "Whether the player should be teleported to the spawnpoint every time they join.");
+
+ config.addComment("death-management", "Determines how and where players teleport when they die.\n" +
+ "Options include:\n" +
+ "- spawn - Teleports the player to the spawnpoint of either the world or specified by the plugin.\n" +
+ "- bed - Teleports to the player's bed.\n" +
+ "- anchor - 1.16+ only, teleports to the player's respawn anchor. However, due to limitations with Spigot's API, it may or may not always work. (add Player#getRespawnAnchor pls)\n" +
+ "- warp:Warp Name - Teleports the player to a specified warp. For example, if you want to teleport to Hub, you'd type warp:Hub\n" +
+ "- tpr - Teleports the player to a random location. Can only be used when the rapid response system is enabled." +
+ "- {default} - Uses the default respawn option, which is spawn unless set differently.\n" +
+ "If you're using EssentialsX Spawn and want AT to take over respawn mechanics, set respawn-listener-priority in EssX's config.yml file to lowest.");
+
+ config.makeSectionLenient("death-management");
+ config.addDefault("death-management.default", "spawn");
+ config.addExample("death-management.world", "{default}");
+ config.addExample("death-management.special-world", "warp:Special");
+ config.addExample("death-management.another-world", "bed");
+
+ config.addDefault("default-permissions", new ArrayList<>(Arrays.asList("at.member.*", "at.member.warp.*")), "Permissions",
+ "The default permissions given to users without OP.\n" +
+ "By default, Advanced Teleport allows users without OP to use all member features.\n" +
+ "This allows for permission management without a permissions plugin, especially if a user doesn't understand how such plugins work.\n" +
+ "However, if you have a permissions plugin and Vault installed, you cannot make admin permissions work by default.");
+ config.addDefault("allow-admin-permissions-as-default-perms", false, "Allows admin permissions to be allowed as default permissions by default.\n" +
+ "If you want to use admin permissions, it's often recommended to use a permissions plugin such as LuckPerms.\n" +
+ "Do not enable this if you are unsure of the risks this option proposes.");
+
+ config.save();
+ System.out.println("Intermission");
+ config.reload();
+
+ Assert.assertTrue(config.getBoolean("use-basic-teleport-features"));
+ Assert.assertTrue(config.getBoolean("use-warps"));
+ Assert.assertTrue(config.getBoolean("use-randomtp"));
+ Assert.assertTrue(config.getBoolean("use-homes"));
+ Assert.assertEquals(new ArrayList<>(), config.getStringList("disabled-commands"));
+
+ Assert.assertEquals(60, config.getInteger("request-lifetime"));
+ Assert.assertTrue(config.getBoolean("allow-multiple-requests"));
+ Assert.assertTrue(config.getBoolean("notify-on-expire"));
+
+ Assert.assertEquals(3, config.getInteger("warm-up-timer-duration"));
+ Assert.assertTrue(config.getBoolean("cancel-warm-up-on-rotation"));
+ Assert.assertTrue(config.getBoolean("cancel-warm-up-on-movement"));
+
+ Assert.assertEquals("default", config.getString("per-command-warm-ups.tpa"));
+
+ config.set("cooldown-duration", "60");
+ Assert.assertEquals("60", config.getString("cooldown-duration"));
+ Assert.assertEquals(60, config.getInteger("cooldown-duration"));
+ Assert.assertEquals(new ArrayList<>(), config.getList("disabled-commands"));
+ }
+}
diff --git a/API/src/test/java/io/github/thatsmusic99/configurationmaster/AnnotatedConfigTest.java b/API/src/test/java/io/github/thatsmusic99/configurationmaster/AnnotatedConfigTest.java
new file mode 100644
index 0000000..811f0d7
--- /dev/null
+++ b/API/src/test/java/io/github/thatsmusic99/configurationmaster/AnnotatedConfigTest.java
@@ -0,0 +1,19 @@
+package io.github.thatsmusic99.configurationmaster;
+
+import io.github.thatsmusic99.configurationmaster.config.ExampleConfig;
+import org.junit.Test;
+
+import java.io.File;
+
+public class AnnotatedConfigTest {
+
+ @Test
+ public void testAnnotatedConfig() throws Exception {
+ ExampleConfig config = new ExampleConfig(new File("test-config.yml"));
+ config.load();
+ assert config.COMBAT_DURATION == 30;
+
+ config.COMBAT_DURATION = 20;
+ config.save();
+ }
+}
diff --git a/API/src/test/java/io/github/thatsmusic99/configurationmaster/CommandWhitelistTest.java b/API/src/test/java/io/github/thatsmusic99/configurationmaster/CommandWhitelistTest.java
new file mode 100644
index 0000000..bc1c012
--- /dev/null
+++ b/API/src/test/java/io/github/thatsmusic99/configurationmaster/CommandWhitelistTest.java
@@ -0,0 +1,65 @@
+package io.github.thatsmusic99.configurationmaster;
+
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Sample configuration setup for CommandWhitelist by YouHaveTrouble
+ */
+public class CommandWhitelistTest {
+
+ @Test
+ public void doCommandWhitelistTests() throws Exception {
+ ConfigFile config = ConfigFile.loadConfig(new File("command-whitelist.yml"));
+
+ config.addDefault("messages.prefix", "CommandWhitelist > ");
+ config.addDefault("messages.command_denied", "No such command.");
+ config.addDefault("messages.subcommand_denied", "You cannot use this subcommand");
+ config.addDefault("messages.no_permission", "You don't have permission to do this.");
+ config.addDefault("messages.no_such_subcommand", "No subcommand by that name.");
+ config.addDefault("messages.config_reloaded", "Configuration reloaded.");
+ config.addDefault("messages.added_to_whitelist", "Whitelisted command %s for permission %s");
+ config.addDefault("messages.removed_from_whitelist", "Removed command %s from permission %s");
+ config.addDefault("messages.group_doesnt_exist", "Group doesn't exist or error occured");
+
+ config.addComment("messages", "Messages use MiniMessage formatting (https://docs.adventure.kyori.net/minimessage.html#format)");
+
+ config.addDefault("use_protocollib", false, "Do not enable if you don't have issues with aliased commands.\nThis requires server restart to take effect.");
+
+ config.makeSectionLenient("groups");
+ List exampleCommands = new ArrayList<>();
+ exampleCommands.add("example");
+ List exampleSubCommands = new ArrayList<>();
+ exampleSubCommands.add("example of");
+
+ config.addExample("groups.example.commands", exampleCommands, "This is the WHITELIST of commands that players will be able to see/use in the group \"example\"");
+ config.addExample("groups.example.subcommands", exampleSubCommands, "This is the BLACKLIST of subcommands that players will NOT be able to see/use in the group \"example\"");
+ config.addComment("groups.example", "All groups except from default require commandwhitelist.group. permission\ncommandwhitelist.group.example in this case\n If you wish to leave the list empty, put \"commands: []\" or \"subcommands: []\"");
+
+
+ List defaultCommands = new ArrayList<>();
+ defaultCommands.add("help");
+ defaultCommands.add("spawn");
+ defaultCommands.add("bal");
+ defaultCommands.add("balance");
+ defaultCommands.add("baltop");
+ defaultCommands.add("pay");
+ defaultCommands.add("r");
+ defaultCommands.add("msg");
+ defaultCommands.add("tpa");
+ defaultCommands.add("tpahere");
+ defaultCommands.add("tpaccept");
+ defaultCommands.add("tpdeny");
+ defaultCommands.add("warp");
+ List defaultSubcommands = new ArrayList<>();
+ defaultSubcommands.add("help about");
+
+
+ config.save();
+ }
+}
diff --git a/API/src/test/java/io/github/thatsmusic99/configurationmaster/config/ExampleConfig.java b/API/src/test/java/io/github/thatsmusic99/configurationmaster/config/ExampleConfig.java
new file mode 100644
index 0000000..0db457a
--- /dev/null
+++ b/API/src/test/java/io/github/thatsmusic99/configurationmaster/config/ExampleConfig.java
@@ -0,0 +1,35 @@
+package io.github.thatsmusic99.configurationmaster.config;
+
+import io.github.thatsmusic99.configurationmaster.annotations.Example;
+import io.github.thatsmusic99.configurationmaster.annotations.Option;
+import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
+import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
+import org.jetbrains.annotations.NotNull;
+import org.yaml.snakeyaml.error.YAMLException;
+
+import java.io.File;
+import java.io.IOException;
+
+public class ExampleConfig extends ConfigFile {
+
+ @Option(comment = "How long a player is considered to be in combat for.", section = "Combat")
+ public int COMBAT_DURATION = 30;
+
+ @Option(comment = "Players who should not be in combat and why.", lenient = true)
+ @Example(key = "Thatsmusic99", value = "Git gud")
+ @Example(key = "Niestrat99", value = "Too handsome")
+ public ConfigSection DENIED_PLAYERS;
+ @Option(comment = "How long the grace period lasts.")
+ public int GRACE_PERIOD = 30;
+
+ /**
+ * Used to load a config file without safety precautions taken by the API.
+ *
+ * @param file The config file to be loaded.
+ * @throws YAMLException if the file being loaded contains syntax errors.
+ * @see ConfigFile#loadConfig(File)
+ */
+ public ExampleConfig(@NotNull File file) throws IOException, IllegalAccessException {
+ super(file);
+ }
+}
diff --git a/Bukkit/ConfigurationMaster-Bukkit.iml b/Bukkit/ConfigurationMaster-Bukkit.iml
new file mode 100644
index 0000000..fa63d4b
--- /dev/null
+++ b/Bukkit/ConfigurationMaster-Bukkit.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ SPIGOT
+
+
+
+
+
\ No newline at end of file
diff --git a/Bukkit/pom.xml b/Bukkit/pom.xml
new file mode 100644
index 0000000..b6ae390
--- /dev/null
+++ b/Bukkit/pom.xml
@@ -0,0 +1,58 @@
+
+
+
+ ConfigurationMaster
+ com.github.thatsmusic99
+ v2.0.0-rc.3
+
+ 4.0.0
+
+ ConfigurationMaster-Bukkit
+ v2.0.0-rc.3
+
+
+ 8
+ 8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.3.0
+
+ false
+ false
+ /tmp
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
+
+
+ com.github.thatsmusic99
+ ConfigurationMaster-API
+ v2.0.0-rc.2
+
+
+
+ org.spigotmc
+ spigot-api
+ 1.16.2-R0.1-SNAPSHOT
+ provided
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/io/github/thatsmusic99/configurationmaster/CMFile.java b/Bukkit/src/main/java/io/github/thatsmusic99/configurationmaster/CMFile.java
similarity index 98%
rename from src/main/java/io/github/thatsmusic99/configurationmaster/CMFile.java
rename to Bukkit/src/main/java/io/github/thatsmusic99/configurationmaster/CMFile.java
index d4e96bd..c3ef8cf 100644
--- a/src/main/java/io/github/thatsmusic99/configurationmaster/CMFile.java
+++ b/Bukkit/src/main/java/io/github/thatsmusic99/configurationmaster/CMFile.java
@@ -1,13 +1,15 @@
package io.github.thatsmusic99.configurationmaster;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.io.*;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
/**
* CMFile is the specialised configuration file used by
@@ -33,7 +35,9 @@
* conventions and to make it easier for others to use and understand.
*
* @author Holly (Thatsmusic99)
+ * @deprecated Please use the ConfigFile class instead.
*/
+@Deprecated
public abstract class CMFile {
// The actual configuration file.
@@ -90,6 +94,9 @@ public CMFile(Plugin plugin, String name) {
* @param name The name of the config file.
*/
public CMFile(Plugin plugin, File folder, String name) {
+ plugin.getLogger().severe("This plugin is using the CMFile class in ConfigurationMaster. " +
+ "This class is being removed in the next major release as it has been replaced by the standalone ConfigFile." +
+ "Please urge the developer to update their ConfigurationMaster code as soon as possible.");
this.plugin = plugin;
this.folder = folder;
this.name = name;
@@ -1124,9 +1131,9 @@ public boolean isNew() {
*
* @return The config file as a FileConfiguration object.
*/
- @Nullable
+ @Deprecated
public FileConfiguration getConfig() {
- return config;
+ return YamlConfiguration.loadConfiguration(configFile);
}
/**
diff --git a/src/main/java/io/github/thatsmusic99/configurationmaster/ConfigurationMaster.java b/Bukkit/src/main/java/io/github/thatsmusic99/configurationmaster/ConfigurationMaster.java
similarity index 100%
rename from src/main/java/io/github/thatsmusic99/configurationmaster/ConfigurationMaster.java
rename to Bukkit/src/main/java/io/github/thatsmusic99/configurationmaster/ConfigurationMaster.java
diff --git a/src/main/resources/plugin.yml b/Bukkit/src/main/resources/plugin.yml
similarity index 85%
rename from src/main/resources/plugin.yml
rename to Bukkit/src/main/resources/plugin.yml
index 3228800..bdd50bd 100644
--- a/src/main/resources/plugin.yml
+++ b/Bukkit/src/main/resources/plugin.yml
@@ -1,5 +1,5 @@
main: io.github.thatsmusic99.configurationmaster.ConfigurationMaster
name: ConfigurationMaster
-version: 1.0.2
+version: 2.0.0-BETA-3
author: Thatsmusic99
api-version: 1.13
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 8c80932..ef18c5a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,8 @@
com.github.thatsmusic99
ConfigurationMaster
- v1.0.2
+ pom
+ v2.0.0-rc.3
@@ -22,6 +23,10 @@
+
+ API
+ Bukkit
+
@@ -31,19 +36,18 @@
-
- org.spigotmc
- spigot-api
- 1.16.2-R0.1-SNAPSHOT
+ org.jetbrains
+ annotations
+ 24.0.1
provided
- org.jetbrains
- annotations
- 13.0
+ junit
+ junit
+ 4.13.2
+ test
-
\ No newline at end of file