Browse Source

Initial commit

Lortseam 5 years ago
commit
c0024109fa

+ 29 - 0
.gitignore

@@ -0,0 +1,29 @@
+# gradle
+
+.gradle/
+build/
+out/
+classes/
+
+# eclipse
+
+*.launch
+
+# idea
+
+.idea/
+*.iml
+*.ipr
+*.iws
+
+# vscode
+
+.settings/
+.vscode/
+bin/
+.classpath
+.project
+
+# fabric
+
+run/

+ 77 - 0
build.gradle

@@ -0,0 +1,77 @@
+plugins {
+	id 'fabric-loom' version '0.2.7-SNAPSHOT'
+	id 'maven-publish'
+}
+
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
+
+archivesBaseName = project.archives_base_name
+version = project.mod_version
+group = project.maven_group
+
+dependencies {
+	//to change the versions see the gradle.properties file
+	minecraft "com.mojang:minecraft:${project.minecraft_version}"
+	mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
+	modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
+
+	modImplementation "me.shedaniel.cloth:config-2:${project.cloth_config_version}"
+
+	compileOnly "org.projectlombok:lombok:${project.lombok_version}"
+	annotationProcessor "org.projectlombok:lombok:${project.lombok_version}"
+	compileOnly "org.jetbrains:annotations:${project.jetbrains_annotations_version}"
+}
+
+processResources {
+	inputs.property "version", project.version
+
+	from(sourceSets.main.resources.srcDirs) {
+		include "fabric.mod.json"
+		expand "version": project.version
+	}
+
+	from(sourceSets.main.resources.srcDirs) {
+		exclude "fabric.mod.json"
+	}
+}
+
+// ensure that the encoding is set to UTF-8, no matter what the system default is
+// this fixes some edge cases with special characters not displaying correctly
+// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html
+tasks.withType(JavaCompile) {
+	options.encoding = "UTF-8"
+}
+
+// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
+// if it is present.
+// If you remove this task, sources will not be generated.
+task sourcesJar(type: Jar, dependsOn: classes) {
+	classifier = "sources"
+	from sourceSets.main.allSource
+}
+
+jar {
+	from "LICENSE"
+}
+
+// configure the maven publication
+publishing {
+	publications {
+		mavenJava(MavenPublication) {
+			// add all the jars that should be included when publishing to maven
+			artifact(remapJar) {
+				builtBy remapJar
+			}
+			artifact(sourcesJar) {
+				builtBy remapSourcesJar
+			}
+		}
+	}
+
+	// select the repositories you want to publish to
+	repositories {
+		// uncomment to publish to the local maven
+		// mavenLocal()
+	}
+}

+ 20 - 0
gradle.properties

@@ -0,0 +1,20 @@
+# Done to increase the memory available to gradle.
+org.gradle.jvmargs=-Xmx1G
+
+# Fabric Properties
+	# check these on https://fabricmc.net/use
+	minecraft_version=1.15.2
+	yarn_mappings=1.15.2+build.15
+	loader_version=0.8.2+build.194
+
+# Mod Properties
+	mod_version = 0.1.0
+	maven_group = me.lortseam
+	archives_base_name = completeconfig
+
+# Dependencies
+	# currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api
+	fabric_version=0.5.1+build.294-1.15
+	cloth_config_version=2.11.2
+	lombok_version=1.18.12
+	jetbrains_annotations_version=19.0.0

BIN
gradle/wrapper/gradle-wrapper.jar


+ 5 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 183 - 0
gradlew

@@ -0,0 +1,183 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 103 - 0
gradlew.bat

@@ -0,0 +1,103 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 10 - 0
settings.gradle

@@ -0,0 +1,10 @@
+pluginManagement {
+    repositories {
+        jcenter()
+        maven {
+            name = 'Fabric'
+            url = 'https://maven.fabricmc.net/'
+        }
+        gradlePluginPortal()
+    }
+}

+ 24 - 0
src/main/java/me/lortseam/completeconfig/CompleteConfig.java

