From 8435dcfcf8c24eab31cfe6e55854c9fb000858ad Mon Sep 17 00:00:00 2001 From: awruff Date: Fri, 24 Jul 2026 02:46:43 -0400 Subject: [PATCH 1/4] feat: support yacl buttons (fixes #834) feat: add walksylib compat (fixes #828) feat: add midnightlib compat (fixes #791) feat: add button to open original screen on compat configs wip: add malilib compat --- .../main/kotlin/oneconfig-setup.gradle.kts | 3 + gradle/fabric.versions.toml | 2 + gradle/fabric/1.21.10.versions.toml | 4 +- gradle/fabric/1.21.11.versions.toml | 4 +- gradle/fabric/1.21.4.versions.toml | 4 +- gradle/fabric/1.21.5.versions.toml | 4 +- gradle/fabric/1.21.8.versions.toml | 4 +- gradle/fabric/26.1.versions.toml | 4 +- gradle/fabric/26.2.versions.toml | 4 +- .../internal/OneConfigMixinInit.java | 9 + .../compat/malilib/Mixin_GuiConfigsBase.java | 23 ++ .../midnightlib/Mixin_MidnightConfig.java | 23 ++ .../Mixin_WalksyLib_ModEntryPointList.java | 23 ++ .../oneconfig/internal/compat/CompatLoader.kt | 25 ++ .../internal/compat/MaLiLibCompat.kt | 222 +++++++++++++++ .../internal/compat/MidnightLibCompat.kt | 168 ++++++++++++ .../internal/compat/WalksyLibCompat.kt | 257 ++++++++++++++++++ .../oneconfig/internal/compat/YACLCompat.kt | 69 +++++ minecraft/stonecutter.gradle.kts | 3 + .../internal/ui/api/ConfigRegistry.kt | 1 + .../internal/ui/components/Header.kt | 37 +++ .../internal/ui/screens/ModConfig.kt | 4 +- .../oneconfig/internal/ui/shell/Data.kt | 2 + 23 files changed, 891 insertions(+), 8 deletions(-) create mode 100644 minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/malilib/Mixin_GuiConfigsBase.java create mode 100644 minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/midnightlib/Mixin_MidnightConfig.java create mode 100644 minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/walksylib/Mixin_WalksyLib_ModEntryPointList.java create mode 100644 minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MaLiLibCompat.kt create mode 100644 minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MidnightLibCompat.kt create mode 100644 minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/WalksyLibCompat.kt diff --git a/buildSrc/src/main/kotlin/oneconfig-setup.gradle.kts b/buildSrc/src/main/kotlin/oneconfig-setup.gradle.kts index c6c251398..b0f6fe9f5 100644 --- a/buildSrc/src/main/kotlin/oneconfig-setup.gradle.kts +++ b/buildSrc/src/main/kotlin/oneconfig-setup.gradle.kts @@ -38,6 +38,9 @@ repositories { maven("https://maven.isxander.dev/releases") { content { includeGroup("dev.isxander") } } + maven("https://masa.dy.fi/maven/sakura-ryoko") { + content { includeGroupAndSubgroups("fi.dy.masa") } + } maven("https://api.modrinth.com/maven") { content { includeGroup("maven.modrinth") } // for some reason yacl versions exist that aren't on the official repo??? } diff --git a/gradle/fabric.versions.toml b/gradle/fabric.versions.toml index 8244492c0..562b2ab0e 100644 --- a/gradle/fabric.versions.toml +++ b/gradle/fabric.versions.toml @@ -5,6 +5,8 @@ loader = "0.19.2" [libraries] fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "loader" } clothconfig = { module = "me.shedaniel.cloth:cloth-config-fabric", version = "0" } +midnightlib = { module = "eu.midnightdust:midnightlib", version = "0" } +walksylib = { module = "maven.modrinth:walksylib", version = "0" } [plugins] loom-remap = {id = "net.fabricmc.fabric-loom-remap", version.ref = "loom"} diff --git a/gradle/fabric/1.21.10.versions.toml b/gradle/fabric/1.21.10.versions.toml index 76ca6f229..7fdb0097f 100644 --- a/gradle/fabric/1.21.10.versions.toml +++ b/gradle/fabric/1.21.10.versions.toml @@ -11,6 +11,7 @@ resource-loader-v1 = "1.0.2+225bca4068" modmenu = "16.0.0-rc.1" yacl = "3.8.0+1.21.9-fabric" +malilib = "0.26.7" moulconfig = "3.11.0" vulkanmod = "jkDMvvWU" @@ -28,10 +29,11 @@ resource-loader-v1 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } +malilib = { module = "fi.dy.masa.malilib:malilib-fabric-1.21.10", version.ref = "malilib" } moulconfig = { module = "org.notenoughupdates.moulconfig:common", version.ref = "moulconfig" } vulkanmod = { module = "maven.modrinth:vulkanmod", version.ref = "vulkanmod" } [bundles] -compat = ["modmenu", "yacl"] +compat = ["modmenu", "yacl", "malilib"] fabric-api = ["api-base", "lifecycle-events-v1", "rendering-v1", "screen-api-v1", "transitive-access-wideners-v1", "command-api-v2", "key-binding-api-v1", "resource-loader-v0", "resource-loader-v1"] \ No newline at end of file diff --git a/gradle/fabric/1.21.11.versions.toml b/gradle/fabric/1.21.11.versions.toml index f93f3df45..2365bba2a 100644 --- a/gradle/fabric/1.21.11.versions.toml +++ b/gradle/fabric/1.21.11.versions.toml @@ -10,6 +10,7 @@ resource-loader-v1 = "1.0.10+78c8b46653" modmenu = "17.0.0-alpha.1" yacl = "3.8.0+1.21.9-fabric" +malilib = "0.27.16" moulconfig = "3.11.0" dandelion = "1.0.0-alpha.15+1.21.11" odin = "0.2.1" @@ -34,6 +35,7 @@ resource-loader-v1 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } +malilib = { module = "fi.dy.masa.malilib:malilib-fabric-1.21.11", version.ref = "malilib" } dandelion = { module = "net.azureaaron:dandelion", version.ref = "dandelion" } odin = { module = "maven.modrinth:odin", version.ref = "odin" } skyblocker = { module = "maven.modrinth:skyblocker-liap", version.ref = "skyblocker" } @@ -48,5 +50,5 @@ meowdding-lib = { module = "me.owdding.meowdding-lib:meowdding-lib", version.ref skyblock-api = { module = "tech.thatgravyboat:skyblock-api", version.ref = "skyblock-api" } [bundles] -compat = ["modmenu", "yacl", "dandelion", "odin", "skyblocker", "apec"] +compat = ["modmenu", "yacl", "malilib", "dandelion", "odin", "skyblocker", "apec"] fabric-api = ["api-base", "lifecycle-events-v1", "rendering-v1", "screen-api-v1", "transitive-access-wideners-v1", "command-api-v2", "key-binding-api-v1", "resource-loader-v1"] \ No newline at end of file diff --git a/gradle/fabric/1.21.4.versions.toml b/gradle/fabric/1.21.4.versions.toml index 93f8af7b6..30870b766 100644 --- a/gradle/fabric/1.21.4.versions.toml +++ b/gradle/fabric/1.21.4.versions.toml @@ -10,6 +10,7 @@ resource-loader-v0 = "3.1.1+360374ac04" modmenu = "12.0.0" yacl = "3.7.1+1.21.4-fabric" +malilib = "0.23.5" moulconfig = "3.11.0" vulkanmod = "RAtVo03C" @@ -26,10 +27,11 @@ resource-loader-v0 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } +malilib = { module = "fi.dy.masa.malilib:malilib-fabric-1.21.4", version.ref = "malilib" } moulconfig = { module = "org.notenoughupdates.moulconfig:common", version.ref = "moulconfig" } vulkanmod = { module = "maven.modrinth:vulkanmod", version.ref = "vulkanmod" } [bundles] -compat = ["modmenu", "yacl"] +compat = ["modmenu", "yacl", "malilib"] fabric-api = ["api-base", "lifecycle-events-v1", "rendering-v1", "screen-api-v1", "transitive-access-wideners-v1", "command-api-v2", "key-binding-api-v1", "resource-loader-v0"] \ No newline at end of file diff --git a/gradle/fabric/1.21.5.versions.toml b/gradle/fabric/1.21.5.versions.toml index 51df3b09e..a5f0f8bd7 100644 --- a/gradle/fabric/1.21.5.versions.toml +++ b/gradle/fabric/1.21.5.versions.toml @@ -10,6 +10,7 @@ resource-loader-v0 = "3.1.7+847e5f5c49" modmenu = "14.0.0-rc.2" yacl = "3.7.1+1.21.5-fabric" +malilib = "0.24.3" moulconfig = "3.11.0" vulkanmod = "7pYvUnJk" @@ -26,10 +27,11 @@ resource-loader-v0 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } +malilib = { module = "fi.dy.masa.malilib:malilib-fabric-1.21.5", version.ref = "malilib" } moulconfig = { module = "org.notenoughupdates.moulconfig:common", version.ref = "moulconfig" } vulkanmod = { module = "maven.modrinth:vulkanmod", version.ref = "vulkanmod" } [bundles] -compat = ["modmenu", "yacl"] +compat = ["modmenu", "yacl", "malilib"] fabric-api = ["api-base", "lifecycle-events-v1", "rendering-v1", "screen-api-v1", "transitive-access-wideners-v1", "command-api-v2", "key-binding-api-v1", "resource-loader-v0"] \ No newline at end of file diff --git a/gradle/fabric/1.21.8.versions.toml b/gradle/fabric/1.21.8.versions.toml index 7b09a5da1..cc0a991f6 100644 --- a/gradle/fabric/1.21.8.versions.toml +++ b/gradle/fabric/1.21.8.versions.toml @@ -10,6 +10,7 @@ resource-loader-v0 = "3.1.11+946bf4c3f3" modmenu = "15.0.0-beta.3" yacl = "3.7.1+1.21.6-fabric" +malilib = "0.25.7" moulconfig = "3.11.0" [libraries] @@ -24,8 +25,9 @@ resource-loader-v0 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } +malilib = { module = "fi.dy.masa.malilib:malilib-fabric-1.21.8", version.ref = "malilib" } moulconfig = { module = "org.notenoughupdates.moulconfig:common", version.ref = "moulconfig" } [bundles] -compat = ["modmenu", "yacl"] +compat = ["modmenu", "yacl", "malilib"] fabric-api = ["api-base", "lifecycle-events-v1", "rendering-v1", "screen-api-v1", "transitive-access-wideners-v1", "command-api-v2", "key-binding-api-v1", "resource-loader-v0"] \ No newline at end of file diff --git a/gradle/fabric/26.1.versions.toml b/gradle/fabric/26.1.versions.toml index 114c2c75a..f60725333 100644 --- a/gradle/fabric/26.1.versions.toml +++ b/gradle/fabric/26.1.versions.toml @@ -9,6 +9,7 @@ resource-loader-v1 = "2.0.9+d871b99ec3" modmenu = "18.0.0-beta.1" yacl = "3.9.0+26.1-fabric" +malilib = "0.28.1" odin = "0.2.2" skyblocker = "7OtCaJGB" @@ -29,6 +30,7 @@ resource-loader-v1 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } +malilib = { module = "fi.dy.masa.malilib:malilib-fabric-26.1", version.ref = "malilib" } odin = { module = "maven.modrinth:odin", version.ref = "odin" } skyblocker = { module = "maven.modrinth:skyblocker-liap", version.ref = "skyblocker" } @@ -39,5 +41,5 @@ meowdding-lib = { module = "me.owdding.meowdding-lib:meowdding-lib", version.ref skyblock-api = { module = "tech.thatgravyboat:skyblock-api", version.ref = "skyblock-api" } [bundles] -compat = ["modmenu", "yacl", "odin", "skyblocker"] +compat = ["modmenu", "yacl", "malilib", "odin", "skyblocker"] fabric-api = ["api-base", "lifecycle-events-v1", "transitive-access-wideners-v1", "command-api-v2", "key-mapping-api-v1", "resource-loader-v0", "resource-loader-v1"] diff --git a/gradle/fabric/26.2.versions.toml b/gradle/fabric/26.2.versions.toml index 1065f3ad0..523e5f34f 100644 --- a/gradle/fabric/26.2.versions.toml +++ b/gradle/fabric/26.2.versions.toml @@ -9,6 +9,7 @@ resource-loader-v1 = "2.0.13+9edec1269c" # 26.2 uses Minecraft's native Vulkan renderer -> no Cinnabar. modmenu = "20.0.0-beta.4" yacl = "3.9.4+26.2-fabric" +malilib = "0.29.3" skyblocker = "PwRyrygz" [libraries] @@ -21,8 +22,9 @@ resource-loader-v1 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } +malilib = { module = "fi.dy.masa.malilib:malilib-fabric-26.2", version.ref = "malilib" } skyblocker = { module = "maven.modrinth:skyblocker-liap", version.ref = "skyblocker" } [bundles] -compat = ["modmenu", "yacl", "skyblocker"] +compat = ["modmenu", "yacl", "malilib", "skyblocker"] fabric-api = ["api-base", "lifecycle-events-v1", "transitive-access-wideners-v1", "command-api-v2", "key-mapping-api-v1", "resource-loader-v1"] diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java index a401c8311..d54934139 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java @@ -91,9 +91,18 @@ public List getMixins() { //? yacl_compat mixins.add("compat.yacl.Mixin_YetAnotherConfigLib_Builder"); + //? malilib_compat + mixins.add("compat.malilib.Mixin_GuiConfigsBase"); + //? clothconfig_compat mixins.add("compat.cloth.Mixin_ConfigBuilderImpl"); + //? midnightlib_compat + mixins.add("compat.midnightlib.Mixin_MidnightConfig"); + + //? walksylib_compat + mixins.add("compat.walksylib.Mixin_WalksyLib_ModEntryPointList"); + //? tr7zw_compat mixins.add("compat.tr7zw.Mixin_AbstractConfigScreen"); diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/malilib/Mixin_GuiConfigsBase.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/malilib/Mixin_GuiConfigsBase.java new file mode 100644 index 000000000..bea4868d0 --- /dev/null +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/malilib/Mixin_GuiConfigsBase.java @@ -0,0 +1,23 @@ +package org.polyfrost.oneconfig.internal.mixin.compat.malilib; + +//? malilib_compat { +import org.polyfrost.oneconfig.internal.compat.MaLiLibCompat; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Pseudo +@Mixin(targets = "fi.dy.masa.malilib.gui.GuiConfigsBase", remap = false) +public class Mixin_GuiConfigsBase { + + @Inject(method = "initGui", at = @At("TAIL"), remap = false) + private void oneconfig$onInitGui(CallbackInfo ci) { + try { + MaLiLibCompat.onScreen(this); + } catch (Throwable ignored) { + } + } +} +//? } diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/midnightlib/Mixin_MidnightConfig.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/midnightlib/Mixin_MidnightConfig.java new file mode 100644 index 000000000..d622cb18a --- /dev/null +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/midnightlib/Mixin_MidnightConfig.java @@ -0,0 +1,23 @@ +package org.polyfrost.oneconfig.internal.mixin.compat.midnightlib; + +//? midnightlib_compat { +import org.polyfrost.oneconfig.internal.compat.MidnightLibCompat; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Pseudo +@Mixin(targets = "eu.midnightdust.lib.config.MidnightConfig", remap = false) +public class Mixin_MidnightConfig { + + @Inject(method = "init", at = @At("TAIL"), remap = false) + private static void oneconfig$onInit(String modid, Class config, CallbackInfo ci) { + try { + MidnightLibCompat.onInit(modid, config); + } catch (Throwable ignored) { + } + } +} +//? } diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/walksylib/Mixin_WalksyLib_ModEntryPointList.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/walksylib/Mixin_WalksyLib_ModEntryPointList.java new file mode 100644 index 000000000..6bfd6a8ed --- /dev/null +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/walksylib/Mixin_WalksyLib_ModEntryPointList.java @@ -0,0 +1,23 @@ +package org.polyfrost.oneconfig.internal.mixin.compat.walksylib; + +//? walksylib_compat { +import org.polyfrost.oneconfig.internal.compat.WalksyLibCompat; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Pseudo +@Mixin(targets = "main.walksy.lib.core.mods.ModEntryPointList", remap = false) +public class Mixin_WalksyLib_ModEntryPointList { + + @Inject(method = "loadModConfigs", at = @At("TAIL"), remap = false) + private void oneconfig$onLoadModConfigs(CallbackInfo ci) { + try { + WalksyLibCompat.onLoad(this); + } catch (Throwable ignored) { + } + } +} +//? } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/CompatLoader.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/CompatLoader.kt index 86881889e..103704157 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/CompatLoader.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/CompatLoader.kt @@ -22,6 +22,7 @@ object CompatLoader { "com.teamresourceful.resourcefulconfig", "com.teamresourceful.resourcefulconfigkt", "dev.isxander.yacl3", + "fi.dy.masa", "me.shedaniel", "org.polyfrost.oneconfig", "java.lang", @@ -84,6 +85,30 @@ object CompatLoader { fun hasMod(id: String): Boolean = ModInfo.loadedMods.any { it.id == id } + private const val MOD_MENU_CLASS = "com.terraformersmc.modmenu.ModMenu" + + fun originalScreenOpener(modId: String): Runnable? { + val hasScreen = runCatching { + Class.forName(MOD_MENU_CLASS) + .getMethod("hasConfigScreen", String::class.java) + .invoke(null, modId) as? Boolean + }.getOrNull() ?: false + if (!hasScreen) return null + return Runnable { + runCatching { + val parent = Platform.screen().current() + val method = Class.forName(MOD_MENU_CLASS).methods.firstOrNull { + it.name == "getConfigScreen" && it.parameterCount == 2 + } ?: return@runCatching + var screen: Any? = null + withForcedModId(modId) { + screen = method.invoke(null, modId, parent) + } + screen?.let { Platform.screen().display(it) } + } + } + } + private val list: MutableList Unit>> = mutableListOf() init { diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MaLiLibCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MaLiLibCompat.kt new file mode 100644 index 000000000..949398ab8 --- /dev/null +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MaLiLibCompat.kt @@ -0,0 +1,222 @@ +//? malilib_compat { +package org.polyfrost.oneconfig.internal.compat + +import org.apache.logging.log4j.LogManager +import org.polyfrost.oneconfig.api.config.v1.CompatSnapshots +import org.polyfrost.oneconfig.api.config.v1.Properties +import org.polyfrost.oneconfig.api.config.v1.Tree +import org.polyfrost.oneconfig.api.config.v1.Visualizer +import org.polyfrost.oneconfig.api.config.v1.dsl.category +import org.polyfrost.oneconfig.api.config.v1.dsl.noCache +import org.polyfrost.oneconfig.api.config.v1.dsl.saveFunction +import org.polyfrost.oneconfig.api.config.v1.dsl.subcategory +import org.polyfrost.oneconfig.api.platform.v1.ModInfo +import java.lang.reflect.Method +import java.util.Collections +import java.util.IdentityHashMap +import java.util.UUID + +object MaLiLibCompat { + + private val LOGGER = LogManager.getLogger("OneConfig/MaLiLib-Compat") + + private const val CONFIG_MANAGER = "fi.dy.masa.malilib.config.ConfigManager" + + private val trees = HashMap() + private val seen = HashMap>() + + @JvmStatic + fun onScreen(screen: Any) { + runCatching { + val modId = invoke(screen, "getModId") as? String ?: return + if (CompatLoader.nativeLoadedConfigs.contains(modId)) return + + @Suppress("UNCHECKED_CAST") + val wrappers = invoke(screen, "getConfigs") as? Collection<*> ?: return + if (wrappers.isEmpty()) return + + val tree = trees.getOrPut(modId) { createTree(modId) } + val added = seen.getOrPut(modId) { Collections.newSetFromMap(IdentityHashMap()) } + val category = resolveTitle(screen).nonBlankOrNull() ?: (ModInfo.loadedMods.firstOrNull { it.id == modId }?.name ?: modId) + + var subcategory: String? = null + var newProps = false + for (wrapper in wrappers) { + if (wrapper == null) continue + val type = (invoke(wrapper, "getType") as? Enum<*>)?.name + if (type == "LABEL") { + subcategory = (invoke(wrapper, "getLabel") as? String)?.nonBlankOrNull() + continue + } + val config = invoke(wrapper, "getConfig") ?: continue + if (!added.add(config)) continue + runCatching { + if (parseConfig(config, category, subcategory, modId, tree)) newProps = true + }.onFailure { LOGGER.warn("Failed to parse MaLiLib config", it) } + } + + if (newProps && tree.getMetadata("oc_malilib_registered") == null) { + tree.addMetadata("oc_malilib_registered", true) + CompatSnapshots.register(tree) + } + }.onFailure { LOGGER.warn("Failed to parse MaLiLib screen", it) } + } + + private fun createTree(modId: String): Tree { + val mod = ModInfo.loadedMods.firstOrNull { it.id == modId } + val tree = Tree.tree() + tree.id = modId + tree.title = mod?.name?.takeIf { it.isNotBlank() } ?: modId + tree.noCache = true + tree.saveFunction = Runnable { + runCatching { + val cm = Class.forName(CONFIG_MANAGER) + val instance = cm.getMethod("getInstance").invoke(null) + runCatching { cm.getMethod("onConfigsChanged", String::class.java).invoke(instance, modId) } + cm.getMethod("saveAllConfigs").invoke(instance) + }.onFailure { LOGGER.warn("Failed to save MaLiLib configs for {}", modId, it) } + } + mod?.extractIconFile()?.let { tree.addMetadata("icon_path", it) } + CompatLoader.originalScreenOpener(modId)?.let { tree.addMetadata("open_original_screen", it) } + return tree + } + + private fun parseConfig(config: Any, category: String, subcategory: String?, modId: String, tree: Tree): Boolean { + val type = (invoke(config, "getType") as? Enum<*>)?.name ?: return false + val name = (invoke(config, "getConfigGuiDisplayName") as? String)?.nonBlankOrNull() + ?: (invoke(config, "getName") as? String)?.nonBlankOrNull() ?: return false + val desc = (invoke(config, "getComment") as? String)?.nonBlankOrNull() + + val spec = when (type) { + "BOOLEAN" -> PropSpec( + Visualizer.SwitchVisualizer::class.java, + { invoke(config, "getBooleanValue") }, + { v -> invokeSet(config, "setBooleanValue", Boolean::class.javaPrimitiveType!!, v) }, + default = { invoke(config, "getDefaultBooleanValue") }, + ) + "INTEGER" -> PropSpec( + Visualizer.SliderVisualizer::class.java, + { invoke(config, "getIntegerValue") }, + { v -> invokeSet(config, "setIntegerValue", Int::class.javaPrimitiveType!!, (v as Number).toInt()) }, + default = { invoke(config, "getDefaultIntegerValue") }, + min = { (invoke(config, "getMinIntegerValue") as? Number)?.toFloat() }, + max = { (invoke(config, "getMaxIntegerValue") as? Number)?.toFloat() }, + ) + "DOUBLE" -> PropSpec( + Visualizer.SliderVisualizer::class.java, + { invoke(config, "getDoubleValue") }, + { v -> invokeSet(config, "setDoubleValue", Double::class.javaPrimitiveType!!, (v as Number).toDouble()) }, + default = { invoke(config, "getDefaultDoubleValue") }, + min = { (invoke(config, "getMinDoubleValue") as? Number)?.toFloat() }, + max = { (invoke(config, "getMaxDoubleValue") as? Number)?.toFloat() }, + ) + "FLOAT" -> PropSpec( + Visualizer.SliderVisualizer::class.java, + { invoke(config, "getFloatValue") }, + { v -> invokeSet(config, "setFloatValue", Float::class.javaPrimitiveType!!, (v as Number).toFloat()) }, + default = { invoke(config, "getDefaultFloatValue") }, + min = { (invoke(config, "getMinFloatValue") as? Number)?.toFloat() }, + max = { (invoke(config, "getMaxFloatValue") as? Number)?.toFloat() }, + ) + "STRING" -> PropSpec( + Visualizer.TextVisualizer::class.java, + { invoke(config, "getStringValue") }, + { v -> invokeSet(config, "setValueFromString", String::class.java, v?.toString() ?: "") }, + default = { invoke(config, "getDefaultStringValue") }, + ) + "COLOR" -> PropSpec( + Visualizer.ColorVisualizer::class.java, + { invoke(config, "getIntegerValue") }, + { v -> invokeSet(config, "setIntegerValue", Int::class.javaPrimitiveType!!, (v as Number).toInt()) }, + default = { invoke(config, "getDefaultIntegerValue") }, + ) + "OPTION_LIST" -> optionListSpec(config) + else -> null // HOTKEY, STRING_LIST, COLOR_LIST, LOCKED_LIST, TABLE: unsupported + } ?: return false + + val current = runCatching { spec.getter() }.getOrNull() ?: return false + + val property = Properties.functional( + getter = { runCatching { spec.getter() }.getOrNull() }, + setter = { v -> runCatching { spec.setter(v) } }, + id = UUID.randomUUID().toString(), + name = name, + description = desc, + ) + property.addMetadata("visualizer", spec.visualizer) + runCatching { spec.default() }.getOrNull()?.let { property.addMetadata("default", it) } + property.category = category + property.subcategory = subcategory + + spec.min?.let { property.addMetadata("min", runCatching { it() }.getOrNull() ?: 0f) } + spec.max?.let { property.addMetadata("max", runCatching { it() }.getOrNull() ?: 100f) } + spec.options?.let { property.addMetadata("options", it) } + spec.optionLabels?.let { property.addMetadata("optionLabels", it) } + + tree.put(property) + return true + } + + private fun optionListSpec(config: Any): PropSpec? { + val current = invoke(config, "getOptionListValue") ?: return null + val constants = current::class.java.enumConstants ?: return null + if (constants.isEmpty()) return null + val options = constants.map { it.toString() } + val labels = constants.map { + (invoke(it, "getDisplayName") as? String)?.nonBlankOrNull() + ?: (invoke(it, "getStringValue") as? String)?.nonBlankOrNull() + ?: it.toString() + } + return PropSpec( + Visualizer.DropdownVisualizer::class.java, + { invoke(config, "getOptionListValue") }, + { v -> if (v != null) invokeSet(config, "setOptionListValue", v::class.java, v) }, + default = { invoke(config, "getDefaultOptionListValue") }, + options = options, + optionLabels = labels, + ) + } + + private class PropSpec( + val visualizer: Class, + val getter: () -> Any?, + val setter: (Any?) -> Unit, + val default: () -> Any? = { null }, + val min: (() -> Float?)? = null, + val max: (() -> Float?)? = null, + val options: List? = null, + val optionLabels: List? = null, + ) + + private fun resolveTitle(screen: Any): String? = + runCatching { + var cls: Class<*>? = screen.javaClass + while (cls != null && cls != Any::class.java) { + cls.declaredFields.firstOrNull { it.name == "title" && it.type == String::class.java }?.let { + it.isAccessible = true + return@runCatching it.get(screen) as? String + } + cls = cls.superclass + } + invoke(screen, "getTitleString") as? String + }.getOrNull() + + private fun invoke(target: Any, name: String): Any? { + val method = findMethod(target.javaClass, name) ?: return null + return runCatching { method.invoke(target) }.getOrNull() + } + + private fun invokeSet(target: Any, name: String, paramType: Class<*>, value: Any?) { + val method = target.javaClass.methods.firstOrNull { + it.name == name && it.parameterCount == 1 && + (it.parameterTypes[0] == paramType || it.parameterTypes[0].isAssignableFrom(paramType)) + }?.apply { isAccessible = true } ?: return + method.invoke(target, value) + } + + private fun findMethod(cls: Class<*>, name: String): Method? = + cls.methods.firstOrNull { it.name == name && it.parameterCount == 0 }?.apply { isAccessible = true } + + private fun String?.nonBlankOrNull(): String? = this?.trim()?.takeIf { it.isNotEmpty() } +} +//? } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MidnightLibCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MidnightLibCompat.kt new file mode 100644 index 000000000..9f60ea406 --- /dev/null +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MidnightLibCompat.kt @@ -0,0 +1,168 @@ +//? midnightlib_compat { +package org.polyfrost.oneconfig.internal.compat + +import org.apache.logging.log4j.LogManager +import org.polyfrost.oneconfig.api.config.v1.CompatSnapshots +import org.polyfrost.oneconfig.api.config.v1.Properties +import org.polyfrost.oneconfig.api.config.v1.Tree +import org.polyfrost.oneconfig.api.config.v1.Visualizer +import org.polyfrost.oneconfig.api.config.v1.dsl.category +import org.polyfrost.oneconfig.api.config.v1.dsl.noCache +import org.polyfrost.oneconfig.api.config.v1.dsl.saveFunction +import org.polyfrost.oneconfig.api.config.v1.dsl.subcategory +import org.polyfrost.oneconfig.api.platform.v1.ModInfo +import org.polyfrost.oneconfig.api.platform.v1.Platform +import java.lang.reflect.Field +import java.util.UUID + +object MidnightLibCompat { + + private val LOGGER = LogManager.getLogger("OneConfig/MidnightLib-Compat") + + private const val MIDNIGHT_CONFIG = "eu.midnightdust.lib.config.MidnightConfig" + private const val ENTRY_ANNOTATION = "$MIDNIGHT_CONFIG\$Entry" + + @JvmStatic + fun onInit(modid: String, config: Class<*>) { + if (CompatLoader.nativeLoadedConfigs.contains(modid)) return + CompatLoader.requireTranslations { + runCatching { + parseConfig(modid, config)?.let(CompatSnapshots::register) + }.onFailure { + LOGGER.warn("Failed to parse MidnightLib config for {}", modid, it) + } + } + } + + private fun parseConfig(modid: String, config: Class<*>): Tree? { + val entriesField = findField(config, "entries")?.apply { isAccessible = true } ?: return null + val entries = entriesField.get(null)?.let { + (it as? Map<*, *>)?.values ?: it as? Collection<*> + } ?: return null + + val mod = ModInfo.loadedMods.firstOrNull { it.id == modid } + + val tree = Tree.tree() + tree.id = modid + tree.title = mod?.name?.takeIf { it.isNotBlank() } + ?: translateOrNull("$modid.midnightconfig.title") + ?: modid + tree.noCache = true + val write = findWriteMethod(config) + if (write != null) tree.saveFunction = Runnable { runCatching { write.invoke(null, modid) } } + mod?.extractIconFile()?.let { tree.addMetadata("icon_path", it) } + CompatLoader.originalScreenOpener(modid)?.let { tree.addMetadata("open_original_screen", it) } + + var added = false + for (entry in entries) { + if (entry == null) continue + runCatching { + if (parseEntry(entry, modid, tree)) added = true + }.onFailure { LOGGER.warn("Failed to parse MidnightLib entry", it) } + } + + return if (added) tree else null + } + + private fun parseEntry(entry: Any, modid: String, tree: Tree): Boolean { + if ((readString(entry, "modid") ?: readString(entry, "id")) != modid) return false + val field = (readField(entry, "field")?.get(entry) as? Field)?.apply { isAccessible = true } ?: return false + + val entryAnnotation = field.annotations.firstOrNull { it.annotationClass.java.name == ENTRY_ANNOTATION } + ?: return false + + val currentValue = runCatching { field.get(null) }.getOrNull() ?: return false + + val isColor = readAnnotationBoolean(entryAnnotation, "isColor") + val isSlider = readAnnotationBoolean(entryAnnotation, "isSlider") + val isNumber = currentValue is Int || currentValue is Long || currentValue is Float || currentValue is Double + + val visualizer: Class = when { + isColor && (isNumber || currentValue is String) -> Visualizer.ColorVisualizer::class.java + currentValue is Boolean -> Visualizer.SwitchVisualizer::class.java + isNumber && isSlider -> Visualizer.SliderVisualizer::class.java + isNumber -> Visualizer.NumberVisualizer::class.java + currentValue is String -> Visualizer.TextVisualizer::class.java + currentValue is Enum<*> -> Visualizer.DropdownVisualizer::class.java + else -> return false // lists, nested objects, unsupported types + } + + val fieldName = field.name + val name = translateOrNull("$modid.midnightconfig.$fieldName") ?: prettify(fieldName) + val description = translateOrNull("$modid.midnightconfig.$fieldName.tooltip") + + val property = Properties.functional( + getter = { runCatching { field.get(null) }.getOrNull() }, + setter = { v -> runCatching { field.set(null, v) } }, + id = UUID.randomUUID().toString(), + name = name, + description = description, + ) + + property.addMetadata("visualizer", visualizer) + readField(entry, "defaultValue")?.let { runCatching { it.get(entry) }.getOrNull() } + ?.let { property.addMetadata("default", it) } + + val rawCategory = readAnnotationString(entryAnnotation, "category")?.takeIf { it.isNotBlank() && it != "default" } + val category = rawCategory?.let { translateOrNull("$modid.midnightconfig.category.$it") ?: prettify(it) } ?: "General" + property.category = category + property.subcategory = category + + when { + isColor -> {} + currentValue is Enum<*> -> + property.addMetadata("options", currentValue::class.java.enumConstants?.map { it.toString() } ?: emptyList()) + isNumber -> { + property.addMetadata("min", readAnnotationDouble(entryAnnotation, "min")?.toFloat() ?: 0f) + property.addMetadata("max", readAnnotationDouble(entryAnnotation, "max")?.toFloat() ?: 100f) + } + } + + tree.put(property) + return true + } + + private fun prettify(name: String): String = + name.replace('_', ' ').replaceFirstChar { it.uppercase() } + + private fun translateOrNull(key: String): String? = + if (Platform.i18n().hasTranslation(key)) Platform.i18n().translateString(key)?.takeIf { it.isNotBlank() && it != key } else null + + private fun readString(obj: Any, name: String): String? = + readField(obj, name)?.let { runCatching { it.get(obj) as? String }.getOrNull() } + + private fun readField(obj: Any, name: String): Field? = + findField(obj.javaClass, name)?.apply { isAccessible = true } + + private fun readAnnotationBoolean(annotation: Any, method: String): Boolean = + runCatching { annotation.javaClass.getMethod(method).invoke(annotation) as? Boolean }.getOrNull() ?: false + + private fun readAnnotationString(annotation: Any, method: String): String? = + runCatching { annotation.javaClass.getMethod(method).invoke(annotation) as? String }.getOrNull() + + private fun readAnnotationDouble(annotation: Any, method: String): Double? = + runCatching { (annotation.javaClass.getMethod(method).invoke(annotation) as? Number)?.toDouble() }.getOrNull() + + private fun findWriteMethod(config: Class<*>) = + runCatching { + var cls: Class<*>? = config + while (cls != null && cls != Any::class.java) { + cls.declaredMethods.firstOrNull { it.name == "write" && it.parameterCount == 1 }?.let { + it.isAccessible = true + return@runCatching it + } + cls = cls.superclass + } + null + }.getOrNull() + + private fun findField(cls: Class<*>, name: String): Field? { + var current: Class<*>? = cls + while (current != null && current != Any::class.java) { + current.declaredFields.firstOrNull { it.name == name }?.let { return it } + current = current.superclass + } + return null + } +} +//? } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/WalksyLibCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/WalksyLibCompat.kt new file mode 100644 index 000000000..5af981f27 --- /dev/null +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/WalksyLibCompat.kt @@ -0,0 +1,257 @@ +//? walksylib_compat { +package org.polyfrost.oneconfig.internal.compat + +import org.apache.logging.log4j.LogManager +import org.polyfrost.oneconfig.api.config.v1.CompatSnapshots +import org.polyfrost.oneconfig.api.config.v1.Properties +import org.polyfrost.oneconfig.api.config.v1.Tree +import org.polyfrost.oneconfig.api.config.v1.Visualizer +import org.polyfrost.oneconfig.api.config.v1.dsl.category +import org.polyfrost.oneconfig.api.config.v1.dsl.noCache +import org.polyfrost.oneconfig.api.config.v1.dsl.saveFunction +import org.polyfrost.oneconfig.api.config.v1.dsl.subcategory +import org.polyfrost.oneconfig.api.platform.v1.ModInfo +import java.util.UUID + +object WalksyLibCompat { + + private val LOGGER = LogManager.getLogger("OneConfig/WalksyLib-Compat") + + @JvmStatic + fun onLoad(entryPointList: Any) { + CompatLoader.requireTranslations { + runCatching { + val getMethod = entryPointList.javaClass.methods.firstOrNull { it.name == "get" && it.parameterCount == 0 } ?: return@runCatching + val mods = getMethod.invoke(entryPointList) as? Collection<*> ?: return@runCatching + for (mod in mods) { + if (mod == null) continue + runCatching { parseMod(mod)?.let(CompatSnapshots::register) } + .onFailure { LOGGER.warn("Failed to parse WalksyLib mod config", it) } + } + }.onFailure { LOGGER.warn("Failed to load WalksyLib configs", it) } + } + } + + private fun parseMod(mod: Any): Tree? { + val container = invoke(mod, "getContainer") ?: return null + val metadata = invoke(container, "getMetadata") ?: return null + val modid = invoke(metadata, "getId") as? String ?: return null + if (CompatLoader.nativeLoadedConfigs.contains(modid)) return null + + val hasConfig = invoke(mod, "hasConfig") as? Boolean ?: false + if (!hasConfig) return null + val config = invoke(mod, "getConfig") ?: return null + + val categories = invoke(config, "categories") as? Collection<*> ?: return null + if (categories.isEmpty()) return null + + val modInfo = ModInfo.loadedMods.firstOrNull { it.id == modid } + + val tree = Tree.tree() + tree.id = modid + tree.title = modInfo?.name?.takeIf { it.isNotBlank() } + ?: (invoke(metadata, "getName") as? String)?.takeIf { it.isNotBlank() } + ?: modid + tree.noCache = true + buildSaveFunction(config)?.let { tree.saveFunction = it } + modInfo?.extractIconFile()?.let { tree.addMetadata("icon_path", it) } + CompatLoader.originalScreenOpener(modid)?.let { tree.addMetadata("open_original_screen", it) } + + var added = false + for (category in categories) { + if (category == null) continue + val categoryName = invoke(category, "name") as? String ?: continue + + (invoke(category, "options") as? Collection<*>)?.forEach { option -> + if (option != null && parseOption(option, categoryName, null, tree)) added = true + } + + (invoke(category, "optionGroups") as? Collection<*>)?.forEach { group -> + if (group == null) return@forEach + val groupName = invoke(group, "getName") as? String + (invoke(group, "getOptions") as? Collection<*>)?.forEach { option -> + if (option != null && parseOption(option, categoryName, groupName, tree)) added = true + } + } + } + + return if (added) tree else null + } + + private fun parseOption(option: Any, categoryName: String, subcategoryName: String?, tree: Tree): Boolean { + val optionClass = option::class.java + val type = invoke(option, "getType") as? Class<*> ?: return false + val name = (invoke(option, "getName") as? String)?.takeIf { it.isNotBlank() } ?: return false + val description = resolveDescription(option) + + if (type == Runnable::class.java) { + return parseButton(option, name, description, categoryName, subcategoryName, tree) + } + + if (type.name == COLOR_CLASS) { + return parseColor(option, type, name, description, categoryName, subcategoryName, tree) + } + + val visualizer: Class = when { + type == java.lang.Boolean::class.java -> Visualizer.SwitchVisualizer::class.java + type == Integer::class.java || type == java.lang.Double::class.java || type == java.lang.Float::class.java -> + Visualizer.SliderVisualizer::class.java + type == String::class.java -> Visualizer.TextVisualizer::class.java + Enum::class.java.isAssignableFrom(type) -> Visualizer.DropdownVisualizer::class.java + else -> return false // sprite, pixel grid, string list and other WalksyLib-only types + } + + val getValueM = optionClass.methods.firstOrNull { it.name == "getValue" && it.parameterCount == 0 } + ?.apply { isAccessible = true } ?: return false + val setValueM = optionClass.methods.firstOrNull { it.name == "setValue" && it.parameterCount == 1 } + ?.apply { isAccessible = true } ?: return false + + @Suppress("UNCHECKED_CAST") + val property = Properties.functional( + { runCatching { getValueM.invoke(option) }.getOrNull() }, + { value -> runCatching { setValueM.invoke(option, coerce(value, type)) } }, + UUID.randomUUID().toString(), + name, + description, + type as Class, + ) + + property.addMetadata("visualizer", visualizer) + (invoke(option, "getDefaultValue"))?.let { property.addMetadata("default", it) } + property.category = categoryName + property.subcategory = subcategoryName + + when { + Enum::class.java.isAssignableFrom(type) -> + property.addMetadata("options", type.enumConstants?.map { it.toString() } ?: emptyList()) + visualizer == Visualizer.SliderVisualizer::class.java -> { + (invoke(option, "getMin") as? Number)?.let { property.addMetadata("min", it.toFloat()) } + (invoke(option, "getMax") as? Number)?.let { property.addMetadata("max", it.toFloat()) } + (invoke(option, "getIncrement") as? Number)?.let { property.addMetadata("step", it.toFloat()) } + } + } + + tree.put(property) + return true + } + + private const val COLOR_CLASS = "main.walksy.lib.core.config.local.options.type.WalksyLibColor" + + private fun parseColor( + option: Any, + colorClass: Class<*>, + name: String, + description: String?, + categoryName: String, + subcategoryName: String?, + tree: Tree, + ): Boolean { + val optionClass = option::class.java + val getValueM = optionClass.methods.firstOrNull { it.name == "getValue" && it.parameterCount == 0 } + ?.apply { isAccessible = true } ?: return false + val setValueM = optionClass.methods.firstOrNull { it.name == "setValue" && it.parameterCount == 1 } + ?.apply { isAccessible = true } ?: return false + + val getRed = colorMethod(colorClass, "getRed") ?: return false + val getGreen = colorMethod(colorClass, "getGreen") ?: return false + val getBlue = colorMethod(colorClass, "getBlue") ?: return false + val getAlpha = colorMethod(colorClass, "getAlpha") ?: return false + val ctor = runCatching { + colorClass.getConstructor( + Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE, + ).apply { isAccessible = true } + }.getOrNull() ?: return false + val getAdditions = colorMethod(colorClass, "getAdditions") + val setAdditions = colorClass.methods.firstOrNull { it.name == "setAdditions" && it.parameterCount == 1 } + ?.apply { isAccessible = true } + + fun toColor(wc: Any?): java.awt.Color? { + wc ?: return null + return runCatching { + java.awt.Color( + getRed.invoke(wc) as Int, + getGreen.invoke(wc) as Int, + getBlue.invoke(wc) as Int, + getAlpha.invoke(wc) as Int, + ) + }.getOrNull() + } + + val property = Properties.functional( + { runCatching { toColor(getValueM.invoke(option)) }.getOrNull() ?: java.awt.Color.WHITE }, + { value -> + runCatching { + val wc = ctor.newInstance(value.red, value.green, value.blue, value.alpha) + if (getAdditions != null && setAdditions != null) { + getValueM.invoke(option)?.let { old -> + runCatching { setAdditions.invoke(wc, getAdditions.invoke(old)) } + } + } + setValueM.invoke(option, wc) + } + }, + UUID.randomUUID().toString(), + name, + description, + java.awt.Color::class.java, + ) + + property.addMetadata("visualizer", Visualizer.ColorVisualizer::class.java) + toColor(invoke(option, "getDefaultValue"))?.let { property.addMetadata("default", it) } + property.category = categoryName + property.subcategory = subcategoryName + tree.put(property) + return true + } + + private fun colorMethod(colorClass: Class<*>, name: String): java.lang.reflect.Method? = + colorClass.methods.firstOrNull { it.name == name && it.parameterCount == 0 }?.apply { isAccessible = true } + + private fun parseButton( + option: Any, + name: String, + description: String?, + categoryName: String, + subcategoryName: String?, + tree: Tree, + ): Boolean { + val action = invoke(option, "getValue") as? Runnable ?: return false + val property = Properties.dummy(UUID.randomUUID().toString(), name, description) + property.addMetadata("visualizer", Visualizer.ButtonVisualizer::class.java) + property.addMetadata("textKey", name) + property.addMetadata("runnable", Runnable { + runCatching { action.run() }.onFailure { LOGGER.warn("Failed to run WalksyLib button action", it) } + }) + property.category = categoryName + property.subcategory = subcategoryName + tree.put(property) + return true + } + + private fun coerce(value: Any?, type: Class<*>): Any? = when { + value is Number && type == Integer::class.java -> value.toInt() + value is Number && type == java.lang.Double::class.java -> value.toDouble() + value is Number && type == java.lang.Float::class.java -> value.toFloat() + else -> value + } + + private fun resolveDescription(option: Any): String? = runCatching { + val desc = invoke(option, "getDescription") ?: return null + val supplier = invoke(desc, "getStringSupplier") ?: return null + (invoke(supplier, "get") as? String)?.takeIf { it.isNotBlank() } + }.getOrNull() + + private fun buildSaveFunction(config: Any): Runnable? { + val hasCallback = runCatching { invoke(config, "saveCallback") != null }.getOrDefault(false) + val method = config.javaClass.methods.firstOrNull { + it.name == (if (hasCallback) "runSave" else "onSave") && it.parameterCount == 0 + }?.apply { isAccessible = true } ?: return null + return Runnable { runCatching { method.invoke(config) } } + } + + private fun invoke(target: Any, method: String): Any? = + target.javaClass.methods.firstOrNull { it.name == method && it.parameterCount == 0 } + ?.apply { isAccessible = true } + ?.let { runCatching { it.invoke(target) }.getOrNull() } +} +//? } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/YACLCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/YACLCompat.kt index eeca79b4b..8ef96bcb8 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/YACLCompat.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/YACLCompat.kt @@ -10,6 +10,7 @@ import org.polyfrost.oneconfig.api.config.v1.dsl.noCache import org.polyfrost.oneconfig.api.config.v1.dsl.saveFunction import org.polyfrost.oneconfig.api.config.v1.dsl.subcategory import org.polyfrost.oneconfig.api.platform.v1.ModInfo +import org.polyfrost.oneconfig.api.platform.v1.Platform import java.util.* object YACLCompat { @@ -63,6 +64,9 @@ object YACLCompat { mod?.extractIconFile()?.let { tree.addMetadata("icon_path", it) } + mod?.id?.let { id -> + CompatLoader.originalScreenOpener(id)?.let { tree.addMetadata("open_original_screen", it) } + } for (category in categories) { if (category == null) continue @@ -133,6 +137,13 @@ object YACLCompat { }.getOrNull() } + // ButtonOption exposes no readable value (its binding throws), so handle it before the + // binding logic. Detect it by its interface name and render it as a clickable button. + if (isButtonOption(optionClass)) { + parseButtonOption(option, name, desc, categoryName, subcategoryName, root) + return + } + // Older YACL exposes a Binding (getValue/setValue/defaultValue). Newer YACL uses a state // manager and throws UnsupportedOperationException from binding(), so fall back to the // Option-level pendingValue()/requestSet(T) accessors in that case. @@ -205,6 +216,64 @@ object YACLCompat { root.put(property) } + private fun isButtonOption(optionClass: Class<*>): Boolean { + var cls: Class<*>? = optionClass + while (cls != null && cls != Any::class.java) { + if (cls.simpleName == "ButtonOption") return true + if (cls.interfaces.any { isButtonOption(it) }) return true + cls = cls.superclass + } + return false + } + + private fun parseButtonOption( + option: Any, + name: String, + desc: String?, + categoryName: String, + subcategoryName: String?, + root: Tree, + ) { + val optionClass = option::class.java + + val label = optionClass.methods.firstOrNull { it.name == "text" && it.parameterCount == 0 } + ?.let { runCatching { resolveComponent(it.invoke(option)) }.getOrNull() } + ?.nonBlankOrNull() ?: name + + val actionMethod = optionClass.methods.firstOrNull { it.name == "action" && it.parameterCount == 0 } + val action = actionMethod?.let { runCatching { it.invoke(option) }.getOrNull() } + + val property = Properties.dummy( + id = UUID.randomUUID().toString(), + name = name, + description = desc, + ) + property.addMetadata("visualizer", Visualizer.ButtonVisualizer::class.java) + property.addMetadata("textKey", label) + property.category = categoryName + property.subcategory = subcategoryName + + if (action != null) { + val acceptMethod = action::class.java.methods.firstOrNull { + it.name == "accept" && (it.parameterCount == 1 || it.parameterCount == 2) + }?.apply { isAccessible = true } + if (acceptMethod != null) { + property.metadata?.put("runnable", Runnable { + runCatching { + val screen = Platform.screen().current() + if (acceptMethod.parameterCount == 2) { + acceptMethod.invoke(action, screen, option) + } else { + acceptMethod.invoke(action, screen) + } + }.onFailure { LOGGER.warn("Failed to run YACL button action", it) } + }) + } + } + + root.put(property) + } + private fun resolveController(option: Any): Any? { val controllerMethod = option::class.java.methods.firstOrNull { it.name == "controller" && it.parameterCount == 0 diff --git a/minecraft/stonecutter.gradle.kts b/minecraft/stonecutter.gradle.kts index ed2e28907..fc5111095 100644 --- a/minecraft/stonecutter.gradle.kts +++ b/minecraft/stonecutter.gradle.kts @@ -17,7 +17,10 @@ stonecutter { eval(current.version, "> 1.21.10") && catalogue.has("moulconfig") this["yacl_compat"] = catalogue.has("yacl") + this["malilib_compat"] = current.project.endsWith("-fabric") && catalogue.has("malilib") this["clothconfig_compat"] = catalogue.has("clothconfig") + this["midnightlib_compat"] = catalogue.has("midnightlib") + this["walksylib_compat"] = catalogue.has("walksylib") this["modmenu_compat"] = catalogue.has("modmenu") this["rconfig_compat"] = catalogue.has("rconfig") this["dandelion_compat"] = catalogue.has("dandelion") diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/ConfigRegistry.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/ConfigRegistry.kt index 051881c1a..d10427249 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/ConfigRegistry.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/ConfigRegistry.kt @@ -19,6 +19,7 @@ object ConfigRegistry { "badoptimizations", "respackopts", "midnightlib", + "walksylib", "ukulib", "modernconfig", "collective", diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt index 6e8bbdca4..04c1fe03f 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/Header.kt @@ -1,6 +1,7 @@ package org.polyfrost.oneconfig.internal.ui.components import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith @@ -32,6 +33,8 @@ import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.LineHeightStyle @@ -55,7 +58,9 @@ import org.polyfrost.oneconfig.internal.ui.shell.LocalNavController import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import net.kyori.adventure.text.ComponentLike +import org.polyfrost.oneconfig.internal.ui.api.Tooltip import org.polyfrost.oneconfig.internal.ui.shell.ShellState +import org.polyfrost.oneconfig.internal.ui.themes.Accent import org.polyfrost.oneconfig.internal.ui.themes.LocalTheme @Composable @@ -102,12 +107,44 @@ fun Header() { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(26.dp) ) { + ShellState.openOriginalScreen?.let { OpenOriginalScreenButton(it) } GlobalSearchBar() IconButton("close") { closeRequest() } } } } +@Composable +private fun OpenOriginalScreenButton(runnable: Runnable) { + val theme = LocalTheme.current + val interactionSource = rememberInteractionSource() + val isHovered by interactionSource.collectIsHoveredAsState() + val bgColor by animateColorAsState(if (isHovered) Accent.copy(alpha = 0.75f) else Accent) + + Tooltip( + text = { + Text( + "Opening the original screen may show options that OneConfig doesn't.", + color = theme.textColor, + fontSize = 13.sp, + ) + }, + modifier = Modifier.widthIn(max = 260.dp), + anchor = Alignment.BottomCenter, + ) { + Box( + modifier = Modifier + .pointerHoverIcon(PointerIcon.Hand) + .background(bgColor, theme.buttonShape) + .onClick(interactionSource) { runnable.run() } + .padding(horizontal = 16.dp, vertical = 6.dp), + contentAlignment = Alignment.Center, + ) { + Text("Open original screen", color = theme.accentTextColor, fontSize = 13.sp) + } + } +} + @Composable private fun TitleAuthorByline(title: String) { if (ShellState.titleInfoForTitle != title) return diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ModConfig.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ModConfig.kt index c2f2f095e..0d8410a9c 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ModConfig.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/screens/ModConfig.kt @@ -19,16 +19,18 @@ fun ModConfig(id: String, initialCategory: String? = null) { val config = ConfigRegistry.findById(id) val tree = ConfigRegistry.findTree(id) - DisposableEffect(id, config?.title, config?.authors, config?.credits) { + DisposableEffect(id, config?.title, config?.authors, config?.credits, tree) { val title = config?.title?.asRenderText() ShellState.title = title ShellState.titleInfoForTitle = title ShellState.titleAuthors = config?.authors ShellState.titleCredits = config?.credits + ShellState.openOriginalScreen = tree?.getMetadata("open_original_screen") onDispose { ShellState.titleInfoForTitle = null ShellState.titleAuthors = null ShellState.titleCredits = null + ShellState.openOriginalScreen = null } } diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Data.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Data.kt index 8eaa29ebc..d3ce6f4a9 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Data.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/shell/Data.kt @@ -23,6 +23,8 @@ object ShellState { var titleAuthors by mutableStateOf(null) var titleCredits by mutableStateOf(null) + var openOriginalScreen by mutableStateOf(null) + var playerName by mutableStateOf("Player") /** PNG bytes for the local player's head avatar, or null until loaded. */ From 145c90c84a0816ccd364ec1ebab51743e8042117 Mon Sep 17 00:00:00 2001 From: awruff Date: Fri, 24 Jul 2026 19:17:12 -0400 Subject: [PATCH 2/4] feat: walksylib sprite picker compat --- .../internal/compat/WalksyLibCompat.kt | 95 ++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/WalksyLibCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/WalksyLibCompat.kt index 5af981f27..b137219c6 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/WalksyLibCompat.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/WalksyLibCompat.kt @@ -11,6 +11,7 @@ import org.polyfrost.oneconfig.api.config.v1.dsl.noCache import org.polyfrost.oneconfig.api.config.v1.dsl.saveFunction import org.polyfrost.oneconfig.api.config.v1.dsl.subcategory import org.polyfrost.oneconfig.api.platform.v1.ModInfo +import net.minecraft.client.Minecraft import java.util.UUID object WalksyLibCompat { @@ -92,13 +93,17 @@ object WalksyLibCompat { return parseColor(option, type, name, description, categoryName, subcategoryName, tree) } + if (type.name == SPRITE_CLASS) { + return parseSprite(option, type, name, description, categoryName, subcategoryName, tree) + } + val visualizer: Class = when { type == java.lang.Boolean::class.java -> Visualizer.SwitchVisualizer::class.java type == Integer::class.java || type == java.lang.Double::class.java || type == java.lang.Float::class.java -> Visualizer.SliderVisualizer::class.java type == String::class.java -> Visualizer.TextVisualizer::class.java Enum::class.java.isAssignableFrom(type) -> Visualizer.DropdownVisualizer::class.java - else -> return false // sprite, pixel grid, string list and other WalksyLib-only types + else -> return false // pixel grid, string list and other WalksyLib-only types } val getValueM = optionClass.methods.firstOrNull { it.name == "getValue" && it.parameterCount == 0 } @@ -207,6 +212,94 @@ object WalksyLibCompat { private fun colorMethod(colorClass: Class<*>, name: String): java.lang.reflect.Method? = colorClass.methods.firstOrNull { it.name == name && it.parameterCount == 0 }?.apply { isAccessible = true } + private const val SPRITE_CLASS = "main.walksy.lib.core.utils.IdentifierWrapper" + private const val CONFIG_MANAGER_CLASS = "main.walksy.lib.core.manager.WalksyLibConfigManager" + private val SPRITE_TYPES = arrayOf("png") + + private fun parseSprite( + option: Any, + wrapperClass: Class<*>, + name: String, + description: String?, + categoryName: String, + subcategoryName: String?, + tree: Tree, + ): Boolean { + val optionClass = option::class.java + val getValueM = optionClass.methods.firstOrNull { it.name == "getValue" && it.parameterCount == 0 } + ?.apply { isAccessible = true } ?: return false + val setValueM = optionClass.methods.firstOrNull { it.name == "setValue" && it.parameterCount == 1 } + ?.apply { isAccessible = true } ?: return false + + val getFileName = wrapperClass.methods.firstOrNull { it.name == "getFileName" && it.parameterCount == 0 } + ?.apply { isAccessible = true } ?: return false + val wrapperCtor = wrapperClass.constructors.firstOrNull { + it.parameterCount == 2 && it.parameterTypes[1] == String::class.java + }?.apply { isAccessible = true } ?: return false + + val managerClass = runCatching { Class.forName(CONFIG_MANAGER_CLASS, false, wrapperClass.classLoader) } + .getOrNull() ?: return false + val cachedImageDirM = managerClass.methods.firstOrNull { it.name == "getCachedImageDir" && it.parameterCount == 0 } + ?.apply { isAccessible = true } ?: return false + val loadTextureM = managerClass.methods.firstOrNull { + it.name == "loadTextureFromCache" && it.parameterCount == 1 + }?.apply { isAccessible = true } ?: return false + + fun cachedDir(): java.nio.file.Path? = runCatching { cachedImageDirM.invoke(null) as? java.nio.file.Path }.getOrNull() + + val defaultWrapper = invoke(option, "getDefaultValue") + val defaultString = defaultWrapper?.let { runCatching { getFileName.invoke(it) as? String }.getOrNull() } + ?.takeIf { it.isNotBlank() }?.let { cachedDir()?.resolve(it)?.toString() } ?: "" + + val property = Properties.functional( + { + runCatching { + val fileName = getFileName.invoke(getValueM.invoke(option)) as? String + fileName?.takeIf { it.isNotBlank() }?.let { cachedDir()?.resolve(it)?.toString() } ?: "" + }.getOrNull() ?: "" + }, + { value -> + runCatching { + val path = value.takeIf { it.isNotBlank() } + if (path == null || (defaultWrapper != null && path == defaultString)) { + if (defaultWrapper != null) { + runCatching { setValueM.invoke(option, defaultWrapper) } + .onFailure { LOGGER.warn("Failed to reset WalksyLib sprite value", it) } + } + return@runCatching + } + val source = java.io.File(path) + if (!source.isFile) return@runCatching + val fileName = source.name + val dest = cachedDir()?.resolve(fileName) + if (dest != null && source.toPath().toAbsolutePath() != dest.toAbsolutePath()) { + java.nio.file.Files.copy(source.toPath(), dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING) + } + Minecraft.getInstance().execute { + runCatching { + val identifier = loadTextureM.invoke(null, fileName) ?: return@execute + setValueM.invoke(option, wrapperCtor.newInstance(identifier, fileName)) + }.onFailure { LOGGER.warn("Failed to apply WalksyLib sprite value", it) } + } + }.onFailure { LOGGER.warn("Failed to set WalksyLib sprite value", it) } + }, + UUID.randomUUID().toString(), + name, + description, + String::class.java, + ) + + property.addMetadata("visualizer", Visualizer.FileVisualizer::class.java) + property.addMetadata("types", SPRITE_TYPES) + property.addMetadata("filterName", "PNG Image") + property.addMetadata("placeholder", "Select a .png texture") + if (defaultWrapper != null) property.addMetadata("default", defaultString) + property.category = categoryName + property.subcategory = subcategoryName + tree.put(property) + return true + } + private fun parseButton( option: Any, name: String, From 34c94a64e58ca5401268ee2cd5b291e6c495b204 Mon Sep 17 00:00:00 2001 From: awruff Date: Fri, 24 Jul 2026 19:17:18 -0400 Subject: [PATCH 3/4] fix: midnightlib not working properly --- .../internal/compat/MidnightLibCompat.kt | 225 ++++++++++++++---- 1 file changed, 182 insertions(+), 43 deletions(-) diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MidnightLibCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MidnightLibCompat.kt index 9f60ea406..fcc9228e4 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MidnightLibCompat.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MidnightLibCompat.kt @@ -4,6 +4,7 @@ package org.polyfrost.oneconfig.internal.compat import org.apache.logging.log4j.LogManager import org.polyfrost.oneconfig.api.config.v1.CompatSnapshots import org.polyfrost.oneconfig.api.config.v1.Properties +import org.polyfrost.oneconfig.api.config.v1.Property import org.polyfrost.oneconfig.api.config.v1.Tree import org.polyfrost.oneconfig.api.config.v1.Visualizer import org.polyfrost.oneconfig.api.config.v1.dsl.category @@ -14,6 +15,7 @@ import org.polyfrost.oneconfig.api.platform.v1.ModInfo import org.polyfrost.oneconfig.api.platform.v1.Platform import java.lang.reflect.Field import java.util.UUID +import java.util.function.Supplier object MidnightLibCompat { @@ -21,6 +23,12 @@ object MidnightLibCompat { private const val MIDNIGHT_CONFIG = "eu.midnightdust.lib.config.MidnightConfig" private const val ENTRY_ANNOTATION = "$MIDNIGHT_CONFIG\$Entry" + private const val COMMENT_ANNOTATION = "$MIDNIGHT_CONFIG\$Comment" + + private class Pending( + val property: Property<*>, + val conditions: List, + ) @JvmStatic fun onInit(modid: String, config: Class<*>) { @@ -53,77 +61,194 @@ object MidnightLibCompat { mod?.extractIconFile()?.let { tree.addMetadata("icon_path", it) } CompatLoader.originalScreenOpener(modid)?.let { tree.addMetadata("open_original_screen", it) } - var added = false + val subcategoryByCategory = HashMap() + val propertyByField = HashMap>() + val pending = ArrayList() + for (entry in entries) { if (entry == null) continue runCatching { - if (parseEntry(entry, modid, tree)) added = true + if ((readString(entry, "modid") ?: readString(entry, "id")) != modid) return@runCatching + + val field = (readField(entry, "field")?.get(entry) as? Field)?.apply { isAccessible = true } + ?: return@runCatching + + val comment = readAnnotation(field, entry, "comment", COMMENT_ANNOTATION) + if (comment != null) { + val category = categoryOf(modid, readAnnotationString(comment, "category")) + subcategoryByCategory[category] = commentLabel(modid, field.name, comment) + return@runCatching + } + + val entryAnnotation = readAnnotation(field, entry, "entry", ENTRY_ANNOTATION) + ?: return@runCatching + + val property = parseEntry(entry, modid, field, entryAnnotation, subcategoryByCategory) + ?: return@runCatching + tree.put(property) + propertyByField[field.name] = property + pending += Pending(property, readConditions(entry)) }.onFailure { LOGGER.warn("Failed to parse MidnightLib entry", it) } } - return if (added) tree else null - } - - private fun parseEntry(entry: Any, modid: String, tree: Tree): Boolean { - if ((readString(entry, "modid") ?: readString(entry, "id")) != modid) return false - val field = (readField(entry, "field")?.get(entry) as? Field)?.apply { isAccessible = true } ?: return false + wireConditions(pending, propertyByField) - val entryAnnotation = field.annotations.firstOrNull { it.annotationClass.java.name == ENTRY_ANNOTATION } - ?: return false + return if (pending.isNotEmpty()) tree else null + } - val currentValue = runCatching { field.get(null) }.getOrNull() ?: return false + private fun parseEntry( + entry: Any, + modid: String, + field: Field, + entryAnnotation: Any, + subcategoryByCategory: Map, + ): Property<*>? { + val currentValue = runCatching { field.get(null) }.getOrNull() val isColor = readAnnotationBoolean(entryAnnotation, "isColor") val isSlider = readAnnotationBoolean(entryAnnotation, "isSlider") val isNumber = currentValue is Int || currentValue is Long || currentValue is Float || currentValue is Double - val visualizer: Class = when { - isColor && (isNumber || currentValue is String) -> Visualizer.ColorVisualizer::class.java - currentValue is Boolean -> Visualizer.SwitchVisualizer::class.java - isNumber && isSlider -> Visualizer.SliderVisualizer::class.java - isNumber -> Visualizer.NumberVisualizer::class.java - currentValue is String -> Visualizer.TextVisualizer::class.java - currentValue is Enum<*> -> Visualizer.DropdownVisualizer::class.java - else -> return false // lists, nested objects, unsupported types - } - val fieldName = field.name val name = translateOrNull("$modid.midnightconfig.$fieldName") ?: prettify(fieldName) val description = translateOrNull("$modid.midnightconfig.$fieldName.tooltip") + val id = UUID.randomUUID().toString() - val property = Properties.functional( - getter = { runCatching { field.get(null) }.getOrNull() }, - setter = { v -> runCatching { field.set(null, v) } }, - id = UUID.randomUUID().toString(), - name = name, - description = description, - ) + val property: Property<*> + val visualizer: Class + val extraMetadata = HashMap() + var defaultValue = readField(entry, "defaultValue")?.let { runCatching { it.get(entry) }.getOrNull() } + + when { + isColor && currentValue is String -> { + visualizer = Visualizer.ColorVisualizer::class.java + extraMetadata["noAlpha"] = true + property = Properties.functional( + getter = { parseColorHex(field.get(null) as? String) }, + setter = { v: Int -> runCatching { field.set(null, formatColorHex(v)) } }, + id = id, name = name, description = description, type = Int::class.javaObjectType, + ) + defaultValue = (defaultValue as? String)?.let(::parseColorHex) + } + + isColor && isNumber -> { + visualizer = Visualizer.ColorVisualizer::class.java + property = functionalField(field, id, name, description) + } + + currentValue is Boolean -> { + visualizer = Visualizer.SwitchVisualizer::class.java + property = functionalField(field, id, name, description) + } + + isNumber -> { + visualizer = if (isSlider) Visualizer.SliderVisualizer::class.java else Visualizer.NumberVisualizer::class.java + extraMetadata["min"] = readAnnotationDouble(entryAnnotation, "min")?.toFloat() ?: 0f + extraMetadata["max"] = readAnnotationDouble(entryAnnotation, "max")?.toFloat() ?: 100f + property = functionalField(field, id, name, description) + } + + currentValue is Enum<*> -> { + visualizer = Visualizer.DropdownVisualizer::class.java + val constants = currentValue::class.java.enumConstants?.map { it.toString() } ?: emptyList() + extraMetadata["options"] = constants + extraMetadata["optionLabels"] = constants.map { enumLabel(modid, currentValue::class.java, it) } + property = functionalField(field, id, name, description) + } + + currentValue is String -> { + visualizer = Visualizer.TextVisualizer::class.java + property = functionalField(field, id, name, description) + } + + currentValue is List<*> && currentValue.all { it is String } -> { + visualizer = Visualizer.TextVisualizer::class.java + extraMetadata["multiline"] = true + property = Properties.functional( + getter = { (field.get(null) as? List<*>)?.joinToString("\n") ?: "" }, + setter = { v: String -> + runCatching { + field.set(null, v.split('\n').map(String::trim).filter(String::isNotEmpty).toMutableList()) + } + }, + id = id, name = name, description = description, type = String::class.java, + ) + @Suppress("UNCHECKED_CAST") + defaultValue = (defaultValue as? List)?.joinToString("\n") + } + + else -> return null // nested objects / unsupported types + } property.addMetadata("visualizer", visualizer) - readField(entry, "defaultValue")?.let { runCatching { it.get(entry) }.getOrNull() } - ?.let { property.addMetadata("default", it) } + extraMetadata.forEach { (k, v) -> property.addMetadata(k, v) } + defaultValue?.let { property.addMetadata("default", it) } - val rawCategory = readAnnotationString(entryAnnotation, "category")?.takeIf { it.isNotBlank() && it != "default" } - val category = rawCategory?.let { translateOrNull("$modid.midnightconfig.category.$it") ?: prettify(it) } ?: "General" + val category = categoryOf(modid, readAnnotationString(entryAnnotation, "category")) property.category = category - property.subcategory = category + property.subcategory = subcategoryByCategory[category] ?: category + return property + } - when { - isColor -> {} - currentValue is Enum<*> -> - property.addMetadata("options", currentValue::class.java.enumConstants?.map { it.toString() } ?: emptyList()) - isNumber -> { - property.addMetadata("min", readAnnotationDouble(entryAnnotation, "min")?.toFloat() ?: 0f) - property.addMetadata("max", readAnnotationDouble(entryAnnotation, "max")?.toFloat() ?: 100f) + private fun functionalField(field: Field, id: String, name: String, description: String?): Property<*> = + Properties.functional( + getter = { runCatching { field.get(null) }.getOrNull() }, + setter = { v: Any? -> runCatching { field.set(null, v) } }, + id = id, name = name, description = description, + ) + + private fun wireConditions(pending: List, propertyByField: Map>) { + for (p in pending) { + for (condition in p.conditions) { + val requiredOption = readAnnotationString(condition, "requiredOption")?.takeIf { it.isNotBlank() } ?: continue + val requiredValues = readAnnotationStringArray(condition, "requiredValue") + val locked = readAnnotationBoolean(condition, "visibleButLocked") + val source = propertyByField[requiredOption] ?: continue + + val evaluate = Supplier { + val value = runCatching { source.get() }.getOrNull()?.toString() + when { + value != null && requiredValues.contains(value) -> Property.Display.SHOWN + locked -> Property.Display.DISABLED + else -> Property.Display.HIDDEN + } + } + p.property.addDisplayCondition(evaluate) + @Suppress("UNCHECKED_CAST") + (source as Property).addCallback { p.property.revaluateDisplay(); false } } } + } + + private fun categoryOf(modid: String, raw: String?): String { + val cat = raw?.takeIf { it.isNotBlank() && it != "default" } ?: return "General" + return translateOrNull("$modid.midnightconfig.category.$cat") ?: prettify(cat) + } - tree.put(property) - return true + private fun commentLabel(modid: String, fieldName: String, comment: Any): String { + readAnnotationString(comment, "name")?.takeIf { it.isNotBlank() }?.let { return it } + return translateOrNull("$modid.midnightconfig.$fieldName") + ?: prettify(fieldName.trimStart('_')) + } + + private fun enumLabel(modid: String, enumClass: Class<*>, constant: String): String = + translateOrNull("$modid.midnightconfig.enum.${enumClass.simpleName}.$constant") ?: prettify(constant) + + private fun parseColorHex(hex: String?): Int { + val h = hex?.trim()?.removePrefix("#") ?: return 0xFF000000.toInt() + return runCatching { + when (h.length) { + 6 -> 0xFF000000.toInt() or h.toInt(16) + 8 -> h.toLong(16).toInt() + else -> 0xFF000000.toInt() + } + }.getOrDefault(0xFF000000.toInt()) } + private fun formatColorHex(argb: Int): String = "#%06X".format(argb and 0xFFFFFF) + private fun prettify(name: String): String = - name.replace('_', ' ').replaceFirstChar { it.uppercase() } + name.replace('_', ' ').trim().replaceFirstChar { it.uppercase() } private fun translateOrNull(key: String): String? = if (Platform.i18n().hasTranslation(key)) Platform.i18n().translateString(key)?.takeIf { it.isNotBlank() && it != key } else null @@ -131,6 +256,16 @@ object MidnightLibCompat { private fun readString(obj: Any, name: String): String? = readField(obj, name)?.let { runCatching { it.get(obj) as? String }.getOrNull() } + private fun readAnnotation(field: Field, entry: Any, entryInfoField: String, annotationName: String): Any? { + readField(entry, entryInfoField)?.let { runCatching { it.get(entry) }.getOrNull() }?.let { return it } + return field.annotations.firstOrNull { it.annotationClass.java.name == annotationName } + } + + private fun readConditions(entry: Any): List { + val raw = readField(entry, "conditions")?.let { runCatching { it.get(entry) }.getOrNull() } ?: return emptyList() + return (raw as? Array<*>)?.filterNotNull() ?: emptyList() + } + private fun readField(obj: Any, name: String): Field? = findField(obj.javaClass, name)?.apply { isAccessible = true } @@ -140,6 +275,10 @@ object MidnightLibCompat { private fun readAnnotationString(annotation: Any, method: String): String? = runCatching { annotation.javaClass.getMethod(method).invoke(annotation) as? String }.getOrNull() + private fun readAnnotationStringArray(annotation: Any, method: String): List = + runCatching { (annotation.javaClass.getMethod(method).invoke(annotation) as? Array<*>)?.filterIsInstance() } + .getOrNull() ?: emptyList() + private fun readAnnotationDouble(annotation: Any, method: String): Double? = runCatching { (annotation.javaClass.getMethod(method).invoke(annotation) as? Number)?.toDouble() }.getOrNull() From 66701da98aa04dee24e5a20c8c8b184ebbce1ea9 Mon Sep 17 00:00:00 2001 From: awruff Date: Fri, 24 Jul 2026 19:43:08 -0400 Subject: [PATCH 4/4] temp: remove malilib compat malilib has a lot of advanced configuration options, for mods like litematica and tweakeroo. I think for right now its better to just use malilib's ui for the best experience --- .../main/kotlin/oneconfig-setup.gradle.kts | 3 - gradle/fabric/1.21.10.versions.toml | 4 +- gradle/fabric/1.21.11.versions.toml | 4 +- gradle/fabric/1.21.4.versions.toml | 4 +- gradle/fabric/1.21.5.versions.toml | 4 +- gradle/fabric/1.21.8.versions.toml | 4 +- gradle/fabric/26.1.versions.toml | 4 +- gradle/fabric/26.2.versions.toml | 4 +- .../internal/OneConfigMixinInit.java | 3 - .../compat/malilib/Mixin_GuiConfigsBase.java | 23 -- .../oneconfig/internal/compat/CompatLoader.kt | 1 - .../internal/compat/MaLiLibCompat.kt | 222 ------------------ minecraft/stonecutter.gradle.kts | 1 - 13 files changed, 7 insertions(+), 274 deletions(-) delete mode 100644 minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/malilib/Mixin_GuiConfigsBase.java delete mode 100644 minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MaLiLibCompat.kt diff --git a/buildSrc/src/main/kotlin/oneconfig-setup.gradle.kts b/buildSrc/src/main/kotlin/oneconfig-setup.gradle.kts index b0f6fe9f5..c6c251398 100644 --- a/buildSrc/src/main/kotlin/oneconfig-setup.gradle.kts +++ b/buildSrc/src/main/kotlin/oneconfig-setup.gradle.kts @@ -38,9 +38,6 @@ repositories { maven("https://maven.isxander.dev/releases") { content { includeGroup("dev.isxander") } } - maven("https://masa.dy.fi/maven/sakura-ryoko") { - content { includeGroupAndSubgroups("fi.dy.masa") } - } maven("https://api.modrinth.com/maven") { content { includeGroup("maven.modrinth") } // for some reason yacl versions exist that aren't on the official repo??? } diff --git a/gradle/fabric/1.21.10.versions.toml b/gradle/fabric/1.21.10.versions.toml index 7fdb0097f..76ca6f229 100644 --- a/gradle/fabric/1.21.10.versions.toml +++ b/gradle/fabric/1.21.10.versions.toml @@ -11,7 +11,6 @@ resource-loader-v1 = "1.0.2+225bca4068" modmenu = "16.0.0-rc.1" yacl = "3.8.0+1.21.9-fabric" -malilib = "0.26.7" moulconfig = "3.11.0" vulkanmod = "jkDMvvWU" @@ -29,11 +28,10 @@ resource-loader-v1 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } -malilib = { module = "fi.dy.masa.malilib:malilib-fabric-1.21.10", version.ref = "malilib" } moulconfig = { module = "org.notenoughupdates.moulconfig:common", version.ref = "moulconfig" } vulkanmod = { module = "maven.modrinth:vulkanmod", version.ref = "vulkanmod" } [bundles] -compat = ["modmenu", "yacl", "malilib"] +compat = ["modmenu", "yacl"] fabric-api = ["api-base", "lifecycle-events-v1", "rendering-v1", "screen-api-v1", "transitive-access-wideners-v1", "command-api-v2", "key-binding-api-v1", "resource-loader-v0", "resource-loader-v1"] \ No newline at end of file diff --git a/gradle/fabric/1.21.11.versions.toml b/gradle/fabric/1.21.11.versions.toml index 2365bba2a..f93f3df45 100644 --- a/gradle/fabric/1.21.11.versions.toml +++ b/gradle/fabric/1.21.11.versions.toml @@ -10,7 +10,6 @@ resource-loader-v1 = "1.0.10+78c8b46653" modmenu = "17.0.0-alpha.1" yacl = "3.8.0+1.21.9-fabric" -malilib = "0.27.16" moulconfig = "3.11.0" dandelion = "1.0.0-alpha.15+1.21.11" odin = "0.2.1" @@ -35,7 +34,6 @@ resource-loader-v1 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } -malilib = { module = "fi.dy.masa.malilib:malilib-fabric-1.21.11", version.ref = "malilib" } dandelion = { module = "net.azureaaron:dandelion", version.ref = "dandelion" } odin = { module = "maven.modrinth:odin", version.ref = "odin" } skyblocker = { module = "maven.modrinth:skyblocker-liap", version.ref = "skyblocker" } @@ -50,5 +48,5 @@ meowdding-lib = { module = "me.owdding.meowdding-lib:meowdding-lib", version.ref skyblock-api = { module = "tech.thatgravyboat:skyblock-api", version.ref = "skyblock-api" } [bundles] -compat = ["modmenu", "yacl", "malilib", "dandelion", "odin", "skyblocker", "apec"] +compat = ["modmenu", "yacl", "dandelion", "odin", "skyblocker", "apec"] fabric-api = ["api-base", "lifecycle-events-v1", "rendering-v1", "screen-api-v1", "transitive-access-wideners-v1", "command-api-v2", "key-binding-api-v1", "resource-loader-v1"] \ No newline at end of file diff --git a/gradle/fabric/1.21.4.versions.toml b/gradle/fabric/1.21.4.versions.toml index 30870b766..93f8af7b6 100644 --- a/gradle/fabric/1.21.4.versions.toml +++ b/gradle/fabric/1.21.4.versions.toml @@ -10,7 +10,6 @@ resource-loader-v0 = "3.1.1+360374ac04" modmenu = "12.0.0" yacl = "3.7.1+1.21.4-fabric" -malilib = "0.23.5" moulconfig = "3.11.0" vulkanmod = "RAtVo03C" @@ -27,11 +26,10 @@ resource-loader-v0 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } -malilib = { module = "fi.dy.masa.malilib:malilib-fabric-1.21.4", version.ref = "malilib" } moulconfig = { module = "org.notenoughupdates.moulconfig:common", version.ref = "moulconfig" } vulkanmod = { module = "maven.modrinth:vulkanmod", version.ref = "vulkanmod" } [bundles] -compat = ["modmenu", "yacl", "malilib"] +compat = ["modmenu", "yacl"] fabric-api = ["api-base", "lifecycle-events-v1", "rendering-v1", "screen-api-v1", "transitive-access-wideners-v1", "command-api-v2", "key-binding-api-v1", "resource-loader-v0"] \ No newline at end of file diff --git a/gradle/fabric/1.21.5.versions.toml b/gradle/fabric/1.21.5.versions.toml index a5f0f8bd7..51df3b09e 100644 --- a/gradle/fabric/1.21.5.versions.toml +++ b/gradle/fabric/1.21.5.versions.toml @@ -10,7 +10,6 @@ resource-loader-v0 = "3.1.7+847e5f5c49" modmenu = "14.0.0-rc.2" yacl = "3.7.1+1.21.5-fabric" -malilib = "0.24.3" moulconfig = "3.11.0" vulkanmod = "7pYvUnJk" @@ -27,11 +26,10 @@ resource-loader-v0 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } -malilib = { module = "fi.dy.masa.malilib:malilib-fabric-1.21.5", version.ref = "malilib" } moulconfig = { module = "org.notenoughupdates.moulconfig:common", version.ref = "moulconfig" } vulkanmod = { module = "maven.modrinth:vulkanmod", version.ref = "vulkanmod" } [bundles] -compat = ["modmenu", "yacl", "malilib"] +compat = ["modmenu", "yacl"] fabric-api = ["api-base", "lifecycle-events-v1", "rendering-v1", "screen-api-v1", "transitive-access-wideners-v1", "command-api-v2", "key-binding-api-v1", "resource-loader-v0"] \ No newline at end of file diff --git a/gradle/fabric/1.21.8.versions.toml b/gradle/fabric/1.21.8.versions.toml index cc0a991f6..7b09a5da1 100644 --- a/gradle/fabric/1.21.8.versions.toml +++ b/gradle/fabric/1.21.8.versions.toml @@ -10,7 +10,6 @@ resource-loader-v0 = "3.1.11+946bf4c3f3" modmenu = "15.0.0-beta.3" yacl = "3.7.1+1.21.6-fabric" -malilib = "0.25.7" moulconfig = "3.11.0" [libraries] @@ -25,9 +24,8 @@ resource-loader-v0 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } -malilib = { module = "fi.dy.masa.malilib:malilib-fabric-1.21.8", version.ref = "malilib" } moulconfig = { module = "org.notenoughupdates.moulconfig:common", version.ref = "moulconfig" } [bundles] -compat = ["modmenu", "yacl", "malilib"] +compat = ["modmenu", "yacl"] fabric-api = ["api-base", "lifecycle-events-v1", "rendering-v1", "screen-api-v1", "transitive-access-wideners-v1", "command-api-v2", "key-binding-api-v1", "resource-loader-v0"] \ No newline at end of file diff --git a/gradle/fabric/26.1.versions.toml b/gradle/fabric/26.1.versions.toml index f60725333..114c2c75a 100644 --- a/gradle/fabric/26.1.versions.toml +++ b/gradle/fabric/26.1.versions.toml @@ -9,7 +9,6 @@ resource-loader-v1 = "2.0.9+d871b99ec3" modmenu = "18.0.0-beta.1" yacl = "3.9.0+26.1-fabric" -malilib = "0.28.1" odin = "0.2.2" skyblocker = "7OtCaJGB" @@ -30,7 +29,6 @@ resource-loader-v1 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } -malilib = { module = "fi.dy.masa.malilib:malilib-fabric-26.1", version.ref = "malilib" } odin = { module = "maven.modrinth:odin", version.ref = "odin" } skyblocker = { module = "maven.modrinth:skyblocker-liap", version.ref = "skyblocker" } @@ -41,5 +39,5 @@ meowdding-lib = { module = "me.owdding.meowdding-lib:meowdding-lib", version.ref skyblock-api = { module = "tech.thatgravyboat:skyblock-api", version.ref = "skyblock-api" } [bundles] -compat = ["modmenu", "yacl", "malilib", "odin", "skyblocker"] +compat = ["modmenu", "yacl", "odin", "skyblocker"] fabric-api = ["api-base", "lifecycle-events-v1", "transitive-access-wideners-v1", "command-api-v2", "key-mapping-api-v1", "resource-loader-v0", "resource-loader-v1"] diff --git a/gradle/fabric/26.2.versions.toml b/gradle/fabric/26.2.versions.toml index 523e5f34f..1065f3ad0 100644 --- a/gradle/fabric/26.2.versions.toml +++ b/gradle/fabric/26.2.versions.toml @@ -9,7 +9,6 @@ resource-loader-v1 = "2.0.13+9edec1269c" # 26.2 uses Minecraft's native Vulkan renderer -> no Cinnabar. modmenu = "20.0.0-beta.4" yacl = "3.9.4+26.2-fabric" -malilib = "0.29.3" skyblocker = "PwRyrygz" [libraries] @@ -22,9 +21,8 @@ resource-loader-v1 = { module = "net.fabricmc.fabric-api:fabric-resource-loader- modmenu = { module = "com.terraformersmc:modmenu", version.ref = "modmenu"} yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } -malilib = { module = "fi.dy.masa.malilib:malilib-fabric-26.2", version.ref = "malilib" } skyblocker = { module = "maven.modrinth:skyblocker-liap", version.ref = "skyblocker" } [bundles] -compat = ["modmenu", "yacl", "malilib", "skyblocker"] +compat = ["modmenu", "yacl", "skyblocker"] fabric-api = ["api-base", "lifecycle-events-v1", "transitive-access-wideners-v1", "command-api-v2", "key-mapping-api-v1", "resource-loader-v1"] diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java index d54934139..1211d6919 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java @@ -91,9 +91,6 @@ public List getMixins() { //? yacl_compat mixins.add("compat.yacl.Mixin_YetAnotherConfigLib_Builder"); - //? malilib_compat - mixins.add("compat.malilib.Mixin_GuiConfigsBase"); - //? clothconfig_compat mixins.add("compat.cloth.Mixin_ConfigBuilderImpl"); diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/malilib/Mixin_GuiConfigsBase.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/malilib/Mixin_GuiConfigsBase.java deleted file mode 100644 index bea4868d0..000000000 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/malilib/Mixin_GuiConfigsBase.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.polyfrost.oneconfig.internal.mixin.compat.malilib; - -//? malilib_compat { -import org.polyfrost.oneconfig.internal.compat.MaLiLibCompat; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Pseudo; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Pseudo -@Mixin(targets = "fi.dy.masa.malilib.gui.GuiConfigsBase", remap = false) -public class Mixin_GuiConfigsBase { - - @Inject(method = "initGui", at = @At("TAIL"), remap = false) - private void oneconfig$onInitGui(CallbackInfo ci) { - try { - MaLiLibCompat.onScreen(this); - } catch (Throwable ignored) { - } - } -} -//? } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/CompatLoader.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/CompatLoader.kt index 103704157..3b49b3795 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/CompatLoader.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/CompatLoader.kt @@ -22,7 +22,6 @@ object CompatLoader { "com.teamresourceful.resourcefulconfig", "com.teamresourceful.resourcefulconfigkt", "dev.isxander.yacl3", - "fi.dy.masa", "me.shedaniel", "org.polyfrost.oneconfig", "java.lang", diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MaLiLibCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MaLiLibCompat.kt deleted file mode 100644 index 949398ab8..000000000 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/MaLiLibCompat.kt +++ /dev/null @@ -1,222 +0,0 @@ -//? malilib_compat { -package org.polyfrost.oneconfig.internal.compat - -import org.apache.logging.log4j.LogManager -import org.polyfrost.oneconfig.api.config.v1.CompatSnapshots -import org.polyfrost.oneconfig.api.config.v1.Properties -import org.polyfrost.oneconfig.api.config.v1.Tree -import org.polyfrost.oneconfig.api.config.v1.Visualizer -import org.polyfrost.oneconfig.api.config.v1.dsl.category -import org.polyfrost.oneconfig.api.config.v1.dsl.noCache -import org.polyfrost.oneconfig.api.config.v1.dsl.saveFunction -import org.polyfrost.oneconfig.api.config.v1.dsl.subcategory -import org.polyfrost.oneconfig.api.platform.v1.ModInfo -import java.lang.reflect.Method -import java.util.Collections -import java.util.IdentityHashMap -import java.util.UUID - -object MaLiLibCompat { - - private val LOGGER = LogManager.getLogger("OneConfig/MaLiLib-Compat") - - private const val CONFIG_MANAGER = "fi.dy.masa.malilib.config.ConfigManager" - - private val trees = HashMap() - private val seen = HashMap>() - - @JvmStatic - fun onScreen(screen: Any) { - runCatching { - val modId = invoke(screen, "getModId") as? String ?: return - if (CompatLoader.nativeLoadedConfigs.contains(modId)) return - - @Suppress("UNCHECKED_CAST") - val wrappers = invoke(screen, "getConfigs") as? Collection<*> ?: return - if (wrappers.isEmpty()) return - - val tree = trees.getOrPut(modId) { createTree(modId) } - val added = seen.getOrPut(modId) { Collections.newSetFromMap(IdentityHashMap()) } - val category = resolveTitle(screen).nonBlankOrNull() ?: (ModInfo.loadedMods.firstOrNull { it.id == modId }?.name ?: modId) - - var subcategory: String? = null - var newProps = false - for (wrapper in wrappers) { - if (wrapper == null) continue - val type = (invoke(wrapper, "getType") as? Enum<*>)?.name - if (type == "LABEL") { - subcategory = (invoke(wrapper, "getLabel") as? String)?.nonBlankOrNull() - continue - } - val config = invoke(wrapper, "getConfig") ?: continue - if (!added.add(config)) continue - runCatching { - if (parseConfig(config, category, subcategory, modId, tree)) newProps = true - }.onFailure { LOGGER.warn("Failed to parse MaLiLib config", it) } - } - - if (newProps && tree.getMetadata("oc_malilib_registered") == null) { - tree.addMetadata("oc_malilib_registered", true) - CompatSnapshots.register(tree) - } - }.onFailure { LOGGER.warn("Failed to parse MaLiLib screen", it) } - } - - private fun createTree(modId: String): Tree { - val mod = ModInfo.loadedMods.firstOrNull { it.id == modId } - val tree = Tree.tree() - tree.id = modId - tree.title = mod?.name?.takeIf { it.isNotBlank() } ?: modId - tree.noCache = true - tree.saveFunction = Runnable { - runCatching { - val cm = Class.forName(CONFIG_MANAGER) - val instance = cm.getMethod("getInstance").invoke(null) - runCatching { cm.getMethod("onConfigsChanged", String::class.java).invoke(instance, modId) } - cm.getMethod("saveAllConfigs").invoke(instance) - }.onFailure { LOGGER.warn("Failed to save MaLiLib configs for {}", modId, it) } - } - mod?.extractIconFile()?.let { tree.addMetadata("icon_path", it) } - CompatLoader.originalScreenOpener(modId)?.let { tree.addMetadata("open_original_screen", it) } - return tree - } - - private fun parseConfig(config: Any, category: String, subcategory: String?, modId: String, tree: Tree): Boolean { - val type = (invoke(config, "getType") as? Enum<*>)?.name ?: return false - val name = (invoke(config, "getConfigGuiDisplayName") as? String)?.nonBlankOrNull() - ?: (invoke(config, "getName") as? String)?.nonBlankOrNull() ?: return false - val desc = (invoke(config, "getComment") as? String)?.nonBlankOrNull() - - val spec = when (type) { - "BOOLEAN" -> PropSpec( - Visualizer.SwitchVisualizer::class.java, - { invoke(config, "getBooleanValue") }, - { v -> invokeSet(config, "setBooleanValue", Boolean::class.javaPrimitiveType!!, v) }, - default = { invoke(config, "getDefaultBooleanValue") }, - ) - "INTEGER" -> PropSpec( - Visualizer.SliderVisualizer::class.java, - { invoke(config, "getIntegerValue") }, - { v -> invokeSet(config, "setIntegerValue", Int::class.javaPrimitiveType!!, (v as Number).toInt()) }, - default = { invoke(config, "getDefaultIntegerValue") }, - min = { (invoke(config, "getMinIntegerValue") as? Number)?.toFloat() }, - max = { (invoke(config, "getMaxIntegerValue") as? Number)?.toFloat() }, - ) - "DOUBLE" -> PropSpec( - Visualizer.SliderVisualizer::class.java, - { invoke(config, "getDoubleValue") }, - { v -> invokeSet(config, "setDoubleValue", Double::class.javaPrimitiveType!!, (v as Number).toDouble()) }, - default = { invoke(config, "getDefaultDoubleValue") }, - min = { (invoke(config, "getMinDoubleValue") as? Number)?.toFloat() }, - max = { (invoke(config, "getMaxDoubleValue") as? Number)?.toFloat() }, - ) - "FLOAT" -> PropSpec( - Visualizer.SliderVisualizer::class.java, - { invoke(config, "getFloatValue") }, - { v -> invokeSet(config, "setFloatValue", Float::class.javaPrimitiveType!!, (v as Number).toFloat()) }, - default = { invoke(config, "getDefaultFloatValue") }, - min = { (invoke(config, "getMinFloatValue") as? Number)?.toFloat() }, - max = { (invoke(config, "getMaxFloatValue") as? Number)?.toFloat() }, - ) - "STRING" -> PropSpec( - Visualizer.TextVisualizer::class.java, - { invoke(config, "getStringValue") }, - { v -> invokeSet(config, "setValueFromString", String::class.java, v?.toString() ?: "") }, - default = { invoke(config, "getDefaultStringValue") }, - ) - "COLOR" -> PropSpec( - Visualizer.ColorVisualizer::class.java, - { invoke(config, "getIntegerValue") }, - { v -> invokeSet(config, "setIntegerValue", Int::class.javaPrimitiveType!!, (v as Number).toInt()) }, - default = { invoke(config, "getDefaultIntegerValue") }, - ) - "OPTION_LIST" -> optionListSpec(config) - else -> null // HOTKEY, STRING_LIST, COLOR_LIST, LOCKED_LIST, TABLE: unsupported - } ?: return false - - val current = runCatching { spec.getter() }.getOrNull() ?: return false - - val property = Properties.functional( - getter = { runCatching { spec.getter() }.getOrNull() }, - setter = { v -> runCatching { spec.setter(v) } }, - id = UUID.randomUUID().toString(), - name = name, - description = desc, - ) - property.addMetadata("visualizer", spec.visualizer) - runCatching { spec.default() }.getOrNull()?.let { property.addMetadata("default", it) } - property.category = category - property.subcategory = subcategory - - spec.min?.let { property.addMetadata("min", runCatching { it() }.getOrNull() ?: 0f) } - spec.max?.let { property.addMetadata("max", runCatching { it() }.getOrNull() ?: 100f) } - spec.options?.let { property.addMetadata("options", it) } - spec.optionLabels?.let { property.addMetadata("optionLabels", it) } - - tree.put(property) - return true - } - - private fun optionListSpec(config: Any): PropSpec? { - val current = invoke(config, "getOptionListValue") ?: return null - val constants = current::class.java.enumConstants ?: return null - if (constants.isEmpty()) return null - val options = constants.map { it.toString() } - val labels = constants.map { - (invoke(it, "getDisplayName") as? String)?.nonBlankOrNull() - ?: (invoke(it, "getStringValue") as? String)?.nonBlankOrNull() - ?: it.toString() - } - return PropSpec( - Visualizer.DropdownVisualizer::class.java, - { invoke(config, "getOptionListValue") }, - { v -> if (v != null) invokeSet(config, "setOptionListValue", v::class.java, v) }, - default = { invoke(config, "getDefaultOptionListValue") }, - options = options, - optionLabels = labels, - ) - } - - private class PropSpec( - val visualizer: Class, - val getter: () -> Any?, - val setter: (Any?) -> Unit, - val default: () -> Any? = { null }, - val min: (() -> Float?)? = null, - val max: (() -> Float?)? = null, - val options: List? = null, - val optionLabels: List? = null, - ) - - private fun resolveTitle(screen: Any): String? = - runCatching { - var cls: Class<*>? = screen.javaClass - while (cls != null && cls != Any::class.java) { - cls.declaredFields.firstOrNull { it.name == "title" && it.type == String::class.java }?.let { - it.isAccessible = true - return@runCatching it.get(screen) as? String - } - cls = cls.superclass - } - invoke(screen, "getTitleString") as? String - }.getOrNull() - - private fun invoke(target: Any, name: String): Any? { - val method = findMethod(target.javaClass, name) ?: return null - return runCatching { method.invoke(target) }.getOrNull() - } - - private fun invokeSet(target: Any, name: String, paramType: Class<*>, value: Any?) { - val method = target.javaClass.methods.firstOrNull { - it.name == name && it.parameterCount == 1 && - (it.parameterTypes[0] == paramType || it.parameterTypes[0].isAssignableFrom(paramType)) - }?.apply { isAccessible = true } ?: return - method.invoke(target, value) - } - - private fun findMethod(cls: Class<*>, name: String): Method? = - cls.methods.firstOrNull { it.name == name && it.parameterCount == 0 }?.apply { isAccessible = true } - - private fun String?.nonBlankOrNull(): String? = this?.trim()?.takeIf { it.isNotEmpty() } -} -//? } diff --git a/minecraft/stonecutter.gradle.kts b/minecraft/stonecutter.gradle.kts index fc5111095..9d76c1afd 100644 --- a/minecraft/stonecutter.gradle.kts +++ b/minecraft/stonecutter.gradle.kts @@ -17,7 +17,6 @@ stonecutter { eval(current.version, "> 1.21.10") && catalogue.has("moulconfig") this["yacl_compat"] = catalogue.has("yacl") - this["malilib_compat"] = current.project.endsWith("-fabric") && catalogue.has("malilib") this["clothconfig_compat"] = catalogue.has("clothconfig") this["midnightlib_compat"] = catalogue.has("midnightlib") this["walksylib_compat"] = catalogue.has("walksylib")