diff options
| author | nea <nea@nea.moe> | 2023-04-10 20:40:12 +0200 |
|---|---|---|
| committer | nea <nea@nea.moe> | 2023-04-10 20:40:12 +0200 |
| commit | ecdef28c57b6b221a7c41d9fab5c474de2a4d584 (patch) | |
| tree | 0c8b97c3d6009682c4e94c7f1ad9d663e2d4325b | |
| parent | e2144d17be87f279d47b8d0d1e7bcff05ad2ff35 (diff) | |
| download | SkyHanni-ecdef28c57b6b221a7c41d9fab5c474de2a4d584.tar.gz SkyHanni-ecdef28c57b6b221a7c41d9fab5c474de2a4d584.tar.bz2 SkyHanni-ecdef28c57b6b221a7c41d9fab5c474de2a4d584.zip | |
wip
5 files changed, 368 insertions, 18 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.java b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.java index 9a4990595..f0f5b243d 100644 --- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.java +++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.java @@ -84,8 +84,8 @@ public class SkyHanniMod { public static RepoManager repo; public static ConfigManager configManager; private static Logger logger; - public static org.slf4j.Logger getLogger(String name) { - return org.slf4j.LoggerFactory.getLogger("SkyHanni." + name); + public static Logger getLogger(String name) { + return LogManager.getLogger("SkyHanni." + name); } public static List<Object> modules = new ArrayList<>(); diff --git a/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt b/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt index dd10d8482..5e0e61b8e 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt @@ -1,14 +1,17 @@ package at.hannibal2.skyhanni.config import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.migration.MigratingConfigLoader import at.hannibal2.skyhanni.events.ConfigLoadEvent import at.hannibal2.skyhanni.features.garden.CropType import com.google.gson.GsonBuilder +import com.google.gson.JsonElement import io.github.moulberry.moulconfig.observer.PropertyTypeAdapterFactory import io.github.moulberry.moulconfig.processor.BuiltinMoulConfigGuis import io.github.moulberry.moulconfig.processor.ConfigProcessorDriver import io.github.moulberry.moulconfig.processor.MoulConfigProcessor import java.io.* +import java.lang.RuntimeException import java.nio.charset.StandardCharsets class ConfigManager { @@ -25,6 +28,20 @@ class ConfigManager { private var configFile: File? = null lateinit var processor: MoulConfigProcessor<Features> + fun loadConfig(file: File): Features { + val x = MigratingConfigLoader.loadConfig( + gson.fromJson(file.readText(), JsonElement::class.java), + Features::class.java + ) + return when (x) { + MigratingConfigLoader.LoadResult.UseDefault -> Features() + is MigratingConfigLoader.LoadResult.Instance -> x.instance!! + is MigratingConfigLoader.LoadResult.Failure -> throw RuntimeException("Failed to load field ${x.field}", x.exception) + MigratingConfigLoader.LoadResult.Invalid -> error("LoadResult.Invalid returned directly?") + } + } + + fun firstLoad() { try { configDirectory.mkdir() @@ -37,24 +54,9 @@ class ConfigManager { if (configFile!!.exists()) { try { - val inputStreamReader = InputStreamReader(FileInputStream(configFile!!), StandardCharsets.UTF_8) - val bufferedReader = BufferedReader(inputStreamReader) - val builder = StringBuilder() - for (line in bufferedReader.lines()) { - val result = fixConfig(line) - builder.append(result) - builder.append("\n") - } - - - SkyHanniMod.feature = gson.fromJson( - builder.toString(), - Features::class.java - ) + SkyHanniMod.feature = loadConfig(configFile!!) logger.info("Loaded config from file") } catch (e: Exception) { - println("config error") - e.printStackTrace() val backupFile = configFile!!.resolveSibling("config-${System.currentTimeMillis()}-backup.json") logger.error( "Exception while reading $configFile. Will load blank config and save backup to $backupFile", diff --git a/src/main/java/at/hannibal2/skyhanni/config/core/config/Position.java b/src/main/java/at/hannibal2/skyhanni/config/core/config/Position.java index ec1f4fbfc..7b161b5d5 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/core/config/Position.java +++ b/src/main/java/at/hannibal2/skyhanni/config/core/config/Position.java @@ -36,6 +36,7 @@ public class Position { private boolean clicked = false; public String internalName = null; + Position(){} public Position(int x, int y) { this(x, y, false, false); } diff --git a/src/main/java/at/hannibal2/skyhanni/config/migration/MigratingConfigLoader.kt b/src/main/java/at/hannibal2/skyhanni/config/migration/MigratingConfigLoader.kt new file mode 100644 index 000000000..0ef989d63 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/config/migration/MigratingConfigLoader.kt @@ -0,0 +1,268 @@ +package at.hannibal2.skyhanni.config.migration + +import at.hannibal2.skyhanni.config.migration.MigratingConfigLoader.LoadingAdapter +import at.hannibal2.skyhanni.events.ConfigMigrationEvent +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.annotations.Expose +import io.github.moulberry.moulconfig.observer.Property +import net.minecraftforge.common.MinecraftForge +import java.lang.reflect.* + +val <T> Class<T>.allFields: List<Field> + get() = this.declaredFields.toList() + (this.superclass?.allFields ?: listOf()) +val <T> Class<T>.allAccessibleFields: List<Field> + get() = allFields.also { it.forEach { it.isAccessible = true } } + +val Type.nonGeneric: Class<*>? + get() = when (this) { + is ParameterizedType -> this.rawType.nonGeneric + is Class<*> -> this + is WildcardType -> this.upperBounds[0].nonGeneric + is TypeVariable<*> -> this.bounds[0].nonGeneric + else -> null + } + +object MigratingConfigLoader { + + interface ResolutionPath { + val parent: ResolutionPath? + val label: String + fun path(): String = parent?.path()?.let { "$it." } + label + + object Root : ResolutionPath { + override val parent: ResolutionPath? get() = null + override val label: String get() = "Root" + } + + data class FieldChild(val field: Field, override val parent: ResolutionPath) : ResolutionPath { + override val label: String = field.name + } + + data class IndirectChild(override val label: String, override val parent: ResolutionPath) : ResolutionPath + } + + sealed interface LoadResult<out T> { + fun <V> map(mapper: (T?) -> V?): LoadResult<V> { + if (this is Instance<T>) { + return Instance(mapper(this.instance)) + } + return this as LoadResult<V> + } + + fun or(other: LoadResult<@UnsafeVariance T>): LoadResult<T> + + data class Instance<T>(val instance: T?) : LoadResult<T> { + override fun or(other: LoadResult<T>): LoadResult<T> { + return this + } + } + + data class Failure(val exception: Throwable, val field: Field?) : LoadResult<Nothing> { + override fun or(other: LoadResult<Nothing>): LoadResult<Nothing> { + if (other is Invalid) return this + return other + } + } + + object UseDefault : LoadResult<Nothing> { + override fun or(other: LoadResult<Nothing>): LoadResult<Nothing> { + if (other is Instance) return other + return this + } + } + + object Invalid : LoadResult<Nothing> { + override fun or(other: LoadResult<Nothing>): LoadResult<Nothing> { + return other + } + } + } + + fun interface LoadingAdapter<T> { + fun adapt(field: Field?, hierarchy: List<JsonElement?>, type: Type): LoadResult<T> + } + + val adapters = listOf( + LoadingAdapter { field, hierarchy, type -> + val ng = type.nonGeneric ?: return@LoadingAdapter LoadResult.Invalid + if (!ng.isPrimitive) return@LoadingAdapter LoadResult.Invalid + loadElement( + field, hierarchy, mapOf( + java.lang.Integer.TYPE to java.lang.Integer::class.java, + java.lang.Boolean.TYPE to java.lang.Boolean::class.java, + java.lang.Short.TYPE to java.lang.Short::class.java, + java.lang.Float.TYPE to java.lang.Float::class.java, + java.lang.Double.TYPE to java.lang.Double::class.java, + java.lang.Long.TYPE to java.lang.Long::class.java, + java.lang.Byte.TYPE to java.lang.Byte::class.java, + java.lang.Character.TYPE to java.lang.Character::class.java, + )[ng]!! + ) + }, + directLoader { LoadResult.Instance(it.asString) }, + directLoader { LoadResult.Instance(it.asInt) }, + directLoader { LoadResult.Instance(it.asFloat) }, + directLoader { LoadResult.Instance(it.asLong) }, + directLoader { LoadResult.Instance(it.asDouble) }, + directLoader { LoadResult.Instance(it.asNumber) }, + directLoader { LoadResult.Instance(it.asBigDecimal) }, + directLoader { LoadResult.Instance(it.asBigInteger) }, + directLoader { LoadResult.Instance(it.asBoolean) }, + directLoader { LoadResult.Instance(it.asShort) }, + directLoader { LoadResult.Instance(it.asJsonObject) }, + directLoader { LoadResult.Instance(it.asJsonArray) }, + directLoader { LoadResult.Instance(it.asJsonPrimitive) }, + directLoader { LoadResult.Instance(it as JsonNull) }, + directLoader { LoadResult.Instance(it.asByte) }, + directLoader { LoadResult.Instance(it.asCharacter) }, + LoadingAdapter { field, hierarchy, type -> + val ng = type.nonGeneric ?: return@LoadingAdapter LoadResult.Invalid + if (ng != List::class.java) return@LoadingAdapter LoadResult.Invalid + type as ParameterizedType + val builder = mutableListOf<Any?>() + for (jsonElement in hierarchy.last()!!.asJsonArray) { + val x = loadElement(null, hierarchy + jsonElement, type.actualTypeArguments[0]) + if (x is LoadResult.Instance) { + builder.add(x.instance) + } else if (x is LoadResult.UseDefault) { + return@LoadingAdapter LoadResult.Failure( + RuntimeException("Cannot UseDefault for list element"), + field + ) + } else { + return@LoadingAdapter x + } + } + return@LoadingAdapter LoadResult.Instance(builder) + }, + LoadingAdapter { field, hierarchy, type -> + val ng = type.nonGeneric ?: return@LoadingAdapter LoadResult.Invalid + if (ng != Map::class.java) return@LoadingAdapter LoadResult.Invalid + type as ParameterizedType + val builder = mutableMapOf<Any?, Any?>() + for ((key, value) in (hierarchy.last()!! as JsonObject).entrySet()) { + val keyEl = loadElement(null, hierarchy + JsonPrimitive(key), type.actualTypeArguments[0]) + if (keyEl !is LoadResult.Instance<*>) { + if (keyEl is LoadResult.UseDefault) { + return@LoadingAdapter LoadResult.Failure( + RuntimeException("Cannot UseDefault for map key"), + field + ) + } + return@LoadingAdapter keyEl + } + val valueEl = loadElement(null, hierarchy + value, type.actualTypeArguments[0]) + if (valueEl !is LoadResult.Instance<*>) { + if (valueEl is LoadResult.UseDefault) { + return@LoadingAdapter LoadResult.Failure( + RuntimeException("Cannot UseDefault for map key"), + field + ) + } + return@LoadingAdapter keyEl + } + builder[keyEl.instance] = valueEl.instance + } + return@LoadingAdapter LoadResult.Instance(builder) + }, + LoadingAdapter { field, hierarchy, type -> + val ng = type.nonGeneric ?: return@LoadingAdapter LoadResult.Invalid + if (ng != Property::class.java) return@LoadingAdapter LoadResult.Invalid + if (type !is ParameterizedType) return@LoadingAdapter LoadResult.Invalid + loadElement( + ng.getDeclaredField("value").also { it.isAccessible = true }, + hierarchy + hierarchy.last(), + type.actualTypeArguments[0] + ).map { Property.of(it) } + }, + LoadingAdapter { field, hierarchy, type -> + val ng = type.nonGeneric ?: return@LoadingAdapter LoadResult.Invalid + if (!ng.isEnum) return@LoadingAdapter LoadResult.Invalid + ng as Class<out Enum<*>> + val el = hierarchy.last()!!.asJsonPrimitive + LoadResult.Instance(ng.enumConstants.find { if (el.isString) it.name == el.asString else it.ordinal == el.asInt }!!) + }, + LoadingAdapter(::loadClass), + ) + + + fun <T : Any> loadConfig(root: JsonElement, clazz: Class<T>): LoadResult<T> { + return loadElement(null, listOf(root), clazz) + } + + inline fun <reified T : Any> loader(crossinline block: (field: Field?, hierarchy: List<JsonElement?>) -> LoadResult<T>): LoadingAdapter<T> { + return LoadingAdapter { field, hierarchy, type -> + if (type.nonGeneric != T::class.java) LoadResult.Invalid + else block(field, hierarchy) + } + } + + inline fun <reified T : Any> directLoader(crossinline block: (element: JsonElement) -> LoadResult<T>): LoadingAdapter<T> { + return LoadingAdapter { field, hierarchy, type -> + if (type.nonGeneric != T::class.java) LoadResult.Invalid + else block(hierarchy.last()!!) + } + } + + + fun <T : Any> loadElement(field: Field?, hierarchy: List<JsonElement?>, clazz: Class<T>): LoadResult<T> { + return loadElement(field, hierarchy, clazz as Type) as LoadResult<T> + } + + fun loadElement(field: Field?, hierarchy: List<JsonElement?>, type: Type): LoadResult<Any?> { + var bestResult: LoadResult<Any?> = LoadResult.Invalid + for (adapter in adapters) { + val adapt = try { + adapter.adapt(field, hierarchy, type) + } catch (e: Exception) { + LoadResult.Failure(e, field) + } + if (adapt is LoadResult.Instance<*>) { + bestResult = adapt + break + } + bestResult = bestResult.or(adapt) + } + val event = ConfigMigrationEvent(field, hierarchy, type, bestResult).also { + try { + MinecraftForge.EVENT_BUS.post(it) + } catch (e: Throwable) { + it.value = it.value.or(LoadResult.Failure(e, field)) + } + } + if (event.value is LoadResult.Invalid) { + return LoadResult.Failure( + RuntimeException("Could not resolve a loader for ${type.typeName} (${type.nonGeneric})"), + field + ) + } + return event.value + } + + private fun loadClass(field: Field?, hierarchy: List<JsonElement?>, type: Type): LoadResult<Any?> { + val ng = type.nonGeneric ?: return LoadResult.Invalid + if (ng.isAnonymousClass || ng.isEnum || ng.isInterface || ng.isPrimitive) return LoadResult.Invalid + val instance = ng.getDeclaredConstructor().also { it.isAccessible = true }.newInstance() + require(ng.isInstance(instance)) // this is all we can check at runtime, sadly + val toBeFilled = ng.allAccessibleFields.filter { it.isAnnotationPresent(Expose::class.java) } + for (childField in toBeFilled) { + when ( + val value = loadElement( + childField, + hierarchy + hierarchy.last()?.asJsonObject?.get(childField.name), + childField.genericType + ) + ) { + is LoadResult.Instance -> childField.set(instance, value.instance) + LoadResult.UseDefault -> {} + is LoadResult.Failure -> return value + LoadResult.Invalid -> return value + } + } + return LoadResult.Instance(instance) + } + +}
\ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/events/ConfigMigrationEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/ConfigMigrationEvent.kt new file mode 100644 index 000000000..408eacf49 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/events/ConfigMigrationEvent.kt @@ -0,0 +1,79 @@ +package at.hannibal2.skyhanni.events + +import at.hannibal2.skyhanni.config.features.Garden +import at.hannibal2.skyhanni.config.migration.MigratingConfigLoader +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.lang.reflect.Field +import java.lang.reflect.Type +import kotlin.reflect.KProperty1 + + +data class ConfigMigrationEvent( + val property: Field?, + val objectHierarchy: List<JsonElement?>, + val type: Type, + var value: MigratingConfigLoader.LoadResult<*> +) : LorenzEvent() { + + + @SubscribeEvent + fun migrateSomething(event: ConfigMigrationEvent) { + migrate(Garden::composter) { + parent().parent().child("oldPropertyIdk") + } + } + + + fun use(value: Any?) { + this.value = MigratingConfigLoader.LoadResult.Instance(value) + } + + fun isFailing(): Boolean { + return value is MigratingConfigLoader.LoadResult.Failure || value is MigratingConfigLoader.LoadResult.Invalid + } + + fun useDefault() { + this.value = MigratingConfigLoader.LoadResult.UseDefault + } + + data class MigrateContext(val hierarchy: List<JsonElement?>) { + fun parent(n: Int = 1): MigrateContext = MigrateContext(hierarchy.dropLast(n)) + fun child(name: String) = MigrateContext(hierarchy + (hierarchy.last() as? JsonObject)?.get(name)) + fun root(): MigrateContext = MigrateContext(hierarchy.take(1)) + } + + inline fun <reified T, V> migrate(prop: KProperty1<T, V>, noinline block: MigrateContext.() -> MigrateContext) { + if (prop.name != property?.name) return + val field = try { + T::class.java.getDeclaredField(prop.name) + } catch (e: NoSuchFieldException) { + return + } + migrate(field, block) + } + + inline fun <reified T, V> migrate( + prop: KProperty1<T, V>, + noinline block: MigrateContext.() -> MigrateContext, + oldType: Type, + noinline mapper: (Any?) -> T? + ) { + if (prop.name != property?.name) return + val field = try { + T::class.java.getDeclaredField(prop.name) + } catch (e: NoSuchFieldException) { + return + } + migrate(field, block, oldType, mapper) + } + + fun migrate(prop: Field, block: MigrateContext.() -> MigrateContext) = migrate(prop, block, type) { it } + fun migrate(prop: Field, block: MigrateContext.() -> MigrateContext, oldType: Type, mapper: (Any?) -> Any?) { + if (prop != property) return + this.value = + MigratingConfigLoader.loadElement(null, MigrateContext(objectHierarchy).let(block).hierarchy, oldType) + .map(mapper) + } +}
\ No newline at end of file |