@@ -0,0 +1,24 @@
+package me.lortseam.completeconfig;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+public class CompleteConfig {
+
+    private static final Set<ConfigManager> managers = new HashSet<>();
+
+    public static ConfigManager register(String modID) {
+        if (getManager(modID).isPresent()) {
+            throw new RuntimeException("There is already registered a manager for this mod ID!");
+        }
+        ConfigManager manager = new ConfigManager(modID);
+        managers.add(manager);
+        return manager;
+    }
+
+    public static Optional<ConfigManager> getManager(String modID) {
+        return managers.stream().filter(manager -> manager.getModID().equals(modID)).findAny();
+    }
+
+}

+ 316 - 0
src/main/java/me/lortseam/completeconfig/ConfigManager.java

@@ -0,0 +1,316 @@
+package me.lortseam.completeconfig;
+
+import com.google.common.base.CaseFormat;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import lombok.AccessLevel;
+import lombok.Getter;
+import me.lortseam.completeconfig.api.ConfigCategory;
+import me.lortseam.completeconfig.api.ConfigEntry;
+import me.lortseam.completeconfig.api.ConfigEntryContainer;
+import me.lortseam.completeconfig.api.ConfigEntrySaveConsumer;
+import me.lortseam.completeconfig.collection.Collection;
+import me.lortseam.completeconfig.entry.BoundedEntry;
+import me.lortseam.completeconfig.entry.Entry;
+import me.lortseam.completeconfig.saveconsumer.SaveConsumer;
+import me.lortseam.completeconfig.serialization.CollectionsDeserializer;
+import me.lortseam.completeconfig.serialization.EntrySerializer;
+import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
+import me.shedaniel.clothconfig2.api.ConfigBuilder;
+import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
+import me.shedaniel.clothconfig2.impl.builders.FieldBuilder;
+import me.shedaniel.clothconfig2.impl.builders.SubCategoryBuilder;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.resource.language.I18n;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.*;
+import java.lang.reflect.Modifier;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.stream.Collectors;
+
+//TODO: Sortierung der Categories, Subcategories und Entrys (Nach Registrierungsreihenfolge oder Alphabet; allgemein und für jeden Container einzeln?)
+public class ConfigManager {
+
+    @Getter(AccessLevel.PACKAGE)
+    private final String modID;
+    private final Path jsonPath;
+    private final LinkedHashMap<String, Collection> config = new LinkedHashMap<>();
+    private final JsonElement json;
+    private final Set<SaveConsumer> pendingSaveConsumers = new HashSet<>();
+
+    ConfigManager(String modID) {
+        this.modID = modID;
+        jsonPath = Paths.get(FabricLoader.getInstance().getConfigDirectory().toPath().toString(), modID + ".json");
+        json = load();
+    }
+
+    private JsonElement load() {
+        if(!Files.exists(jsonPath)) return JsonNull.INSTANCE;
+        try {
+            return new Gson().fromJson(new FileReader(jsonPath.toString()), JsonElement.class);
+        } catch (FileNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private LinkedHashMap<String, Entry> getContainerEntries(ConfigEntryContainer container) {
+        LinkedHashMap<String, Entry> entries = new LinkedHashMap<>();
+        Class clazz = container.getClass();
+        while (clazz != null) {
+            Set<SaveConsumer> saveConsumers = new HashSet<>();
+            Iterator<SaveConsumer> iter = pendingSaveConsumers.iterator();
+            while (iter.hasNext()) {
+                SaveConsumer saveConsumer = iter.next();
+                if (saveConsumer.getFieldClass() == clazz) {
+                    saveConsumers.add(saveConsumer);
+                    pendingSaveConsumers.remove(saveConsumer);
+                }
+            }
+            Arrays.stream(clazz.getDeclaredMethods()).filter(method -> !Modifier.isStatic(method.getModifiers()) && method.isAnnotationPresent(ConfigEntrySaveConsumer.class)).forEach(method -> {
+                ConfigEntrySaveConsumer saveConsumerAnnotation = method.getDeclaredAnnotation(ConfigEntrySaveConsumer.class);
+                String fieldName = saveConsumerAnnotation.value();
+                Class<? extends ConfigEntryContainer> fieldClass = saveConsumerAnnotation.container();
+                if (fieldClass == ConfigEntryContainer.class) {
+                    saveConsumers.add(new SaveConsumer(method, container, fieldName));
+                } else {
+                    Map<String, Entry> fieldClassEntries = findEntries(config, fieldClass);
+                    if (fieldClassEntries.isEmpty()) {
+                        pendingSaveConsumers.add(new SaveConsumer(method, container, fieldName, fieldClass));
+                    } else {
+                        Entry entry = fieldClassEntries.get(fieldName);
+                        if (entry == null) {
+                            throw new RuntimeException("Could not find field " + fieldName + " in " + fieldClass + " of save consumer method " + method);
+                        }
+                        entry.addSaveConsumer(method, container);
+                    }
+                }
+            });
+            LinkedHashMap<String, Entry> clazzEntries = new LinkedHashMap<>();
+            //TODO: Warnung in der Konsole anzeigen, wenn Container POJO ist (isConfigPOJO() == true) aber trotzdem ein Feld mit @ConfigEntry annotiert ist, oder wenn Container kein POJO ist und Feld mit @ConfigEntry.Ignore annotiert ist
+            Arrays.stream(clazz.getDeclaredFields()).filter(field -> !Modifier.isStatic(field.getModifiers()) && (container.isConfigPOJO() && !field.isAnnotationPresent(ConfigEntry.Ignore.class) || field.isAnnotationPresent(ConfigEntry.class))).forEach(field -> {
+                if (!field.isAccessible()) {
+                    field.setAccessible(true);
+                }
+                String translationKey = null;
+                if (field.isAnnotationPresent(ConfigEntry.TranslationKey.class)) {
+                    translationKey = field.getDeclaredAnnotation(ConfigEntry.TranslationKey.class).value();
+                    if (StringUtils.isBlank(translationKey)) {
+                        throw new RuntimeException("Translation key for entry field " + field + " was blank!");
+                    }
+                }
+                Entry entry;
+                if (field.isAnnotationPresent(ConfigEntry.Integer.Bound.class)) {
+                    if (field.getType() != Integer.TYPE) {
+                        throw new RuntimeException("Cannot apply integer bound to non integer field " + field + "!");
+                    }
+                    ConfigEntry.Integer.Bound bound = field.getDeclaredAnnotation(ConfigEntry.Integer.Bound.class);
+                    entry = new BoundedEntry<>(field, Integer.TYPE, container, translationKey, bound.min(), bound.max());
+                } else if (field.isAnnotationPresent(ConfigEntry.Long.Bound.class)) {
+                    if (field.getType() != Long.TYPE) {
+                        throw new RuntimeException("Cannot apply long bound to non long field " + field + "!");
+                    }
+                    ConfigEntry.Long.Bound bound = field.getDeclaredAnnotation(ConfigEntry.Long.Bound.class);
+                    entry = new BoundedEntry<>(field, Long.TYPE, container, translationKey, bound.min(), bound.max());
+                } else {
+                    entry = new Entry<>(field, field.getType(), container, translationKey);
+                }
+                String fieldName = field.getName();
+                saveConsumers.removeIf(saveConsumer -> {
+                    if (!saveConsumer.getFieldName().equals(fieldName)) {
+                        return false;
+                    }
+                    entry.addSaveConsumer(saveConsumer.getMethod(), saveConsumer.getParentObject());
+                    return true;
+                });
+                clazzEntries.put(fieldName, entry);
+            });
+            if (!saveConsumers.isEmpty()) {
+                SaveConsumer saveConsumer = saveConsumers.iterator().next();
+                throw new RuntimeException("Could not find field " + saveConsumer.getFieldName() + " of save consumer method " + saveConsumer.getMethod());
+            }
+            clazzEntries.putAll(entries);
+            entries = clazzEntries;
+            clazz = clazz.getSuperclass();
+        }
+        return entries;
+    }
+
+    private Map<String, Entry> findEntries(LinkedHashMap<String, Collection> collections, Class<? extends ConfigEntryContainer> parentClass) {
+        Map<String, Entry> entries = new HashMap<>();
+        for (Collection collection : collections.values()) {
+            entries.putAll(collection.getEntries().entrySet().stream().filter(entry -> entry.getValue().getParentObject().getClass() == parentClass).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
+            entries.putAll(findEntries(collection.getCollections(), parentClass));
+        }
+        return entries;
+    }
+
+    public void register(ConfigCategory... categories) {
+        Arrays.stream(categories).forEach(category -> registerCategory(config, category, true));
+    }
+
+    private void registerCategory(LinkedHashMap<String, Collection> configMap, ConfigCategory category, boolean applyJson) {
+        String categoryID = category.getConfigCategoryID();
+        if (StringUtils.isBlank(categoryID)) {
+            throw new RuntimeException("Category ID of " + category.getClass() + " was null or blank!");
+        }
+        if (configMap.containsKey(categoryID)) {
+            throw new RuntimeException("Duplicate category ID found: " + categoryID);
+        }
+        Collection collection = new me.lortseam.completeconfig.collection.Collection();
+        configMap.put(categoryID, collection);
+        registerContainer(collection, category);
+        if (collection.getEntries().isEmpty() && collection.getCollections().isEmpty()) {
+            configMap.remove(categoryID);
+            return;
+        }
+        if (applyJson) {
+            new GsonBuilder()
+                    .registerTypeAdapter(CollectionsDeserializer.TYPE, new CollectionsDeserializer(configMap, categoryID))
+                    .create()
+                    .fromJson(json, CollectionsDeserializer.TYPE);
+        }
+    }
+
+    private void registerContainer(Collection collection, ConfigEntryContainer container) {
+        if (!findEntries(config, container.getClass()).isEmpty()) {
+            throw new RuntimeException("An instance of " + container.getClass() + " is already registered!");
+        }
+        collection.getEntries().putAll(getContainerEntries(container));
+        ConfigEntryContainer[] containers = container.getConfigEntryContainers();
+        if (containers != null) {
+            for (ConfigEntryContainer c : containers) {
+                if (c instanceof ConfigCategory) {
+                    registerCategory(collection.getCollections(), (ConfigCategory) c, false);
+                } else {
+                    registerContainer(collection, c);
+                    collection.getEntries().putAll(getContainerEntries(c));
+                }
+            }
+        }
+    }
+
+    private String joinIDs(String... ids) {
+        return String.join(".", ids);
+    }
+
+    private String buildTranslationKey(String... ids) {
+        return joinIDs("config", modID, joinIDs(ids));
+    }
+
+    public Screen getConfigScreen(Screen parentScreen) {
+        ConfigBuilder builder = ConfigBuilder
+                .create()
+                .setParentScreen(parentScreen)
+                .setTitle(buildTranslationKey("title"))
+                .setSavingRunnable(this::save);
+        config.forEach((categoryID, category) -> {
+            me.shedaniel.clothconfig2.api.ConfigCategory configCategory = builder.getOrCreateCategory(buildTranslationKey(categoryID));
+            for (AbstractConfigListEntry entry : buildCollection(categoryID, category)) {
+                configCategory.addEntry(entry);
+            }
+        });
+        return builder.build();
+    }
+
+    private List<AbstractConfigListEntry> buildCollection(String parentID, Collection collection) {
+        List<AbstractConfigListEntry> list = new ArrayList<>();
+        collection.getEntries().forEach((entryID, entry) -> {
+            ConfigEntryBuilder builder = ConfigEntryBuilder.create();
+            FieldBuilder fieldBuilder = null;
+            Object value = entry.getValue();
+            String translationKey = entry.getTranslationKey() != null ? buildTranslationKey(entry.getTranslationKey()) : buildTranslationKey(parentID, entryID);
+            if (value instanceof Enum) {
+                Enum enumValue = (Enum) value;
+                fieldBuilder = builder.startEnumSelector(translationKey, enumValue.getDeclaringClass(), enumValue)
+                        .setDefaultValue((Enum) entry.getDefaultValue())
+                        .setEnumNameProvider(e -> I18n.translate(joinIDs(translationKey, CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, ((Enum) e).name()))))
+                        .setSaveConsumer(entry::setValue);
+            } else if (value instanceof Boolean) {
+                fieldBuilder = builder.startBooleanToggle(translationKey, (Boolean) value)
+                        .setDefaultValue((Boolean) entry.getDefaultValue())
+                        .setSaveConsumer(entry::setValue);
+            } else if (value instanceof Integer) {
+                if (entry instanceof BoundedEntry) {
+                    BoundedEntry<Integer> boundedIntEntry = (BoundedEntry<Integer>) entry;
+                    fieldBuilder = builder.startIntSlider(translationKey, (Integer) value, boundedIntEntry.getMin(), boundedIntEntry.getMax())
+                            .setDefaultValue(boundedIntEntry.getDefaultValue())
+                            .setSaveConsumer(boundedIntEntry::setValue);
+                } else {
+                    fieldBuilder = builder.startIntField(translationKey, (Integer) value)
+                            .setDefaultValue((Integer) entry.getDefaultValue())
+                            .setSaveConsumer(entry::setValue);
+                }
+            } else if (value instanceof Long) {
+                if (entry instanceof BoundedEntry) {
+                    BoundedEntry<Long> boundedLongEntry = (BoundedEntry<Long>) entry;
+                    fieldBuilder = builder.startLongSlider(translationKey, (Long) value, boundedLongEntry.getMin(), boundedLongEntry.getMax())
+                            .setDefaultValue(boundedLongEntry.getDefaultValue())
+                            .setSaveConsumer(boundedLongEntry::setValue);
+                } else {
+                    fieldBuilder = builder.startIntField(translationKey, (Integer) value)
+                            .setDefaultValue((Integer) entry.getDefaultValue())
+                            .setSaveConsumer(entry::setValue);
+                }
+            } else if (value instanceof Double) {
+                fieldBuilder = builder.startDoubleField(translationKey, (Double) value)
+                        .setDefaultValue((Double) entry.getDefaultValue())
+                        .setSaveConsumer(entry::setValue);
+            } else if (value instanceof Float) {
+                fieldBuilder = builder.startFloatField(translationKey, (Float) value)
+                        .setDefaultValue((Float) entry.getDefaultValue())
+                        .setSaveConsumer(entry::setValue);
+            }
+            if (fieldBuilder == null) {
+                throw new RuntimeException("Unable to create config entry field for type " + value.getClass());
+            }
+            list.add(fieldBuilder.build());
+        });
+        collection.getCollections().forEach((subcategoryID, c) -> {
+            String id = joinIDs(parentID, subcategoryID);
+            SubCategoryBuilder subBuilder = ConfigEntryBuilder.create().startSubCategory(buildTranslationKey(id));
+            subBuilder.addAll(buildCollection(id, c));
+            list.add(subBuilder.build());
+        });
+        return list;
+    }
+
+    private void save() {
+        if (!Files.exists(jsonPath)) {
+            try {
+                Files.createDirectories(jsonPath.getParent());
+                Files.createFile(jsonPath);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+        try(Writer writer = new FileWriter(jsonPath.toString())) {
+            new GsonBuilder()
+                    .registerTypeAdapter(EntrySerializer.TYPE, new EntrySerializer())
+                    .setPrettyPrinting()
+                    .create()
+                    .toJson(config, writer);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void refreshCollections(LinkedHashMap<String, Collection> collections) {
+        collections.values().forEach(collection -> {
+            collection.getEntries().values().forEach(Entry::getValue);
+            refreshCollections(collection.getCollections());
+        });
+    }
+
+    public void refreshAndSave() {
+        refreshCollections(config);
+        save();
+    }
+
+}

+ 12 - 0
src/main/java/me/lortseam/completeconfig/api/ConfigCategory.java

@@ -0,0 +1,12 @@
+package me.lortseam.completeconfig.api;
+
+import com.google.common.base.CaseFormat;
+import org.jetbrains.annotations.NotNull;
+
+public interface ConfigCategory extends ConfigEntryContainer {
+
+    default @NotNull String getConfigCategoryID() {
+        return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, getClass().getSimpleName());
+    }
+
+}

+ 60 - 0
src/main/java/me/lortseam/completeconfig/api/ConfigEntry.java

@@ -0,0 +1,60 @@
+package me.lortseam.completeconfig.api;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+//TODO: Auch Container und Category als Feld Typ zulassen und diese dann registrieren
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ConfigEntry {
+
+    @Target(ElementType.FIELD)
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface TranslationKey {
+
+        String value();
+
+    }
+
+    @NoArgsConstructor(access = AccessLevel.PRIVATE)
+    class Integer {
+
+        @Target(ElementType.FIELD)
+        @Retention(RetentionPolicy.RUNTIME)
+        public @interface Bound {
+
+            int min();
+
+            int max();
+
+        }
+
+    }
+
+    @NoArgsConstructor(access = AccessLevel.PRIVATE)
+    class Long {
+
+        @Target(ElementType.FIELD)
+        @Retention(RetentionPolicy.RUNTIME)
+        public @interface Bound {
+
+            long min();
+
+            long max();
+
+        }
+
+    }
+
+    @Target(ElementType.FIELD)
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface Ignore {
+
+    }
+
+}

+ 13 - 0
src/main/java/me/lortseam/completeconfig/api/ConfigEntryContainer.java

@@ -0,0 +1,13 @@
+package me.lortseam.completeconfig.api;
+
+public interface ConfigEntryContainer {
+
+    default ConfigEntryContainer[] getConfigEntryContainers() {
+        return null;
+    }
+
+    default boolean isConfigPOJO() {
+        return false;
+    }
+    
+}

+ 16 - 0
src/main/java/me/lortseam/completeconfig/api/ConfigEntrySaveConsumer.java

@@ -0,0 +1,16 @@
+package me.lortseam.completeconfig.api;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ConfigEntrySaveConsumer {
+
+    String value();
+
+    Class<? extends ConfigEntryContainer> container() default ConfigEntryContainer.class;
+
+}

+ 18 - 0
src/main/java/me/lortseam/completeconfig/collection/Collection.java

@@ -0,0 +1,18 @@
+package me.lortseam.completeconfig.collection;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import me.lortseam.completeconfig.entry.Entry;
+
+import java.util.LinkedHashMap;
+
+@NoArgsConstructor
+public class Collection {
+
+    //TODO: Entries und Collections werden auch in der Config Json Datei gespeichtert, wenn sie leer sind
+    @Getter
+    private final LinkedHashMap<String, Entry> entries = new LinkedHashMap<>();
+    @Getter
+    private final LinkedHashMap<String, Collection> collections = new LinkedHashMap<>();
+
+}

+ 20 - 0
src/main/java/me/lortseam/completeconfig/entry/BoundedEntry.java

@@ -0,0 +1,20 @@
+package me.lortseam.completeconfig.entry;
+
+import lombok.Getter;
+import me.lortseam.completeconfig.api.ConfigEntryContainer;
+
+import java.lang.reflect.Field;
+
+//TODO: Bound auch beim Einlesen aus JSON beachten
+public class BoundedEntry<T extends Number> extends Entry<T> {
+
+    @Getter
+    private final T min, max;
+
+    public BoundedEntry(Field field, Class<T> type, ConfigEntryContainer parentObject, String translationKey, T min, T max) {
+        super(field, type, parentObject, translationKey);
+        this.min = min;
+        this.max = max;
+    }
+
+}

+ 70 - 0
src/main/java/me/lortseam/completeconfig/entry/Entry.java

@@ -0,0 +1,70 @@
+package me.lortseam.completeconfig.entry;
+
+import lombok.Getter;
+import me.lortseam.completeconfig.api.ConfigEntryContainer;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Entry<T> {
+
+    private final Field field;
+    @Getter
+    private final Class<T> type;
+    @Getter
+    private final ConfigEntryContainer parentObject;
+    @Getter
+    private final T defaultValue;
+    @Getter
+    private final String translationKey;
+    private final Map<Method, ConfigEntryContainer> saveConsumers = new HashMap<>();
+
+    public Entry(Field field, Class<T> type, ConfigEntryContainer parentObject, String translationKey) {
+        this.field = field;
+        this.type = type;
+        this.parentObject = parentObject;
+        defaultValue = getValue();
+        this.translationKey = translationKey;
+    }
+
+    public T getValue() {
+        try {
+            return (T) field.get(parentObject);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void setValue(T value) {
+        if (saveConsumers.values().stream().noneMatch(parentObject -> parentObject == this.parentObject)) {
+            try {
+                field.set(parentObject, value);
+            } catch (IllegalAccessException e) {
+                throw new RuntimeException(e);
+            }
+        }
+        if (!saveConsumers.isEmpty()) {
+            saveConsumers.forEach((method, parentObject) -> {
+                try {
+                    method.invoke(parentObject, value);
+                } catch (IllegalAccessException | InvocationTargetException e) {
+                    throw new RuntimeException(e);
+                }
+            });
+        }
+    }
+
+    public void addSaveConsumer(Method method, ConfigEntryContainer parentObject) {
+        if (method.getParameterCount() != 1 || method.getParameterTypes()[0] != type) {
+            throw new IllegalArgumentException("Save consumer method " + method + " has wrong parameter type(s)!");
+        }
+        if (!method.isAccessible()) {
+            method.setAccessible(true);
+        }
+        saveConsumers.put(method, parentObject);
+    }
+
+}

+ 28 - 0
src/main/java/me/lortseam/completeconfig/saveconsumer/SaveConsumer.java

@@ -0,0 +1,28 @@
+package me.lortseam.completeconfig.saveconsumer;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import me.lortseam.completeconfig.api.ConfigEntryContainer;
+
+import java.lang.reflect.Method;
+
+@RequiredArgsConstructor
+public class SaveConsumer {
+
+    @Getter
+    private final Method method;
+    @Getter
+    private final ConfigEntryContainer parentObject;
+    @Getter
+    private final String fieldName;
+    @Getter
+    private final Class<? extends ConfigEntryContainer> fieldClass;
+
+    public SaveConsumer(Method method, ConfigEntryContainer parentObject, String fieldName) {
+        this.method = method;
+        this.parentObject = parentObject;
+        this.fieldName = fieldName;
+        this.fieldClass = parentObject.getClass();
+    }
+
+}

+ 49 - 0
src/main/java/me/lortseam/completeconfig/serialization/CollectionsDeserializer.java

@@ -0,0 +1,49 @@
+package me.lortseam.completeconfig.serialization;
+
+import com.google.gson.*;
+import com.google.gson.reflect.TypeToken;
+import me.lortseam.completeconfig.collection.Collection;
+
+import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+
+public class CollectionsDeserializer implements JsonDeserializer<LinkedHashMap<String, Collection>> {
+
+    public static final Type TYPE = new TypeToken<LinkedHashMap<String, Collection>>() {}.getType();
+
+    private final LinkedHashMap<String, Collection> configMap;
+    private final String collectionID;
+
+    public CollectionsDeserializer(LinkedHashMap<String, Collection> configMap, String collectionID) {
+        this.configMap = configMap;
+        this.collectionID = collectionID;
+    }
+
+    private CollectionsDeserializer(LinkedHashMap<String, Collection> configMap) {
+        this(configMap, null);
+    }
+
+    @Override
+    public LinkedHashMap<String, Collection> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+        LinkedHashMap<String, JsonElement> map = new Gson().fromJson(json, new TypeToken<LinkedHashMap<String, JsonElement>>() {}.getType());
+        if (collectionID == null) {
+            map.forEach(this::deserialize);
+        } else {
+            deserialize(collectionID, map.get(collectionID));
+        }
+        return null;
+    }
+
+    private void deserialize(String collectionID, JsonElement element) {
+        Collection collection = configMap.get(collectionID);
+        if (collection == null) {
+            return;
+        }
+        new GsonBuilder()
+                .registerTypeAdapter(CollectionsDeserializer.TYPE, new CollectionsDeserializer(collection.getCollections()))
+                .registerTypeAdapter(EntriesDeserializer.TYPE, new EntriesDeserializer(collection.getEntries()))
+                .create()
+                .fromJson(element, Collection.class);
+    }
+
+}

+ 36 - 0
src/main/java/me/lortseam/completeconfig/serialization/EntriesDeserializer.java

@@ -0,0 +1,36 @@
+package me.lortseam.completeconfig.serialization;
+
+import com.google.gson.*;
+import com.google.gson.reflect.TypeToken;
+import me.lortseam.completeconfig.entry.Entry;
+
+import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+
+public class EntriesDeserializer implements JsonDeserializer<LinkedHashMap<String, Entry>> {
+
+    public static final Type TYPE = new TypeToken<LinkedHashMap<String, Entry>>() {}.getType();
+
+    private final LinkedHashMap<String, Entry> configMap;
+
+    public EntriesDeserializer(LinkedHashMap<String, Entry> configMap) {
+        this.configMap = configMap;
+    }
+
+    @Override
+    public LinkedHashMap<String, Entry> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+        LinkedHashMap<String, JsonElement> map = new Gson().fromJson(json, new TypeToken<LinkedHashMap<String, JsonElement>>() {}.getType());
+        map.forEach((entryID, element) -> {
+            Entry<?> entry = configMap.get(entryID);
+            if (entry == null) {
+                return;
+            }
+            new GsonBuilder()
+                    .registerTypeAdapter(EntryDeserializer.TYPE, new EntryDeserializer<>(entry))
+                    .create()
+                    .fromJson(element, new TypeToken<Entry<?>>() {}.getType());
+        });
+        return null;
+    }
+
+}

+ 28 - 0
src/main/java/me/lortseam/completeconfig/serialization/EntryDeserializer.java

@@ -0,0 +1,28 @@
+package me.lortseam.completeconfig.serialization;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.reflect.TypeToken;
+import me.lortseam.completeconfig.entry.Entry;
+
+import java.lang.reflect.Type;
+
+public class EntryDeserializer<T> implements JsonDeserializer<Entry<T>> {
+
+    public static final Type TYPE = new TypeToken<Entry<?>>() {}.getType();
+
+    private final Entry<T> configEntry;
+
+    public EntryDeserializer(Entry<T> configEntry) {
+        this.configEntry = configEntry;
+    }
+
+    @Override
+    public Entry<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+        configEntry.setValue(context.deserialize(json, configEntry.getType()));
+        return configEntry;
+    }
+
+}

+ 22 - 0
src/main/java/me/lortseam/completeconfig/serialization/EntrySerializer.java

@@ -0,0 +1,22 @@
+package me.lortseam.completeconfig.serialization;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import me.lortseam.completeconfig.entry.Entry;
+
+import java.lang.reflect.Type;
+
+@NoArgsConstructor
+public class EntrySerializer implements JsonSerializer<Entry> {
+
+    public static final Type TYPE = Entry.class;
+
+    @Override
+    public JsonElement serialize(Entry entry, Type typeOfSrc, JsonSerializationContext context) {
+        return context.serialize(entry.getValue());
+    }
+
+}

+ 17 - 0
src/main/resources/fabric.mod.json

@@ -0,0 +1,17 @@
+{
+  "schemaVersion": 1,
+  "id": "completeconfig",
+  "version": "${version}",
+
+  "name": "CompleteConfig",
+  "authors": [
+    "Lortseam"
+  ],
+
+  "environment": "client",
+
+  "depends": {
+    "fabricloader": ">=0.7.4",
+    "minecraft": "1.15.x"
+  }
+}