From 9e0a101f3cd3c0af7fd4c3d11121514aa8bcedcf Mon Sep 17 00:00:00 2001 From: lowercasebtw Date: Wed, 2 Sep 2026 17:26:32 -0400 Subject: [PATCH 01/16] Fix Some Sentry Issues POLYPLUS3-1XS POLYPLUS3-29C POLYPLUS3-29D POLYPLUS3-29G POLYPLUS3-1Y2 POLYPLUS3-23T POLYPLUS3-27M --- .../internal/OneConfigMixinInit.java | 32 +++++ .../Mixin_SkyblockerFancyStatusBars.java | 16 +-- .../Mixin_SkyblockerFancyStatusBarsV2.java | 31 +++++ .../internal/compat/SkyblockerCompat.kt | 101 ++++++++++++---- .../oneconfig/internal/ui/compose/SkiaCtx.kt | 26 ++++ .../oneconfig/api/config/v1/Config.java | 11 +- .../api/config/v1/ConfigManager.java | 21 +++- .../config/v1/backend/impl/FileBackend.java | 6 +- .../oneconfig/api/hud/v1/HudManager.kt | 3 +- .../api/hud/v1/OneConfigHudCompat.kt | 75 +++++++++--- .../ui/hud/screens/HudDesignStudio.kt | 114 ++++++++++-------- .../compose/runtime/PolyComposeHost.kt | 18 ++- 12 files changed, 338 insertions(+), 116 deletions(-) create mode 100644 minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsV2.java 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 143d4b17c..d653096c1 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java @@ -42,6 +42,7 @@ import java.util.Set; public class OneConfigMixinInit implements IMixinConfigPlugin { + private static final Logger LOGGER = LogManager.getLogger("OneConfig/MixinInit"); @Override public void onLoad(String mixinPackage) { @@ -54,9 +55,39 @@ public String getRefMapperConfig() { @Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + //? skyblocker_compat { + if (mixinClassName.endsWith(".Mixin_SkyblockerFancyStatusBarsV2")) return skyblockerBarsAreInstanced(); + if (mixinClassName.endsWith(".Mixin_SkyblockerFancyStatusBars")) return !skyblockerBarsAreInstanced(); + //? } return true; } + //? skyblocker_compat { + private static Boolean skyblockerInstanced; + + private static boolean skyblockerBarsAreInstanced() { + if (skyblockerInstanced == null) { + skyblockerInstanced = false; + String path = "de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.class"; + try (java.io.InputStream in = OneConfigMixinInit.class.getClassLoader().getResourceAsStream(path)) { + if (in != null) { + ClassNode node = new ClassNode(); + new org.objectweb.asm.ClassReader(in).accept(node, org.objectweb.asm.ClassReader.SKIP_CODE); + for (org.objectweb.asm.tree.MethodNode method : node.methods) { + //~ if >= 26.1 'render' -> 'extractRenderState' + if (!method.name.equals("extractRenderState")) continue; + skyblockerInstanced = (method.access & org.objectweb.asm.Opcodes.ACC_STATIC) == 0; + break; + } + } + } catch (Throwable t) { + LOGGER.warn("Could not read the installed Skyblocker's status bar shape, assuming static", t); + } + } + return skyblockerInstanced; + } + //? } + @Override public void acceptTargets(Set myTargets, Set otherTargets) { } @@ -111,6 +142,7 @@ public List getMixins() { //? skyblocker_compat { mixins.add("compat.skyblocker.Mixin_SkyblockerFancyStatusBars"); + mixins.add("compat.skyblocker.Mixin_SkyblockerFancyStatusBarsV2"); mixins.add("compat.skyblocker.Mixin_SkyblockerWidgetManager"); //? } diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java index 6f12c919a..2802e0c30 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java @@ -15,20 +15,7 @@ @Mixin(FancyStatusBars.class) public class Mixin_SkyblockerFancyStatusBars { - //? if skyblocker_hud_v2 { - @Inject(method = "initStatic", at = @At("TAIL"), require = 0) - private static void oneconfig$registerHudCompat(CallbackInfo ci) { - SkyblockerCompat.initialize(); - } - - @Inject(method = "extractRenderState", at = @At("HEAD"), cancellable = true, require = 0) - private void oneconfig$suppressWhileEditing(CallbackInfoReturnable cir) { - if (CompatOverlayRenderer.oneConfigScreenOpen() && !SkyblockerCompat.isRedrawing()) { - cir.setReturnValue(false); - } - } - //?} else { - /*@Inject(method = "init", at = @At("TAIL"), require = 0) + @Inject(method = "init", at = @At("TAIL"), require = 0) private static void oneconfig$registerHudCompat(CallbackInfo ci) { SkyblockerCompat.initialize(); } @@ -40,6 +27,5 @@ public class Mixin_SkyblockerFancyStatusBars { cir.setReturnValue(false); } } - *///?} } //? } diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsV2.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsV2.java new file mode 100644 index 000000000..41d555c7f --- /dev/null +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsV2.java @@ -0,0 +1,31 @@ +package org.polyfrost.oneconfig.internal.mixin.compat.skyblocker; + +//? skyblocker_compat { +import de.hysky.skyblocker.skyblock.fancybars.FancyStatusBars; +import org.polyfrost.oneconfig.internal.compat.SkyblockerCompat; +import org.polyfrost.oneconfig.internal.ui.hud.CompatOverlayRenderer; +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; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Pseudo +@Mixin(FancyStatusBars.class) +public class Mixin_SkyblockerFancyStatusBarsV2 { + + @Inject(method = "initStatic", at = @At("TAIL"), require = 0) + private static void oneconfig$registerHudCompat(CallbackInfo ci) { + SkyblockerCompat.initialize(); + } + + //~ if >= 26.1 'render' -> 'extractRenderState' + @Inject(method = "extractRenderState", at = @At("HEAD"), cancellable = true, require = 0) + private void oneconfig$suppressWhileEditing(CallbackInfoReturnable cir) { + if (CompatOverlayRenderer.oneConfigScreenOpen() && !SkyblockerCompat.isRedrawing()) { + cir.setReturnValue(false); + } + } +} +//? } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt index 7b5c82c4f..b9ccbb688 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt @@ -19,6 +19,9 @@ import org.polyfrost.oneconfig.api.hud.v1.OneConfigHudWrapper import org.polyfrost.oneconfig.api.hud.v1.events.HudEditorToggleEvent import org.polyfrost.oneconfig.internal.ui.hud.CompatOverlayRenderer import java.awt.Color +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Modifier import java.util.function.Consumer import kotlin.math.abs @@ -40,40 +43,96 @@ object SkyblockerCompat { @Volatile private var dragged: StatusBar? = null - //? if skyblocker_hud_v2 { - private fun statusBars(): Map = FancyStatusBars.INSTANCE.statusBars - - private fun positioner(): BarPositioner = FancyStatusBars.INSTANCE.barPositioner - - private fun saveBars() = FancyStatusBars.INSTANCE.saveBarConfig() + private class Bars( + val self: Any?, + val statusBars: Field, + val barPositioner: Field, + val saveBarConfig: Method, + val placeBarsInPositioner: Method, + val updatePositions: Method, + val isHealthFancyBarEnabled: Method, + val extractRenderState: Method, + ) - private fun placeBars() = FancyStatusBars.INSTANCE.placeBarsInPositioner() + private var barsResolved = false + private var barsCache: Bars? = null + + private fun bars(): Bars? { + if (!barsResolved) { + barsResolved = true + barsCache = try { + resolveBars() + } catch (e: Throwable) { + LOGGER.warn("Disabling Skyblocker status bars: FancyStatusBars has an unrecognised shape", e) + null + } + } + return barsCache + } - private fun updatePositions(ignoreVisibility: Boolean) = FancyStatusBars.INSTANCE.updatePositions(ignoreVisibility) + private fun resolveBars(): Bars { + val cls = FancyStatusBars::class.java + fun field(name: String) = cls.getDeclaredField(name).apply { isAccessible = true } + fun method(name: String, vararg params: Class<*>) = + cls.getDeclaredMethod(name, *params).apply { isAccessible = true } + + val statusBars = field("statusBars") + val barPositioner = field("barPositioner") + val saveBarConfig = method("saveBarConfig") + val placeBarsInPositioner = method("placeBarsInPositioner") + val updatePositions = method("updatePositions", java.lang.Boolean.TYPE) + val isHealthFancyBarEnabled = method("isHealthFancyBarEnabled") + //~ if >= 26.1 'render' -> 'extractRenderState' + val extractRenderState = method("extractRenderState", GuiGraphicsExtractor::class.java, Minecraft::class.java) + + val self = runCatching { cls.getField("INSTANCE").get(null) }.getOrNull() + ?: runCatching { cls.getMethod("getInstance").invoke(null) }.getOrNull() + if (self == null) { + val modifiers = intArrayOf( + statusBars.modifiers, barPositioner.modifiers, saveBarConfig.modifiers, + placeBarsInPositioner.modifiers, updatePositions.modifiers, + isHealthFancyBarEnabled.modifiers, extractRenderState.modifiers, + ) + if (modifiers.any { !Modifier.isStatic(it) }) error("FancyStatusBars is not static and has no instance") + } - private fun healthFancyBarEnabled(): Boolean = FancyStatusBars.INSTANCE.isHealthFancyBarEnabled() + return Bars( + self, statusBars, barPositioner, saveBarConfig, + placeBarsInPositioner, updatePositions, isHealthFancyBarEnabled, extractRenderState, + ) + } - private fun renderStatusBars(ctx: GuiGraphicsExtractor, mc: Minecraft) { - FancyStatusBars.INSTANCE.extractRenderState(ctx, mc) + @Suppress("UNCHECKED_CAST") + private fun statusBars(): Map { + val bars = bars() ?: return emptyMap() + return bars.statusBars.get(bars.self) as? Map ?: emptyMap() } - //?} else { - /*private fun statusBars(): Map = FancyStatusBars.statusBars - private fun positioner(): BarPositioner = FancyStatusBars.barPositioner + private fun positioner(): BarPositioner { + val bars = bars() ?: error("Skyblocker status bars are unavailable") + return bars.barPositioner.get(bars.self) as BarPositioner + } - private fun saveBars() = FancyStatusBars.saveBarConfig() + private fun saveBars() { + bars()?.let { it.saveBarConfig.invoke(it.self) } + } - private fun placeBars() = FancyStatusBars.placeBarsInPositioner() + private fun placeBars() { + bars()?.let { it.placeBarsInPositioner.invoke(it.self) } + } - private fun updatePositions(ignoreVisibility: Boolean) = FancyStatusBars.updatePositions(ignoreVisibility) + private fun updatePositions(ignoreVisibility: Boolean) { + bars()?.let { it.updatePositions.invoke(it.self, ignoreVisibility) } + } - private fun healthFancyBarEnabled(): Boolean = FancyStatusBars.isHealthFancyBarEnabled() + private fun healthFancyBarEnabled(): Boolean { + val bars = bars() ?: return false + return bars.isHealthFancyBarEnabled.invoke(bars.self) as? Boolean == true + } private fun renderStatusBars(ctx: GuiGraphicsExtractor, mc: Minecraft) { - //~ if >= 26.1 'render' -> 'extractRenderState' - FancyStatusBars.extractRenderState(ctx, mc) + bars()?.let { it.extractRenderState.invoke(it.self, ctx, mc) } } - *///?} @JvmStatic fun isRedrawing(): Boolean = redrawing diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt index 52e6346a2..0eb5e4e0f 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt @@ -621,10 +621,35 @@ object SkiaCtx { } } + private var oversizeReported = false + + private fun maxTextureSize(): Int = + //? if >= 26.2 { + com.mojang.blaze3d.systems.RenderSystem.getDevice().deviceInfo.limits().maxTextureSize() + //? } else if >= 1.21.5 { + /*com.mojang.blaze3d.systems.RenderSystem.getDevice().maxTextureSize + *///? } else { + /*com.mojang.blaze3d.systems.RenderSystem.maxSupportedTextureSize() + *///? } + + private fun viewportFitsTexture(w: Int, h: Int): Boolean { + val max = runCatching { maxTextureSize() }.getOrDefault(0) + if (max <= 0 || (w <= max && h <= max)) { + oversizeReported = false + return true + } + if (!oversizeReported) { + oversizeReported = true + LOG.warn("SkiaCtx: viewport {}x{} is past the max texture size ({}); skipping offscreen surfaces", w, h, max) + } + return false + } + private fun resolveHudSurface(): Surface? { val w = Platform.screen().viewportWidth() val h = Platform.screen().viewportHeight() if (w <= 0 || h <= 0) return null + if (!viewportFitsTexture(w, h)) return null var rt = hudTarget val needNewTarget = rt == null || rt.width != w || rt.height != h @@ -696,6 +721,7 @@ object SkiaCtx { val w = Platform.screen().viewportWidth() val h = Platform.screen().viewportHeight() if (w <= 0 || h <= 0) return null + if (!viewportFitsTexture(w, h)) return null var rt = composeTarget val needNewTarget = rt == null || rt.width != w || rt.height != h diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java index 9293debf6..6bc4c8f11 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java @@ -112,7 +112,7 @@ protected void initialize(boolean byConfigManager) { tree.addMetadata("category", category); if (!ConfigManager.isRebindingProfiles()) { - ConfigManager.backup().backend.save0(tree); + saveDefaultsBackup(tree); } // capture code defaults before register() loads stored values over them so the UI can offer a reset action if (defaultSnapshot == null) { @@ -135,6 +135,15 @@ protected void initialize(boolean byConfigManager) { } } + private void saveDefaultsBackup(Tree tree) { + try { + ConfigManager.backup().backend.save0(tree); + } catch (Throwable t) { + ConfigManager.LOGGER.error("failed to write the defaults backup for config {}, restore-to-default may be unavailable", id, t); + ConfigManager.notifyWriteFailed(this, t); + } + } + /** * Recursively record the current value of every property in [tree] as transient {@code "default"} metadata *
diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java index 7fc8fc3e1..9b050e56c 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java @@ -191,7 +191,12 @@ public static void initialize() { LOGGER.info("Initializing {} configs...", pendingInitialization.size()); while (!pendingInitialization.isEmpty()) { Config config = pendingInitialization.poll(); - if (config != null) config.initialize(true); + if (config == null) continue; + try { + config.initialize(true); + } catch (Throwable t) { + LOGGER.error("failed to initialize config {}, skipping it", config.id, t); + } } LOGGER.info("Initialized configs in {}ms", (System.nanoTime() - t1) / 1_000_000.0); } @@ -246,11 +251,23 @@ private static void notifyResetOptions(Config config, List options) { : options.size() + " options could not be loaded and were reset to their defaults (" + String.join(", ", options) + "). A backup was saved as " + config.getTree().getID() + ".corrupted."; org.polyfrost.oneconfig.api.notifications.v1.Notifications.error(name + ": options reset", message); } catch (Throwable t) { - // notifications are best-effort and must never break config loading LOGGER.error("failed to notify about reset options for config {}", config.id, t); } } + static void notifyWriteFailed(Config config, Throwable cause) { + try { + String name = config.title != null ? config.title : config.id; + Throwable root = cause.getCause() != null ? cause.getCause() : cause; + String reason = root.getMessage(); + org.polyfrost.oneconfig.api.notifications.v1.Notifications.error(name + ": could not write config", + "OneConfig could not write to the config folder" + (reason != null ? " (" + reason + ")" : "") + + ". Your settings still work this session but may not be saved. Check that your disk is not full."); + } catch (Throwable t) { + LOGGER.error("failed to notify about the write failure for config {}", config.id, t); + } + } + private static void initProfiles() { addProfileChangeListener(CompatSnapshots.INSTANCE); Property ownedProfileSubdirs = Properties.simple( diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/backend/impl/FileBackend.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/backend/impl/FileBackend.java index 7d99b8027..56b859d47 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/backend/impl/FileBackend.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/backend/impl/FileBackend.java @@ -75,9 +75,9 @@ protected static String read(Path p) { } protected static void write(Path p, String s) { + Path tmp = p.resolveSibling(p.getFileName() + ".tmp"); try { Files.createDirectories(p.getParent()); - Path tmp = p.resolveSibling(p.getFileName() + ".tmp"); Files.write(tmp, s.getBytes(CHARSET)); try { Files.move(tmp, p, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); @@ -85,6 +85,10 @@ protected static void write(Path p, String s) { Files.move(tmp, p, StandardCopyOption.REPLACE_EXISTING); } } catch (Exception e) { + try { + Files.deleteIfExists(tmp); + } catch (IOException ignored) { + } throw new SerializationException("Failed to write file", e); } } diff --git a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt index db0cfaa19..0154baa43 100644 --- a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt +++ b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt @@ -517,12 +517,13 @@ object HudManager { } private fun shouldDraw(hud: Hud): Boolean { + if (hud is LegacyHudMarker) return false if (hud.hidden && !isEditing) return false return isShown(hud) } private fun keepsBackgroundOnly(hud: Hud): Boolean = - hud.hidden && !isEditing && hud.keepsHiddenBackground && isShown(hud) + hud !is LegacyHudMarker && hud.hidden && !isEditing && hud.keepsHiddenBackground && isShown(hud) /** Everything [shouldDraw] checks apart from the HUD's own hidden flag */ private fun isShown(hud: Hud): Boolean { diff --git a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt index 4ee85311f..87277287f 100644 --- a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt +++ b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt @@ -16,15 +16,42 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : private val hiddenRevision = mutableStateOf(0) + private var faulted = false + + private fun fault(member: String, error: Throwable) { + faulted = true + HudManager.LOGGER.error( + "Disabling compat HUD '${wrapper.id}' from '${wrapper.modId ?: "unknown"}': $member failed, so " + + "that mod is probably a different version than OneConfig was built against", + error, + ) + } + + private inline fun guard(member: String, fallback: T, block: () -> T): T { + if (faulted) return fallback + return try { + block() + } catch (e: LinkageError) { + fault(member, e) + fallback + } catch (e: RuntimeException) { + fault(member, e) + fallback + } + } + override var hidden: Boolean get() { hiddenRevision.value - return wrapper.hidden + return guard("hidden", true) { wrapper.hidden } } set(value) { - if (wrapper.hidden == value) return - wrapper.hidden = value - hiddenRevision.value++ + guard("hidden", Unit) { + if (wrapper.hidden != value) { + wrapper.hidden = value + hiddenRevision.value++ + } + } } override val persistOwnState: Boolean get() = false @@ -36,26 +63,36 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : override fun deletable(): Boolean = false - override val supportsScale: Boolean get() = wrapper.supportsScale - - override var x: Float by wrapper::x - override var y: Float by wrapper::y - override var relativeX: Float by wrapper::x - override var relativeY: Float by wrapper::y - - override var customScale: Float by wrapper::scale + override val supportsScale: Boolean get() = guard("supportsScale", true) { wrapper.supportsScale } + + override var x: Float + get() = guard("x", 0f) { wrapper.x } + set(value) { guard("x", Unit) { wrapper.x = value } } + override var y: Float + get() = guard("y", 0f) { wrapper.y } + set(value) { guard("y", Unit) { wrapper.y = value } } + override var relativeX: Float + get() = x + set(value) { x = value } + override var relativeY: Float + get() = y + set(value) { y = value } + + override var customScale: Float + get() = guard("scale", 1f) { wrapper.scale } + set(value) { guard("scale", Unit) { wrapper.scale = value } } private var lastW = 0f private var lastH = 0f private fun sizeW(): Float { - val live = runCatching { wrapper.scaledWidth }.getOrDefault(0f) + val live = guard("scaledWidth", 0f) { wrapper.scaledWidth } if (live > 0f) lastW = live return if (live > 0f) live else lastW } private fun sizeH(): Float { - val live = runCatching { wrapper.scaledHeight }.getOrDefault(0f) + val live = guard("scaledHeight", 0f) { wrapper.scaledHeight } if (live > 0f) lastH = live return if (live > 0f) live else lastH } @@ -77,19 +114,21 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : get() = sizeH() set(_) {} - override val resizeAxes: HudResize get() = wrapper.resizeAxes + override val resizeAxes: HudResize get() = guard("resizeAxes", HudResize.Both) { wrapper.resizeAxes } override fun applyEditorWidth(width: Float) { - wrapper.scaledWidth = width + guard("scaledWidth", Unit) { wrapper.scaledWidth = width } } override fun updateRelativeX(absX: Float) { x = absX } override fun updateRelativeY(absY: Float) { y = absY } - override fun onEditorDragStart() = wrapper.onDragStart() + override fun onEditorDragStart() { + guard("onDragStart", Unit) { wrapper.onDragStart() } + } override fun onEditorDragEnd() { - wrapper.onDragEnd() + guard("onDragEnd", Unit) { wrapper.onDragEnd() } CompatSnapshots.capture(tree) } } diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt index 0eb46379e..cc6a613ea 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt @@ -951,19 +951,21 @@ fun HudDesignStudio(onReturnToOneConfig: (() -> Unit)? = null) { fun primaryHud(): Hud? = panelHud?.takeIf { it in selectedHuds } ?: selectedHuds.lastOrNull() val deleteHuds: (Collection) -> Unit = { huds -> - val removed = huds.filter { it.deletable() } - if (removed.isNotEmpty()) { + val (live, stale) = huds.partition { it.isReal } + val removed = live.filter { it.deletable() } + val dropped = removed + stale + if (dropped.isNotEmpty()) { Snapshot.withMutableSnapshot { - val removedSet = removed.toSet() - selectedHuds = selectedHuds - removedSet - removed.forEach { hud -> + val droppedSet = dropped.toSet() + selectedHuds = selectedHuds - droppedSet + dropped.forEach { hud -> if (panelHud === hud) panelHud = null if (hoveredHud === hud) hoveredHud = null - HudManager.removeHud(hud, delete = true) + if (hud.isReal) HudManager.removeHud(hud, delete = true) HudDesignSession.forget(hud) } } - UiSounds.play(UiSoundEvent.CLICK) + if (removed.isNotEmpty()) UiSounds.play(UiSoundEvent.CLICK) } } @@ -1019,67 +1021,73 @@ fun HudDesignStudio(onReturnToOneConfig: (() -> Unit)? = null) { LaunchedEffect(Unit) { HudDesignSession.clearCommands() for (command in HudDesignSession.commands) { - when (command) { - is StudioCommand.Select -> { - val huds = command.huds - if (huds.isNotEmpty() && huds.all { it in HudManager.activeInstances }) { - Snapshot.withMutableSnapshot { selectedHuds = huds.toSet() } + try { + when (command) { + is StudioCommand.Select -> { + val huds = command.huds + if (huds.isNotEmpty() && huds.all { it in HudManager.activeInstances }) { + Snapshot.withMutableSnapshot { selectedHuds = huds.toSet() } + } } - } - StudioCommand.OpenSettings -> { - val primary = primaryHud() - if (primary != null) { - Snapshot.withMutableSnapshot { - panelHud = primary - activeCategory = StudioCategory.Settings + StudioCommand.OpenSettings -> { + val primary = primaryHud() + if (primary != null) { + Snapshot.withMutableSnapshot { + panelHud = primary + activeCategory = StudioCategory.Settings + } } } - } - StudioCommand.Copy -> { - Snapshot.withMutableSnapshot { hudClipboard = selectedHuds.toList() } - UiSounds.play(UiSoundEvent.CLICK) - } + StudioCommand.Copy -> { + Snapshot.withMutableSnapshot { hudClipboard = selectedHuds.toList() } + UiSounds.play(UiSoundEvent.CLICK) + } - StudioCommand.Cut -> { - Snapshot.withMutableSnapshot { hudClipboard = selectedHuds.toList() } - deleteHuds(selectedHuds) - } + StudioCommand.Cut -> { + Snapshot.withMutableSnapshot { hudClipboard = selectedHuds.toList() } + deleteHuds(selectedHuds) + } - StudioCommand.Paste -> { - if (hudClipboard.isNotEmpty()) { - val s = Platform.screen().screenToMcScale() - val pasted = duplicateHudGroup( - hudClipboard, - Offset(lastPointerPos[0] * s, lastPointerPos[1] * s), - ) - if (pasted.isNotEmpty()) { - Snapshot.withMutableSnapshot { - selectedHuds = pasted.toSet() - pasteMenuOffset = null + StudioCommand.Paste -> { + if (hudClipboard.isNotEmpty()) { + val s = Platform.screen().screenToMcScale() + val pasted = duplicateHudGroup( + hudClipboard, + Offset(lastPointerPos[0] * s, lastPointerPos[1] * s), + ) + if (pasted.isNotEmpty()) { + Snapshot.withMutableSnapshot { + selectedHuds = pasted.toSet() + pasteMenuOffset = null + } } + UiSounds.play(UiSoundEvent.CLICK) } - UiSounds.play(UiSoundEvent.CLICK) } - } - StudioCommand.Delete -> deleteHuds(selectedHuds) + StudioCommand.Delete -> deleteHuds(selectedHuds) - StudioCommand.SelectAll -> Snapshot.withMutableSnapshot { - selectedHuds = HudManager.activeInstances.filter { !it.locked }.toSet() - } + StudioCommand.SelectAll -> Snapshot.withMutableSnapshot { + selectedHuds = HudManager.activeInstances.filter { !it.locked }.toSet() + } - StudioCommand.Lock -> { - val targets = selectedHuds.toList() - if (targets.isNotEmpty()) { - val lock = targets.any { !it.locked } - Snapshot.withMutableSnapshot { - targets.forEach { it.locked = lock } + StudioCommand.Lock -> { + val targets = selectedHuds.toList() + if (targets.isNotEmpty()) { + val lock = targets.any { !it.locked } + Snapshot.withMutableSnapshot { + targets.forEach { it.locked = lock } + } + OneConfigConfig.INSTANCE.save() } - OneConfigConfig.INSTANCE.save() } } + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + LOGGER.error("Failed to handle $command in the HUD Design Studio", e) } } } @@ -2494,7 +2502,7 @@ fun HudDragLayer(modifier: Modifier = Modifier) { actionBarGapPx = actionBarGapPx, chromeAlpha = 1f, onDelete = { - if (actionBarTarget.deletable()) { + if (actionBarTarget.canDelete()) { Snapshot.withMutableSnapshot { hoveredHud = null if (draggedHud === actionBarTarget) { diff --git a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/runtime/PolyComposeHost.kt b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/runtime/PolyComposeHost.kt index d370f42b2..4d3a202f1 100644 --- a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/runtime/PolyComposeHost.kt +++ b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/runtime/PolyComposeHost.kt @@ -21,6 +21,8 @@ object PolyComposeHost { internal val recomposer: CompositionContext get() = recomposerImpl + private var inFrame = false + fun frame(nanos: Long = System.nanoTime()) { frameWithReport(nanos) } @@ -29,9 +31,17 @@ object PolyComposeHost { * Runs a frame like [frame] and reports whether composition content may have changed */ fun frameWithReport(nanos: Long = System.nanoTime()): Boolean { - Snapshot.sendApplyNotifications() - val appliedBefore = recomposerImpl.changeCount - clock.sendFrame(nanos) - return recomposerImpl.changeCount != appliedBefore || recomposerImpl.hasPendingWork + if (inFrame) return recomposerImpl.hasPendingWork + inFrame = true + try { + return Snapshot.global { + Snapshot.sendApplyNotifications() + val appliedBefore = recomposerImpl.changeCount + clock.sendFrame(nanos) + recomposerImpl.changeCount != appliedBefore || recomposerImpl.hasPendingWork + } + } finally { + inFrame = false + } } } From 2c6e315b216f108d08ac7c8de0927e5ad47687a3 Mon Sep 17 00:00:00 2001 From: lowercasebtw <126462578+lowercasebtw@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:09:01 -0400 Subject: [PATCH 02/16] Update minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt Co-authored-by: Luna --- .../org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt index 0eb5e4e0f..bb625a88d 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt @@ -625,11 +625,11 @@ object SkiaCtx { private fun maxTextureSize(): Int = //? if >= 26.2 { - com.mojang.blaze3d.systems.RenderSystem.getDevice().deviceInfo.limits().maxTextureSize() + RenderSystem.getDevice().deviceInfo.limits().maxTextureSize() //? } else if >= 1.21.5 { - /*com.mojang.blaze3d.systems.RenderSystem.getDevice().maxTextureSize + /*RenderSystem.getDevice().maxTextureSize *///? } else { - /*com.mojang.blaze3d.systems.RenderSystem.maxSupportedTextureSize() + /*RenderSystem.maxSupportedTextureSize() *///? } private fun viewportFitsTexture(w: Int, h: Int): Boolean { From 4d89381b66c224d7fdc4e34491b263d0457a84cb Mon Sep 17 00:00:00 2001 From: lowercasebtw <126462578+lowercasebtw@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:09:10 -0400 Subject: [PATCH 03/16] Update minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java Co-authored-by: Luna --- .../org/polyfrost/oneconfig/internal/OneConfigMixinInit.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b223d79b3..d0445b61e 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java @@ -72,7 +72,7 @@ private static boolean skyblockerBarsAreInstanced() { try (java.io.InputStream in = OneConfigMixinInit.class.getClassLoader().getResourceAsStream(path)) { if (in != null) { ClassNode node = new ClassNode(); - new org.objectweb.asm.ClassReader(in).accept(node, org.objectweb.asm.ClassReader.SKIP_CODE); + new ClassReader(in).accept(node, ClassReader.SKIP_CODE); for (org.objectweb.asm.tree.MethodNode method : node.methods) { //~ if >= 26.1 'render' -> 'extractRenderState' if (!method.name.equals("extractRenderState")) continue; From dfd800ee0f9aed79bea0c3dfcf89413f24678d0f Mon Sep 17 00:00:00 2001 From: lowercasebtw <126462578+lowercasebtw@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:09:19 -0400 Subject: [PATCH 04/16] Update minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java Co-authored-by: Luna --- .../org/polyfrost/oneconfig/internal/OneConfigMixinInit.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d0445b61e..ae978e2e1 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java @@ -73,7 +73,7 @@ private static boolean skyblockerBarsAreInstanced() { if (in != null) { ClassNode node = new ClassNode(); new ClassReader(in).accept(node, ClassReader.SKIP_CODE); - for (org.objectweb.asm.tree.MethodNode method : node.methods) { + for (MethodNode method : node.methods) { //~ if >= 26.1 'render' -> 'extractRenderState' if (!method.name.equals("extractRenderState")) continue; skyblockerInstanced = (method.access & org.objectweb.asm.Opcodes.ACC_STATIC) == 0; From fb22bdf18f8a2495083bc29570fc000d19f136fc Mon Sep 17 00:00:00 2001 From: lowercasebtw <126462578+lowercasebtw@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:09:29 -0400 Subject: [PATCH 05/16] Update minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java Co-authored-by: Luna --- .../org/polyfrost/oneconfig/internal/OneConfigMixinInit.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ae978e2e1..11dd80d36 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java @@ -76,7 +76,7 @@ private static boolean skyblockerBarsAreInstanced() { for (MethodNode method : node.methods) { //~ if >= 26.1 'render' -> 'extractRenderState' if (!method.name.equals("extractRenderState")) continue; - skyblockerInstanced = (method.access & org.objectweb.asm.Opcodes.ACC_STATIC) == 0; + skyblockerInstanced = (method.access & Opcodes.ACC_STATIC) == 0; break; } } From 22d354c80ccfc0ef30dba9eaf1f99f7e58caf080 Mon Sep 17 00:00:00 2001 From: lowercasebtw <126462578+lowercasebtw@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:09:37 -0400 Subject: [PATCH 06/16] Update modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java Co-authored-by: Luna --- .../org/polyfrost/oneconfig/api/config/v1/ConfigManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java index 9b050e56c..5f2eef947 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java @@ -260,7 +260,7 @@ static void notifyWriteFailed(Config config, Throwable cause) { String name = config.title != null ? config.title : config.id; Throwable root = cause.getCause() != null ? cause.getCause() : cause; String reason = root.getMessage(); - org.polyfrost.oneconfig.api.notifications.v1.Notifications.error(name + ": could not write config", + Notifications.error(name + ": could not write config", "OneConfig could not write to the config folder" + (reason != null ? " (" + reason + ")" : "") + ". Your settings still work this session but may not be saved. Check that your disk is not full."); } catch (Throwable t) { From ddf6ff8a027a8191fe9fcf2c6f8ad851bc6b2be4 Mon Sep 17 00:00:00 2001 From: lowercasebtw <126462578+lowercasebtw@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:09:46 -0400 Subject: [PATCH 07/16] Update modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java Co-authored-by: Luna --- .../org/polyfrost/oneconfig/api/config/v1/ConfigManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java index 5f2eef947..dadb9f3c8 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java @@ -249,7 +249,7 @@ private static void notifyResetOptions(Config config, List options) { String message = options.size() == 1 ? "The option '" + options.get(0) + "' could not be loaded and was reset to its default. A backup was saved as " + config.getTree().getID() + ".corrupted." : options.size() + " options could not be loaded and were reset to their defaults (" + String.join(", ", options) + "). A backup was saved as " + config.getTree().getID() + ".corrupted."; - org.polyfrost.oneconfig.api.notifications.v1.Notifications.error(name + ": options reset", message); + Notifications.error(name + ": options reset", message); } catch (Throwable t) { LOGGER.error("failed to notify about reset options for config {}", config.id, t); } From 16192ae3f6cb9ee3d0e8ca91fa2dd92bc22278b5 Mon Sep 17 00:00:00 2001 From: lowercasebtw Date: Wed, 2 Sep 2026 18:36:56 -0400 Subject: [PATCH 08/16] Add missing imports --- .../org/polyfrost/oneconfig/internal/OneConfigMixinInit.java | 3 +++ .../org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt | 1 + .../org/polyfrost/oneconfig/api/config/v1/ConfigManager.java | 1 + 3 files changed, 5 insertions(+) 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 11dd80d36..4f7bf785a 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java @@ -29,7 +29,10 @@ import kotlin.Unit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; //todo import org.polyfrost.oneconfig.internal.generated.RelocatedMixins; //? moul_compat { import org.polyfrost.oneconfig.internal.generated.RelocatedMixins; diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt index bb625a88d..3aafd893f 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt @@ -1,6 +1,7 @@ package org.polyfrost.oneconfig.internal.ui.compose import com.mojang.blaze3d.pipeline.TextureTarget +import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.client.Minecraft import net.minecraft.client.gui.GuiGraphicsExtractor import net.minecraft.resources.Identifier diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java index dadb9f3c8..4f91d4878 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java @@ -41,6 +41,7 @@ import org.polyfrost.oneconfig.api.config.v1.serialize.adapter.impl.OneConfigKeybindAdapter; import org.polyfrost.oneconfig.api.config.v1.serialize.impl.FileSerializer; import org.polyfrost.oneconfig.api.config.v1.serialize.impl.NightConfigSerializer; +import org.polyfrost.oneconfig.api.notifications.v1.Notifications; import java.io.IOException; import java.io.InputStream; From da5c38347d10f6bc06a690ca2a48aa288d3902a4 Mon Sep 17 00:00:00 2001 From: lowercasebtw Date: Wed, 2 Sep 2026 19:02:42 -0400 Subject: [PATCH 09/16] Fix --- .../polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt index b9ccbb688..eeb97d6a7 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt @@ -83,7 +83,7 @@ object SkyblockerCompat { val updatePositions = method("updatePositions", java.lang.Boolean.TYPE) val isHealthFancyBarEnabled = method("isHealthFancyBarEnabled") //~ if >= 26.1 'render' -> 'extractRenderState' - val extractRenderState = method("extractRenderState", GuiGraphicsExtractor::class.java, Minecraft::class.java) + val extract = method("extractRenderState", GuiGraphicsExtractor::class.java, Minecraft::class.java) val self = runCatching { cls.getField("INSTANCE").get(null) }.getOrNull() ?: runCatching { cls.getMethod("getInstance").invoke(null) }.getOrNull() @@ -91,14 +91,14 @@ object SkyblockerCompat { val modifiers = intArrayOf( statusBars.modifiers, barPositioner.modifiers, saveBarConfig.modifiers, placeBarsInPositioner.modifiers, updatePositions.modifiers, - isHealthFancyBarEnabled.modifiers, extractRenderState.modifiers, + isHealthFancyBarEnabled.modifiers, extract.modifiers, ) if (modifiers.any { !Modifier.isStatic(it) }) error("FancyStatusBars is not static and has no instance") } return Bars( self, statusBars, barPositioner, saveBarConfig, - placeBarsInPositioner, updatePositions, isHealthFancyBarEnabled, extractRenderState, + placeBarsInPositioner, updatePositions, isHealthFancyBarEnabled, extract, ) } From a6cfad91650f7a400de90f1d9af50418cb62fb58 Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Thu, 3 Sep 2026 05:05:40 -0400 Subject: [PATCH 10/16] rewrite skyblocker compat fixes --- .../internal/OneConfigMixinInit.java | 35 ------ .../Mixin_SkyblockerFancyStatusBars.java | 9 +- .../Mixin_SkyblockerFancyStatusBarsV2.java | 31 ----- .../internal/compat/SkyblockerCompat.kt | 107 +++++++----------- 4 files changed, 45 insertions(+), 137 deletions(-) delete mode 100644 minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsV2.java 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 4f7bf785a..0be1d5bdc 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java @@ -29,10 +29,7 @@ import kotlin.Unit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; //todo import org.polyfrost.oneconfig.internal.generated.RelocatedMixins; //? moul_compat { import org.polyfrost.oneconfig.internal.generated.RelocatedMixins; @@ -45,7 +42,6 @@ import java.util.Set; public class OneConfigMixinInit implements IMixinConfigPlugin { - private static final Logger LOGGER = LogManager.getLogger("OneConfig/MixinInit"); @Override public void onLoad(String mixinPackage) { @@ -58,39 +54,9 @@ public String getRefMapperConfig() { @Override public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { - //? skyblocker_compat { - if (mixinClassName.endsWith(".Mixin_SkyblockerFancyStatusBarsV2")) return skyblockerBarsAreInstanced(); - if (mixinClassName.endsWith(".Mixin_SkyblockerFancyStatusBars")) return !skyblockerBarsAreInstanced(); - //? } return true; } - //? skyblocker_compat { - private static Boolean skyblockerInstanced; - - private static boolean skyblockerBarsAreInstanced() { - if (skyblockerInstanced == null) { - skyblockerInstanced = false; - String path = "de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.class"; - try (java.io.InputStream in = OneConfigMixinInit.class.getClassLoader().getResourceAsStream(path)) { - if (in != null) { - ClassNode node = new ClassNode(); - new ClassReader(in).accept(node, ClassReader.SKIP_CODE); - for (MethodNode method : node.methods) { - //~ if >= 26.1 'render' -> 'extractRenderState' - if (!method.name.equals("extractRenderState")) continue; - skyblockerInstanced = (method.access & Opcodes.ACC_STATIC) == 0; - break; - } - } - } catch (Throwable t) { - LOGGER.warn("Could not read the installed Skyblocker's status bar shape, assuming static", t); - } - } - return skyblockerInstanced; - } - //? } - @Override public void acceptTargets(Set myTargets, Set otherTargets) { } @@ -152,7 +118,6 @@ public List getMixins() { //? skyblocker_compat { mixins.add("compat.skyblocker.Mixin_SkyblockerFancyStatusBars"); - mixins.add("compat.skyblocker.Mixin_SkyblockerFancyStatusBarsV2"); mixins.add("compat.skyblocker.Mixin_SkyblockerWidgetManager"); //? } diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java index 2802e0c30..965b53533 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java @@ -15,7 +15,12 @@ @Mixin(FancyStatusBars.class) public class Mixin_SkyblockerFancyStatusBars { - @Inject(method = "init", at = @At("TAIL"), require = 0) + @Inject(method = "init", at = @At("TAIL"), require = 0, expect = 0) + private static void oneconfig$registerHudCompatStatic(CallbackInfo ci) { + SkyblockerCompat.initialize(); + } + + @Inject(method = "initStatic", at = @At("TAIL"), require = 0, expect = 0) private static void oneconfig$registerHudCompat(CallbackInfo ci) { SkyblockerCompat.initialize(); } @@ -23,7 +28,7 @@ public class Mixin_SkyblockerFancyStatusBars { //~ if >= 26.1 'render' -> 'extractRenderState' @Inject(method = "extractRenderState", at = @At("HEAD"), cancellable = true, require = 0) private static void oneconfig$suppressWhileEditing(CallbackInfoReturnable cir) { - if (CompatOverlayRenderer.oneConfigScreenOpen() && !SkyblockerCompat.isRedrawing()) { + if (SkyblockerCompat.isActive() && CompatOverlayRenderer.oneConfigScreenOpen() && !SkyblockerCompat.isRedrawing()) { cir.setReturnValue(false); } } diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsV2.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsV2.java deleted file mode 100644 index 41d555c7f..000000000 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsV2.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.polyfrost.oneconfig.internal.mixin.compat.skyblocker; - -//? skyblocker_compat { -import de.hysky.skyblocker.skyblock.fancybars.FancyStatusBars; -import org.polyfrost.oneconfig.internal.compat.SkyblockerCompat; -import org.polyfrost.oneconfig.internal.ui.hud.CompatOverlayRenderer; -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; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; - -@Pseudo -@Mixin(FancyStatusBars.class) -public class Mixin_SkyblockerFancyStatusBarsV2 { - - @Inject(method = "initStatic", at = @At("TAIL"), require = 0) - private static void oneconfig$registerHudCompat(CallbackInfo ci) { - SkyblockerCompat.initialize(); - } - - //~ if >= 26.1 'render' -> 'extractRenderState' - @Inject(method = "extractRenderState", at = @At("HEAD"), cancellable = true, require = 0) - private void oneconfig$suppressWhileEditing(CallbackInfoReturnable cir) { - if (CompatOverlayRenderer.oneConfigScreenOpen() && !SkyblockerCompat.isRedrawing()) { - cir.setReturnValue(false); - } - } -} -//? } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt index eeb97d6a7..89d7c128f 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt @@ -21,7 +21,6 @@ import org.polyfrost.oneconfig.internal.ui.hud.CompatOverlayRenderer import java.awt.Color import java.lang.reflect.Field import java.lang.reflect.Method -import java.lang.reflect.Modifier import java.util.function.Consumer import kotlin.math.abs @@ -43,95 +42,64 @@ object SkyblockerCompat { @Volatile private var dragged: StatusBar? = null - private class Bars( - val self: Any?, - val statusBars: Field, - val barPositioner: Field, - val saveBarConfig: Method, - val placeBarsInPositioner: Method, - val updatePositions: Method, - val isHealthFancyBarEnabled: Method, - val extractRenderState: Method, - ) + private val cls = FancyStatusBars::class.java - private var barsResolved = false - private var barsCache: Bars? = null - - private fun bars(): Bars? { - if (!barsResolved) { - barsResolved = true - barsCache = try { - resolveBars() - } catch (e: Throwable) { - LOGGER.warn("Disabling Skyblocker status bars: FancyStatusBars has an unrecognised shape", e) - null - } - } - return barsCache + private val self: Any? by lazy { + runCatching { cls.getField("INSTANCE").get(null) }.getOrNull() + ?: runCatching { cls.getMethod("getInstance").invoke(null) }.getOrNull() } - private fun resolveBars(): Bars { - val cls = FancyStatusBars::class.java - fun field(name: String) = cls.getDeclaredField(name).apply { isAccessible = true } - fun method(name: String, vararg params: Class<*>) = - cls.getDeclaredMethod(name, *params).apply { isAccessible = true } - - val statusBars = field("statusBars") - val barPositioner = field("barPositioner") - val saveBarConfig = method("saveBarConfig") - val placeBarsInPositioner = method("placeBarsInPositioner") - val updatePositions = method("updatePositions", java.lang.Boolean.TYPE) - val isHealthFancyBarEnabled = method("isHealthFancyBarEnabled") + private fun field(name: String): Field? = runCatching { + cls.getDeclaredField(name).apply { isAccessible = true } + }.onFailure { LOGGER.warn("Skyblocker FancyStatusBars.{} is unavailable", name, it) }.getOrNull() + + private fun method(name: String, vararg params: Class<*>): Method? = runCatching { + cls.getDeclaredMethod(name, *params).apply { isAccessible = true } + }.onFailure { LOGGER.warn("Skyblocker FancyStatusBars.{}() is unavailable", name, it) }.getOrNull() + + private val statusBarsField by lazy { field("statusBars") } + private val barPositionerField by lazy { field("barPositioner") } + private val saveBarConfigMethod by lazy { method("saveBarConfig") } + private val placeBarsMethod by lazy { method("placeBarsInPositioner") } + private val updatePositionsMethod by lazy { method("updatePositions", java.lang.Boolean.TYPE) } + private val healthFancyBarMethod by lazy { method("isHealthFancyBarEnabled") } + private val renderBarsMethod by lazy { //~ if >= 26.1 'render' -> 'extractRenderState' - val extract = method("extractRenderState", GuiGraphicsExtractor::class.java, Minecraft::class.java) - - val self = runCatching { cls.getField("INSTANCE").get(null) }.getOrNull() - ?: runCatching { cls.getMethod("getInstance").invoke(null) }.getOrNull() - if (self == null) { - val modifiers = intArrayOf( - statusBars.modifiers, barPositioner.modifiers, saveBarConfig.modifiers, - placeBarsInPositioner.modifiers, updatePositions.modifiers, - isHealthFancyBarEnabled.modifiers, extract.modifiers, - ) - if (modifiers.any { !Modifier.isStatic(it) }) error("FancyStatusBars is not static and has no instance") - } + method("extractRenderState", GuiGraphicsExtractor::class.java, Minecraft::class.java) + } - return Bars( - self, statusBars, barPositioner, saveBarConfig, - placeBarsInPositioner, updatePositions, isHealthFancyBarEnabled, extract, - ) + private val active: Boolean by lazy { + val ok = renderBarsMethod != null && runCatching { statusBarsField?.get(self) }.getOrNull() != null + if (!ok) LOGGER.warn("Skyblocker status bar compat is off: FancyStatusBars has an unrecognised shape") + ok } + @JvmStatic + fun isActive(): Boolean = active + @Suppress("UNCHECKED_CAST") - private fun statusBars(): Map { - val bars = bars() ?: return emptyMap() - return bars.statusBars.get(bars.self) as? Map ?: emptyMap() - } + private fun statusBars(): Map = + runCatching { statusBarsField?.get(self) as? Map }.getOrNull() ?: emptyMap() - private fun positioner(): BarPositioner { - val bars = bars() ?: error("Skyblocker status bars are unavailable") - return bars.barPositioner.get(bars.self) as BarPositioner - } + private fun positioner(): BarPositioner = + checkNotNull(barPositionerField?.get(self) as? BarPositioner) { "Skyblocker barPositioner is unavailable" } private fun saveBars() { - bars()?.let { it.saveBarConfig.invoke(it.self) } + saveBarConfigMethod?.invoke(self) } private fun placeBars() { - bars()?.let { it.placeBarsInPositioner.invoke(it.self) } + placeBarsMethod?.invoke(self) } private fun updatePositions(ignoreVisibility: Boolean) { - bars()?.let { it.updatePositions.invoke(it.self, ignoreVisibility) } + updatePositionsMethod?.invoke(self, ignoreVisibility) } - private fun healthFancyBarEnabled(): Boolean { - val bars = bars() ?: return false - return bars.isHealthFancyBarEnabled.invoke(bars.self) as? Boolean == true - } + private fun healthFancyBarEnabled(): Boolean = healthFancyBarMethod?.invoke(self) as? Boolean == true private fun renderStatusBars(ctx: GuiGraphicsExtractor, mc: Minecraft) { - bars()?.let { it.extractRenderState.invoke(it.self, ctx, mc) } + renderBarsMethod?.invoke(self, ctx, mc) } @JvmStatic @@ -145,6 +113,7 @@ object SkyblockerCompat { } private fun register() { + if (!isActive()) return var count = 0 for (type in StatusBarType.values()) { runCatching { From 337297ee2aacf04f45e59854f444dff56760cc81 Mon Sep 17 00:00:00 2001 From: lowercasebtw Date: Mon, 7 Sep 2026 21:14:10 -0400 Subject: [PATCH 11/16] chore: update hud api dump apiCheck fails on upstream/v1 as-is: d5e4a50f added OneConfigHudWrapper .ownsPlacement and bc83fc34 added HudEditorToggleEvent.SCREEN_REMOVED and .screenAlreadyGone, but neither re-ran apiDump. Regenerated; the four entries below are the only change. --- modules/hud/api/hud.api | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/hud/api/hud.api b/modules/hud/api/hud.api index 9c901d643..63a3dec70 100644 --- a/modules/hud/api/hud.api +++ b/modules/hud/api/hud.api @@ -329,6 +329,7 @@ public abstract interface class org/polyfrost/oneconfig/api/hud/v1/OneConfigHudW public abstract fun getId ()Ljava/lang/String; public fun getModId ()Ljava/lang/String; public abstract fun getName ()Ljava/lang/String; + public fun getOwnsPlacement ()Z public fun getPlacementReady ()Z public fun getResizeAxes ()Lorg/polyfrost/oneconfig/api/hud/v1/HudResize; public abstract fun getScale ()F @@ -355,6 +356,7 @@ public abstract interface class org/polyfrost/oneconfig/api/hud/v1/OneConfigHudW public final class org/polyfrost/oneconfig/api/hud/v1/OneConfigHudWrapper$DefaultImpls { public static fun getHidden (Lorg/polyfrost/oneconfig/api/hud/v1/OneConfigHudWrapper;)Z public static fun getModId (Lorg/polyfrost/oneconfig/api/hud/v1/OneConfigHudWrapper;)Ljava/lang/String; + public static fun getOwnsPlacement (Lorg/polyfrost/oneconfig/api/hud/v1/OneConfigHudWrapper;)Z public static fun getPlacementReady (Lorg/polyfrost/oneconfig/api/hud/v1/OneConfigHudWrapper;)Z public static fun getResizeAxes (Lorg/polyfrost/oneconfig/api/hud/v1/OneConfigHudWrapper;)Lorg/polyfrost/oneconfig/api/hud/v1/HudResize; public static fun getSupportsScale (Lorg/polyfrost/oneconfig/api/hud/v1/OneConfigHudWrapper;)Z @@ -444,6 +446,8 @@ public final class org/polyfrost/oneconfig/api/hud/v1/Weight : java/lang/Enum { public class org/polyfrost/oneconfig/api/hud/v1/events/HudEditorToggleEvent : org/polyfrost/oneconfig/api/event/v1/events/Event { public static final field CLOSE Lorg/polyfrost/oneconfig/api/hud/v1/events/HudEditorToggleEvent; public static final field OPEN Lorg/polyfrost/oneconfig/api/hud/v1/events/HudEditorToggleEvent; + public static final field SCREEN_REMOVED Lorg/polyfrost/oneconfig/api/hud/v1/events/HudEditorToggleEvent; public final field open Z + public final field screenAlreadyGone Z } From 87a57d29a9181b00931abf4ce3797fc660a95bd1 Mon Sep 17 00:00:00 2001 From: lowercasebtw Date: Mon, 7 Sep 2026 22:20:21 -0400 Subject: [PATCH 12/16] Fix Conflicts & Requested Changes --- .../Mixin_SkyblockerFancyStatusBars.java | 4 +- .../oneconfig/api/config/v1/Config.java | 20 ++++++++- .../api/config/v1/ConfigManager.java | 21 +++++++-- .../api/hud/v1/OneConfigHudCompat.kt | 45 +++++++++++++------ 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java index 965b53533..96b823cd1 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java @@ -16,12 +16,12 @@ public class Mixin_SkyblockerFancyStatusBars { @Inject(method = "init", at = @At("TAIL"), require = 0, expect = 0) - private static void oneconfig$registerHudCompatStatic(CallbackInfo ci) { + private static void oneconfig$registerHudCompat(CallbackInfo ci) { SkyblockerCompat.initialize(); } @Inject(method = "initStatic", at = @At("TAIL"), require = 0, expect = 0) - private static void oneconfig$registerHudCompat(CallbackInfo ci) { + private static void oneconfig$registerHudCompatStatic(CallbackInfo ci) { SkyblockerCompat.initialize(); } diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java index 19a2d184b..3ab5dc607 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java @@ -88,6 +88,10 @@ private void whenInitialized(Runnable action) { deferredSetup.add(action); return; } + if (tree == null && ConfigManager.didInitializationFail(this)) { + ConfigManager.LOGGER.warn("Skipping deferred setup for config {}: its initialization failed", id); + return; + } action.run(); } @@ -345,12 +349,24 @@ protected void addDependency(String option, String name, Supplier failedInitialization = ConcurrentHashMap.newKeySet(); + + static boolean didInitializationFail(Config config) { + return failedInitialization.contains(config.id); + } + @ApiStatus.Internal public static void submitForInitialization(Config config) { // never initialize synchronously here because this runs from the Config base constructor @@ -260,14 +269,18 @@ private static void notifyResetOptions(Config config, List options) { } } + private static final AtomicBoolean writeFailureNotified = new AtomicBoolean(); + static void notifyWriteFailed(Config config, Throwable cause) { + if (!writeFailureNotified.compareAndSet(false, true)) return; try { - String name = config.title != null ? config.title : config.id; Throwable root = cause.getCause() != null ? cause.getCause() : cause; String reason = root.getMessage(); - Notifications.error(name + ": could not write config", - "OneConfig could not write to the config folder" + (reason != null ? " (" + reason + ")" : "") - + ". Your settings still work this session but may not be saved. Check that your disk is not full."); + Notifications.error("OneConfig: could not write to the config folder", + "Saving the defaults backup for '" + (config.title != null ? config.title : config.id) + "' failed" + + (reason != null ? " (" + reason + ")" : "") + + ". Your settings still load and save normally, but resetting an option to its default" + + " may not work. Check that your disk is not full and the config folder is writable."); } catch (Throwable t) { LOGGER.error("failed to notify about the write failure for config {}", config.id, t); } diff --git a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt index 4f01e0e0f..e175eb7cc 100644 --- a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt +++ b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt @@ -16,12 +16,16 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : private val hiddenRevision = mutableStateOf(0) + private val wrapperId = id + private val wrapperModId = runCatching { wrapper.modId }.getOrNull() ?: "unknown" + + @Volatile private var faulted = false private fun fault(member: String, error: Throwable) { faulted = true HudManager.LOGGER.error( - "Disabling compat HUD '${wrapper.id}' from '${wrapper.modId ?: "unknown"}': $member failed, so " + + "Disabling compat HUD '$wrapperId' from '$wrapperModId': $member failed, so " + "that mod is probably a different version than OneConfig was built against", error, ) @@ -46,14 +50,26 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : return guard("hidden", true) { wrapper.hidden } } set(value) { - guard("hidden", Unit) { - if (wrapper.hidden != value) { + val changed = guard("hidden", false) { + if (wrapper.hidden == value) false else { wrapper.hidden = value - hiddenRevision.value++ + true } } + if (changed) hiddenRevision.value++ } + fun guardedPlacementReady(): Boolean = guard("placementReady", false) { wrapper.placementReady } + + fun guardedOwnsPlacement(): Boolean = guard("ownsPlacement", false) { wrapper.ownsPlacement } + + fun guardedLinkedProperties(): List> = + guard("linkedProperties", emptyList()) { wrapper.linkedProperties() } + + fun guardedSave() { + guard("save", Unit) { wrapper.save() } + } + override val persistOwnState: Boolean get() = false override val profileLocalTree: Boolean get() = false @@ -176,22 +192,25 @@ interface OneConfigHudWrapper { hud.setup() val tree = hud.tree if (tree != null) { - for (prop in linkedProperties()) tree.put(prop) - trackPlacementPerProfile(tree) + for (prop in hud.guardedLinkedProperties()) tree.put(prop) + trackPlacementPerProfile(hud, tree) } hud.captureStaticSizeDefaults() hud.capturePositionDefaults() } - private fun trackPlacementPerProfile(tree: Tree) { + private fun trackPlacementPerProfile(hud: OneConfigHudCompat, tree: Tree) { excludeFromSnapshots(tree) - tree.addMetadata(CompatSnapshots.GATE_METADATA, java.util.function.BooleanSupplier { placementReady }) - if (!ownsPlacement) { - tree["oc_compat_x"] = placementProperty("x", "X Position", { x }, { x = it }) - tree["oc_compat_y"] = placementProperty("y", "Y Position", { y }, { y = it }) - if (supportsScale) tree["oc_compat_scale"] = placementProperty("scale", "Scale", { scale }, { scale = it }) + tree.addMetadata(CompatSnapshots.GATE_METADATA, java.util.function.BooleanSupplier { hud.guardedPlacementReady() }) + if (!hud.guardedOwnsPlacement()) { + tree["oc_compat_x"] = placementProperty("x", "X Position", { hud.x }, { hud.x = it }) + tree["oc_compat_y"] = placementProperty("y", "Y Position", { hud.y }, { hud.y = it }) + if (hud.supportsScale) { + tree["oc_compat_scale"] = + placementProperty("scale", "Scale", { hud.customScale }, { hud.customScale = it }) + } } - tree.addMetadata("custom_save", Runnable { save() }) + tree.addMetadata("custom_save", Runnable { hud.guardedSave() }) CompatSnapshots.track(tree) } From 5c639da2080dd24ddfc343936490558c3c31ec36 Mon Sep 17 00:00:00 2001 From: lowercasebtw Date: Thu, 10 Sep 2026 01:41:17 -0400 Subject: [PATCH 13/16] Fix Sentry Issues 72, 71, 7C, 7A, 77, 7D, DS, T0, 9E, 1XR, 1B1, 29G, 29D, 29C, 2AR, 10B, 17R, 27M, BF, KC, 9, 10 --- .../internal/CompatibilityPlatformImpl.java | 5 ++ .../internal/compat/SkyblockerCompat.kt | 16 ++++- .../oneconfig/internal/compat/StellaCompat.kt | 28 ++++---- .../item/MinecraftItemCatalogService.kt | 1 + .../ui/compose/ComposeSceneContextImpl.kt | 6 +- .../internal/ui/compose/ComposeScreen.kt | 2 +- .../internal/ui/compose/ComposeSupport.kt | 1 + .../oneconfig/internal/ui/compose/SkiaCtx.kt | 70 +++++++++++++------ .../internal/ui/sound/McUiSoundService.kt | 3 +- .../notifications/v1/NotificationsManager.kt | 3 +- 10 files changed, 92 insertions(+), 43 deletions(-) diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/api/platform/v1/internal/CompatibilityPlatformImpl.java b/minecraft/src/main/java/org/polyfrost/oneconfig/api/platform/v1/internal/CompatibilityPlatformImpl.java index 2a2ed7c60..094ff1383 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/api/platform/v1/internal/CompatibilityPlatformImpl.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/api/platform/v1/internal/CompatibilityPlatformImpl.java @@ -23,6 +23,11 @@ public class CompatibilityPlatformImpl implements CompatibilityPlatform { @Override public void displayChatMessage(Component text) { + Minecraft minecraft = Minecraft.getInstance(); + if (!minecraft.isSameThread()) { + minecraft.execute(() -> displayChatMessage(text)); + return; + } //? if >=1.21.4 { MinecraftClientAudiences.of().audience().sendMessage(text); //?} else { diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt index c2c6bfe32..4d410dc98 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt @@ -40,8 +40,19 @@ object SkyblockerCompat { @Volatile private var dragged: StatusBar? = null + private var barsUnavailable = false + + private fun noStatusBars(error: Throwable): Map { + if (!barsUnavailable) { + barsUnavailable = true + LOGGER.warn("Skyblocker's status bar API is unavailable, its bars will be left out of the OneConfig HUD editor", error) + } + return emptyMap() + } + //? if skyblocker_hud_v2 { - private fun statusBars(): Map = FancyStatusBars.INSTANCE.statusBars + private fun statusBars(): Map = + runCatching { FancyStatusBars.INSTANCE.statusBars }.getOrElse { noStatusBars(it) } private fun positioner(): BarPositioner = FancyStatusBars.INSTANCE.barPositioner @@ -57,7 +68,8 @@ object SkyblockerCompat { FancyStatusBars.INSTANCE.extractRenderState(ctx, mc) } //?} else { - /*private fun statusBars(): Map = FancyStatusBars.statusBars + /*private fun statusBars(): Map = + runCatching { FancyStatusBars.statusBars }.getOrElse { noStatusBars(it) } private fun positioner(): BarPositioner = FancyStatusBars.barPositioner diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/StellaCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/StellaCompat.kt index 355ec0f19..d771b8bcd 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/StellaCompat.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/StellaCompat.kt @@ -269,21 +269,23 @@ object StellaCompat { if (!element.isEnabled()) return@forEach ctx.pose().pushMatrix() - ctx.pose().translate(element.x, element.y) - ctx.pose().scale(element.scale, element.scale) - - val custom = customRenderers[element.id] - if (custom != null) custom(ctx) - else { - if (element.width == 0 && element.height == 0) { - element.width = element.text.width() + 4 - element.height = element.text.height() + 6 + try { + ctx.pose().translate(element.x, element.y) + ctx.pose().scale(element.scale, element.scale) + + val custom = customRenderers[element.id] + if (custom != null) custom(ctx) + else { + if (element.width == 0 && element.height == 0) { + element.width = element.text.width() + 4 + element.height = element.text.height() + 6 + } + + Render2D.drawString(ctx, element.text, 2, 3, shadow = false) } - - Render2D.drawString(ctx, element.text, 2, 3, shadow = false) + } finally { + ctx.pose().popMatrix() } - - ctx.pose().popMatrix() } } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/item/MinecraftItemCatalogService.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/item/MinecraftItemCatalogService.kt index b5fc8ae36..41e6998b0 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/item/MinecraftItemCatalogService.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/item/MinecraftItemCatalogService.kt @@ -123,6 +123,7 @@ class MinecraftItemCatalogService : ItemCatalogService { val viewportWidth = Platform.screen().viewportWidth() val viewportHeight = Platform.screen().viewportHeight() if ( + Minecraft.getInstance().player == null || guiWidth <= 0 || guiHeight <= 0 || windowWidth <= 0 || windowHeight <= 0 || viewportWidth <= 0 || viewportHeight <= 0 diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeSceneContextImpl.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeSceneContextImpl.kt index 9d9b257d2..a79aaab7c 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeSceneContextImpl.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeSceneContextImpl.kt @@ -99,7 +99,11 @@ private class PlatformImpl : PlatformContext { } fun resetPointerIcon() { - applyPointerIcon(PointerIcon.Default) + //? if >= 26.3 { + /*SDL_SetCursor(SDL_GetDefaultCursor()) + *///?} else { + glfwSetCursor(handle, 0L) + //?} } private fun applyPointerIcon(pointerIcon: PointerIcon) { diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt index f453bc7c0..5273e30ef 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt @@ -691,7 +691,7 @@ abstract class ComposeScreen( //? >= 1.21.10 { override fun charTyped(event: CharacterEvent): Boolean { - val char = Char(event.codepoint) + val char = if (Character.isBmpCodePoint(event.codepoint)) Char(event.codepoint) else KeyEvent.CHAR_UNDEFINED val codepoint = event.codepoint //? >= 26.1 { val modifiers = 0 //dropped from the event in 26.1 because glfw no longer passes them diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeSupport.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeSupport.kt index d91ba4322..886c43e4f 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeSupport.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeSupport.kt @@ -63,6 +63,7 @@ object ComposeSupport { private fun awtInitFailure(): String? = try { Class.forName("java.awt.event.KeyEvent", true, ComposeSupport::class.java.classLoader) + Class.forName("androidx.compose.ui.input.pointer.PointerIcon", true, ComposeSupport::class.java.classLoader) null } catch (t: Throwable) { LOG.error("AWT failed to initialize on this runtime; the OneConfig UI has been disabled.", t) diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt index 659032142..b135342ba 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt @@ -488,14 +488,20 @@ object SkiaCtx { hudRealIsGeneral = true } guiGraphics.pose().pushMatrix() - guiGraphics.pose().scale(1f / guiScale, 1f / guiScale) - guiGraphics.blit(RenderPipelines.GUI_TEXTURED_PREMULTIPLIED_ALPHA, HUD_TEXTURE_LOC, 0, 0, 0f, 0f, w, h, w, h) - guiGraphics.pose().popMatrix() + try { + guiGraphics.pose().scale(1f / guiScale, 1f / guiScale) + guiGraphics.blit(RenderPipelines.GUI_TEXTURED_PREMULTIPLIED_ALPHA, HUD_TEXTURE_LOC, 0, 0, 0f, 0f, w, h, w, h) + } finally { + guiGraphics.pose().popMatrix() + } //? } else { /*guiGraphics.pose().pushPose() - guiGraphics.pose().scale(1f / guiScale, 1f / guiScale, 1f) - guiGraphics.blit(::premulGuiTextured, HUD_TEXTURE_LOC, 0, 0, 0f, 0f, w, h, w, h) - guiGraphics.pose().popPose() + try { + guiGraphics.pose().scale(1f / guiScale, 1f / guiScale, 1f) + guiGraphics.blit(::premulGuiTextured, HUD_TEXTURE_LOC, 0, 0, 0f, 0f, w, h, w, h) + } finally { + guiGraphics.pose().popPose() + } *///? } //? } else { /*var wrapper = hudTextureWrapper @@ -506,6 +512,7 @@ object SkiaCtx { } wrapper.setGlTexId(rt.colorTextureId) guiGraphics.pose().pushPose() + try { guiGraphics.pose().scale(1f / guiScale, 1f / guiScale, 1f) //? >= 1.21.4 { guiGraphics.blit(::premulGuiTextured, HUD_TEXTURE_LOC, 0, 0, 0f, 0f, w, h, w, h) @@ -521,7 +528,9 @@ object SkiaCtx { RenderSystem.disableBlend() RenderSystem.defaultBlendFunc() *///?} - guiGraphics.pose().popPose() + } finally { + guiGraphics.pose().popPose() + } *///? } } @@ -548,14 +557,20 @@ object SkiaCtx { composeRealIsGeneral = true } guiGraphics.pose().pushMatrix() - guiGraphics.pose().scale(1f / guiScale, 1f / guiScale) - guiGraphics.blit(RenderPipelines.GUI_TEXTURED_PREMULTIPLIED_ALPHA, COMPOSE_TEXTURE_LOC, 0, 0, 0f, 0f, w, h, w, h) - guiGraphics.pose().popMatrix() + try { + guiGraphics.pose().scale(1f / guiScale, 1f / guiScale) + guiGraphics.blit(RenderPipelines.GUI_TEXTURED_PREMULTIPLIED_ALPHA, COMPOSE_TEXTURE_LOC, 0, 0, 0f, 0f, w, h, w, h) + } finally { + guiGraphics.pose().popMatrix() + } //? } else { /*guiGraphics.pose().pushPose() - guiGraphics.pose().scale(1f / guiScale, 1f / guiScale, 1f) - guiGraphics.blit(::premulGuiTextured, COMPOSE_TEXTURE_LOC, 0, 0, 0f, 0f, w, h, w, h) - guiGraphics.pose().popPose() + try { + guiGraphics.pose().scale(1f / guiScale, 1f / guiScale, 1f) + guiGraphics.blit(::premulGuiTextured, COMPOSE_TEXTURE_LOC, 0, 0, 0f, 0f, w, h, w, h) + } finally { + guiGraphics.pose().popPose() + } *///? } //? } else { /*var wrapper = composeTextureWrapper @@ -566,6 +581,7 @@ object SkiaCtx { } wrapper.setGlTexId(rt.colorTextureId) guiGraphics.pose().pushPose() + try { guiGraphics.pose().scale(1f / guiScale, 1f / guiScale, 1f) //? >= 1.21.4 { guiGraphics.blit(::premulGuiTextured, COMPOSE_TEXTURE_LOC, 0, 0, 0f, 0f, w, h, w, h) @@ -581,7 +597,9 @@ object SkiaCtx { RenderSystem.disableBlend() RenderSystem.defaultBlendFunc() *///?} - guiGraphics.pose().popPose() + } finally { + guiGraphics.pose().popPose() + } *///? } } @@ -730,16 +748,22 @@ object SkiaCtx { var rt = hudTarget val needNewTarget = rt == null || rt.width != w || rt.height != h if (needNewTarget) { + if (System.currentTimeMillis() - composeAllocFailedAt < ALLOC_RETRY_COOLDOWN_MS) return null destroyHudTarget() - //? if >= 26.2 { - rt = TextureTarget(null, w, h, true, com.mojang.blaze3d.GpuFormat.RGBA8_UNORM) - //? } else if >= 1.21.5 { - /*rt = TextureTarget(null, w, h, true) - *///? } else if >= 1.21.4 { - // rt = TextureTarget(w, h, true) - //? } else { - /*rt = TextureTarget(w, h, true, Minecraft.ON_OSX) - *///? } + rt = try { + //? if >= 26.2 { + TextureTarget(null, w, h, true, com.mojang.blaze3d.GpuFormat.RGBA8_UNORM) + //? } else if >= 1.21.5 { + /*TextureTarget(null, w, h, true) + *///? } else if >= 1.21.4 { + // TextureTarget(w, h, true) + //? } else { + /*TextureTarget(w, h, true, Minecraft.ON_OSX) + *///? } + } catch (e: Throwable) { + onComposeAllocFailure(w, h, e) + return null + } hudTarget = rt //? >= 1.21.5 { diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/sound/McUiSoundService.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/sound/McUiSoundService.kt index 62dbdf21b..9755db982 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/sound/McUiSoundService.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/sound/McUiSoundService.kt @@ -14,7 +14,6 @@ import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong class McUiSoundService : UiSoundService { - private val random = RandomSource.create() private val sliderTick = AtomicInteger(0) @Volatile @@ -237,7 +236,7 @@ class McUiSoundService : UiSoundService { @Volatile var targetVolume: Float, val theme: UiSoundTheme, private val nativeLoop: Boolean, - ) : AbstractTickableSoundInstance(event, source, random) { + ) : AbstractTickableSoundInstance(event, source, RandomSource.create()) { @Volatile private var fadingOut = false diff --git a/modules/notifications/src/main/kotlin/org/polyfrost/oneconfig/api/notifications/v1/NotificationsManager.kt b/modules/notifications/src/main/kotlin/org/polyfrost/oneconfig/api/notifications/v1/NotificationsManager.kt index 5709ed31d..daf2c9481 100644 --- a/modules/notifications/src/main/kotlin/org/polyfrost/oneconfig/api/notifications/v1/NotificationsManager.kt +++ b/modules/notifications/src/main/kotlin/org/polyfrost/oneconfig/api/notifications/v1/NotificationsManager.kt @@ -45,7 +45,8 @@ object NotificationsManager { } private fun mutate(block: () -> Unit) { - if (Snapshot.current.readOnly) block() else Snapshot.withMutableSnapshot(block) + if (Snapshot.current.readOnly) Snapshot.global { Snapshot.withMutableSnapshot(block) } + else Snapshot.withMutableSnapshot(block) } /** From c46457238d7aa70840f3ad5711a653fa32b2a6ff Mon Sep 17 00:00:00 2001 From: lowercasebtw Date: Thu, 10 Sep 2026 02:19:37 -0400 Subject: [PATCH 14/16] Fix merge conflict issue --- .../internal/compat/SkyblockerCompat.kt | 42 +++++-------------- .../oneconfig/internal/ui/compose/SkiaCtx.kt | 14 +++++++ 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt index ea024041b..934ba5917 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt @@ -35,6 +35,7 @@ object SkyblockerCompat { private var initialized = false private var dirty = false + private var barsUnavailable = false @Volatile private var redrawing = false @@ -43,41 +44,11 @@ object SkyblockerCompat { private var dragged: StatusBar? = null private val cls = FancyStatusBars::class.java - private var barsUnavailable = false - - private fun noStatusBars(error: Throwable): Map { - if (!barsUnavailable) { - barsUnavailable = true - LOGGER.warn("Skyblocker's status bar API is unavailable, its bars will be left out of the OneConfig HUD editor", error) - } - return emptyMap() - } - - //? if skyblocker_hud_v2 { - private fun statusBars(): Map = - runCatching { FancyStatusBars.INSTANCE.statusBars }.getOrElse { noStatusBars(it) } - - private fun positioner(): BarPositioner = FancyStatusBars.INSTANCE.barPositioner - - private fun saveBars() = FancyStatusBars.INSTANCE.saveBarConfig() - - private fun placeBars() = FancyStatusBars.INSTANCE.placeBarsInPositioner() - - private fun updatePositions(ignoreVisibility: Boolean) = FancyStatusBars.INSTANCE.updatePositions(ignoreVisibility) - - private fun healthFancyBarEnabled(): Boolean = FancyStatusBars.INSTANCE.isHealthFancyBarEnabled() private val self: Any? by lazy { runCatching { cls.getField("INSTANCE").get(null) }.getOrNull() ?: runCatching { cls.getMethod("getInstance").invoke(null) }.getOrNull() } - //?} else { - /*private fun statusBars(): Map = - runCatching { FancyStatusBars.statusBars }.getOrElse { noStatusBars(it) } - - private fun positioner(): BarPositioner = FancyStatusBars.barPositioner - - private fun saveBars() = FancyStatusBars.saveBarConfig() private fun field(name: String): Field? = runCatching { cls.getDeclaredField(name).apply { isAccessible = true } @@ -107,9 +78,18 @@ object SkyblockerCompat { @JvmStatic fun isActive(): Boolean = active + private fun noStatusBars(error: Throwable?): Map { + if (!barsUnavailable) { + barsUnavailable = true + LOGGER.warn("Skyblocker's status bar API is unavailable, its bars will be left out of the OneConfig HUD editor", error) + } + return emptyMap() + } + @Suppress("UNCHECKED_CAST") private fun statusBars(): Map = - runCatching { statusBarsField?.get(self) as? Map }.getOrNull() ?: emptyMap() + runCatching { statusBarsField?.get(self) as? Map } + .getOrElse { noStatusBars(it) } ?: noStatusBars(null) private fun positioner(): BarPositioner = checkNotNull(barPositionerField?.get(self) as? BarPositioner) { "Skyblocker barPositioner is unavailable" } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt index fb4e8b58b..4f50b911c 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt @@ -1,6 +1,20 @@ package org.polyfrost.oneconfig.internal.ui.compose import com.mojang.blaze3d.pipeline.TextureTarget +import com.mojang.blaze3d.systems.RenderSystem +//? if >= 1.21.5 && < 1.21.8 { +/*import com.mojang.blaze3d.pipeline.BlendFunction +import com.mojang.blaze3d.pipeline.RenderPipeline +import com.mojang.blaze3d.platform.DestFactor +import com.mojang.blaze3d.platform.SourceFactor +*///? } +//? if < 1.21.5 { +/*import com.mojang.blaze3d.platform.GlStateManager +*///? } +//? if >= 1.21.4 && < 1.21.5 { +/*import com.mojang.blaze3d.vertex.DefaultVertexFormat +import com.mojang.blaze3d.vertex.VertexFormat +*///? } import net.minecraft.client.Minecraft import net.minecraft.client.gui.GuiGraphicsExtractor //? if >= 1.21.5 { From 1cc922dd622323e033c5e7b24a3b70b90e5ab4f6 Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Thu, 10 Sep 2026 04:44:50 -0400 Subject: [PATCH 15/16] fix stuff --- .../internal/OneConfigMixinInit.java | 29 +++- ...xin_SkyblockerFancyStatusBarsInstance.java | 31 ++++ ...ixin_SkyblockerFancyStatusBarsStatic.java} | 11 +- .../internal/compat/SkyblockerCompat.kt | 45 ++++-- .../oneconfig/internal/ui/compose/SkiaCtx.kt | 43 +++-- modules/config-impl/api/config-impl.api | 1 + .../api/config/v1/CompatSnapshots.java | 54 ++++--- .../oneconfig/api/config/v1/Config.java | 20 ++- .../api/config/v1/ConfigManager.java | 32 ++-- modules/config/api/config.api | 1 + .../api/config/v1/backend/Backend.java | 9 +- .../org/polyfrost/oneconfig/api/hud/v1/Hud.kt | 5 +- .../oneconfig/api/hud/v1/HudManager.kt | 7 +- .../api/hud/v1/OneConfigHudCompat.kt | 153 +++++++++--------- .../ui/hud/screens/HudDesignStudio.kt | 18 +-- .../compose/runtime/PolyComposeHost.kt | 20 ++- 16 files changed, 306 insertions(+), 173 deletions(-) create mode 100644 minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsInstance.java rename minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/{Mixin_SkyblockerFancyStatusBars.java => Mixin_SkyblockerFancyStatusBarsStatic.java} (73%) 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 81c544735..728fe2552 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/OneConfigMixinInit.java @@ -29,7 +29,10 @@ import kotlin.Unit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; //todo import org.polyfrost.oneconfig.internal.generated.RelocatedMixins; //? moul_compat { import org.polyfrost.oneconfig.internal.generated.RelocatedMixins; @@ -37,6 +40,7 @@ import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -65,7 +69,6 @@ public void acceptTargets(Set myTargets, Set otherTargets) { public List getMixins() { List mixins = new ArrayList<>(); - //? moul_compat { RelocatedMixins.INSTANCE.register(e -> { mixins.add(e); @@ -126,7 +129,12 @@ public List getMixins() { *///? } //? skyblocker_compat { - mixins.add("compat.skyblocker.Mixin_SkyblockerFancyStatusBars"); + Boolean skyblockerSingleton = declaresStaticMethod("de.hysky.skyblocker.skyblock.fancybars.FancyStatusBars", "initStatic"); + if (skyblockerSingleton != null) { + mixins.add(skyblockerSingleton + ? "compat.skyblocker.Mixin_SkyblockerFancyStatusBarsInstance" + : "compat.skyblocker.Mixin_SkyblockerFancyStatusBarsStatic"); + } mixins.add("compat.skyblocker.Mixin_SkyblockerWidgetManager"); //? } @@ -227,6 +235,23 @@ private static boolean isClassPresent(String className) { } } + private static Boolean declaresStaticMethod(String className, String methodName) { + try (InputStream in = OneConfigMixinInit.class.getClassLoader() + .getResourceAsStream(className.replace('.', '/') + ".class")) { + if (in == null) return null; + ClassNode node = new ClassNode(); + new ClassReader(in).accept(node, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + for (MethodNode method : node.methods) { + if (method.name.equals(methodName) && (method.access & Opcodes.ACC_STATIC) != 0) return Boolean.TRUE; + } + return Boolean.FALSE; + } catch (Throwable t) { + LogManager.getLogger(OneConfigMixinInit.class) + .warn("could not read {} to pick a mixin shape, skipping the mixins that depend on it", className, t); + return null; + } + } + @Override public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { } diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsInstance.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsInstance.java new file mode 100644 index 000000000..7781ad947 --- /dev/null +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsInstance.java @@ -0,0 +1,31 @@ +package org.polyfrost.oneconfig.internal.mixin.compat.skyblocker; + +//? skyblocker_compat { +import de.hysky.skyblocker.skyblock.fancybars.FancyStatusBars; +import org.polyfrost.oneconfig.internal.compat.SkyblockerCompat; +import org.polyfrost.oneconfig.internal.ui.hud.CompatOverlayRenderer; +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; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Pseudo +@Mixin(FancyStatusBars.class) +public class Mixin_SkyblockerFancyStatusBarsInstance { + + @Inject(method = "initStatic", at = @At("TAIL"), require = 0, expect = 0) + private static void oneconfig$registerHudCompat(CallbackInfo ci) { + SkyblockerCompat.initialize(); + } + + //~ if >= 26.1 'render' -> 'extractRenderState' + @Inject(method = "extractRenderState", at = @At("HEAD"), cancellable = true, require = 0, expect = 0) + private void oneconfig$suppressWhileEditing(CallbackInfoReturnable cir) { + if (CompatOverlayRenderer.oneConfigScreenOpen() && !SkyblockerCompat.isRedrawing() && SkyblockerCompat.isActive()) { + cir.setReturnValue(false); + } + } +} +//? } diff --git a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsStatic.java similarity index 73% rename from minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java rename to minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsStatic.java index 96b823cd1..cf47f3183 100644 --- a/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBars.java +++ b/minecraft/src/main/java/org/polyfrost/oneconfig/internal/mixin/compat/skyblocker/Mixin_SkyblockerFancyStatusBarsStatic.java @@ -13,22 +13,17 @@ @Pseudo @Mixin(FancyStatusBars.class) -public class Mixin_SkyblockerFancyStatusBars { +public class Mixin_SkyblockerFancyStatusBarsStatic { @Inject(method = "init", at = @At("TAIL"), require = 0, expect = 0) private static void oneconfig$registerHudCompat(CallbackInfo ci) { SkyblockerCompat.initialize(); } - @Inject(method = "initStatic", at = @At("TAIL"), require = 0, expect = 0) - private static void oneconfig$registerHudCompatStatic(CallbackInfo ci) { - SkyblockerCompat.initialize(); - } - //~ if >= 26.1 'render' -> 'extractRenderState' - @Inject(method = "extractRenderState", at = @At("HEAD"), cancellable = true, require = 0) + @Inject(method = "extractRenderState", at = @At("HEAD"), cancellable = true, require = 0, expect = 0) private static void oneconfig$suppressWhileEditing(CallbackInfoReturnable cir) { - if (SkyblockerCompat.isActive() && CompatOverlayRenderer.oneConfigScreenOpen() && !SkyblockerCompat.isRedrawing()) { + if (CompatOverlayRenderer.oneConfigScreenOpen() && !SkyblockerCompat.isRedrawing() && SkyblockerCompat.isActive()) { cir.setReturnValue(false); } } diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt index 934ba5917..310751804 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/compat/SkyblockerCompat.kt @@ -45,9 +45,18 @@ object SkyblockerCompat { private val cls = FancyStatusBars::class.java - private val self: Any? by lazy { - runCatching { cls.getField("INSTANCE").get(null) }.getOrNull() + private val NO_INSTANCE = Any() + + @Volatile + private var selfCache: Any? = null + + private fun self(): Any? { + selfCache?.let { return if (it === NO_INSTANCE) null else it } + val found = runCatching { cls.getField("INSTANCE").get(null) }.getOrNull() ?: runCatching { cls.getMethod("getInstance").invoke(null) }.getOrNull() + if (found == null && !barsReady) return null + selfCache = found ?: NO_INSTANCE + return found } private fun field(name: String): Field? = runCatching { @@ -69,14 +78,24 @@ object SkyblockerCompat { method("extractRenderState", GuiGraphicsExtractor::class.java, Minecraft::class.java) } - private val active: Boolean by lazy { - val ok = renderBarsMethod != null && runCatching { statusBarsField?.get(self) }.getOrNull() != null + private val handlesResolved: Boolean by lazy { + val ok = listOf(renderBarsMethod, saveBarConfigMethod, updatePositionsMethod, barPositionerField, statusBarsField) + .all { it != null } if (!ok) LOGGER.warn("Skyblocker status bar compat is off: FancyStatusBars has an unrecognised shape") ok } + @Volatile + private var barsReady = false + @JvmStatic - fun isActive(): Boolean = active + fun isActive(): Boolean { + if (barsReady) return true + if (!handlesResolved) return false + if (runCatching { statusBarsField?.get(self()) }.getOrNull() == null) return false + barsReady = true + return true + } private fun noStatusBars(error: Throwable?): Map { if (!barsUnavailable) { @@ -88,28 +107,28 @@ object SkyblockerCompat { @Suppress("UNCHECKED_CAST") private fun statusBars(): Map = - runCatching { statusBarsField?.get(self) as? Map } + runCatching { statusBarsField?.get(self()) as? Map } .getOrElse { noStatusBars(it) } ?: noStatusBars(null) private fun positioner(): BarPositioner = - checkNotNull(barPositionerField?.get(self) as? BarPositioner) { "Skyblocker barPositioner is unavailable" } + checkNotNull(barPositionerField?.get(self()) as? BarPositioner) { "Skyblocker barPositioner is unavailable" } private fun saveBars() { - saveBarConfigMethod?.invoke(self) + saveBarConfigMethod?.invoke(self()) } private fun placeBars() { - placeBarsMethod?.invoke(self) + placeBarsMethod?.invoke(self()) } private fun updatePositions(ignoreVisibility: Boolean) { - updatePositionsMethod?.invoke(self, ignoreVisibility) + updatePositionsMethod?.invoke(self(), ignoreVisibility) } - private fun healthFancyBarEnabled(): Boolean = healthFancyBarMethod?.invoke(self) as? Boolean == true + private fun healthFancyBarEnabled(): Boolean = healthFancyBarMethod?.invoke(self()) as? Boolean == true private fun renderStatusBars(ctx: GuiGraphicsExtractor, mc: Minecraft) { - renderBarsMethod?.invoke(self, ctx, mc) + renderBarsMethod?.invoke(self(), ctx, mc) } @JvmStatic @@ -123,7 +142,7 @@ object SkyblockerCompat { } private fun register() { - if (!isActive()) return + if (!handlesResolved) return var count = 0 for (type in StatusBarType.values()) { runCatching { diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt index 4f50b911c..ed7eff7b1 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt @@ -751,8 +751,15 @@ object SkiaCtx { /*RenderSystem.maxSupportedTextureSize() *///? } + private var maxTextureSizeCache = -1 + + private fun cachedMaxTextureSize(): Int { + if (maxTextureSizeCache <= 0) maxTextureSizeCache = runCatching { maxTextureSize() }.getOrDefault(0) + return maxTextureSizeCache + } + private fun viewportFitsTexture(w: Int, h: Int): Boolean { - val max = runCatching { maxTextureSize() }.getOrDefault(0) + val max = cachedMaxTextureSize() if (max <= 0 || (w <= max && h <= max)) { oversizeReported = false return true @@ -760,6 +767,8 @@ object SkiaCtx { if (!oversizeReported) { oversizeReported = true LOG.warn("SkiaCtx: viewport {}x{} is past the max texture size ({}); skipping offscreen surfaces", w, h, max) + destroyHudTarget() + destroyComposeTarget() } return false } @@ -773,7 +782,7 @@ object SkiaCtx { var rt = hudTarget val needNewTarget = rt == null || rt.width != w || rt.height != h if (needNewTarget) { - if (System.currentTimeMillis() - composeAllocFailedAt < ALLOC_RETRY_COOLDOWN_MS) return null + if (System.currentTimeMillis() - allocFailedAt < ALLOC_RETRY_COOLDOWN_MS) return null destroyHudTarget() rt = try { //? if >= 26.2 { @@ -786,7 +795,7 @@ object SkiaCtx { /*TextureTarget(w, h, true, Minecraft.ON_OSX) *///? } } catch (e: Throwable) { - onComposeAllocFailure(w, h, e) + onAllocFailure("hud", w, h, e) return null } hudTarget = rt @@ -809,7 +818,13 @@ object SkiaCtx { hudBrt?.close(); hudBrt = null hudRealIsGeneral = false val svc = vulkanService ?: return null - val (brt, colorFmt) = svc.makeOffscreenBRT(rt, w, h) + val (brt, colorFmt) = try { + svc.makeOffscreenBRT(rt!!, w, h) + } catch (e: Throwable) { + destroyHudTarget() + onAllocFailure("hud", w, h, e) + return null + } hudBrt = brt hudSurface = Surface.makeFromBackendRenderTarget( directContext, brt, @@ -833,8 +848,8 @@ object SkiaCtx { hudTarget = null } - private var composeAllocFailedAt = 0L - private var composeAllocReported = false + private var allocFailedAt = 0L + private var allocReported = false private const val ALLOC_RETRY_COOLDOWN_MS = 2000L @@ -851,7 +866,7 @@ object SkiaCtx { var rt = composeTarget val needNewTarget = rt == null || rt.width != w || rt.height != h if (needNewTarget) { - if (System.currentTimeMillis() - composeAllocFailedAt < ALLOC_RETRY_COOLDOWN_MS) return null + if (System.currentTimeMillis() - allocFailedAt < ALLOC_RETRY_COOLDOWN_MS) return null destroyComposeTarget() rt = try { //? if >= 26.2 { @@ -864,7 +879,7 @@ object SkiaCtx { /*TextureTarget(w, h, true, Minecraft.ON_OSX) *///? } } catch (e: Throwable) { - onComposeAllocFailure(w, h, e) + onAllocFailure("compose", w, h, e) return null } composeTarget = rt @@ -891,7 +906,7 @@ object SkiaCtx { svc.makeOffscreenBRT(rt!!, w, h) } catch (e: Throwable) { destroyComposeTarget() - onComposeAllocFailure(w, h, e) + onAllocFailure("compose", w, h, e) return null } composeBrt = brt.first @@ -910,16 +925,16 @@ object SkiaCtx { return composeSurface } - private fun onComposeAllocFailure(w: Int, h: Int, error: Throwable) { - composeAllocFailedAt = System.currentTimeMillis() + private fun onAllocFailure(what: String, w: Int, h: Int, error: Throwable) { + allocFailedAt = System.currentTimeMillis() destroyComposeTarget() destroyHudTarget() org.polyfrost.oneconfig.internal.ui.SkiaOffscreenTarget.destroyAll() if (isVulkanMode) invalidateVkSurfaces() runCatching { directContext.flush() } - LOG.error("SkiaCtx: failed to allocate the {}x{} compose target; skipping compose frames", w, h, error) - if (!composeAllocReported) { - composeAllocReported = true + LOG.error("SkiaCtx: failed to allocate the {}x{} {} target; skipping offscreen frames", w, h, what, error) + if (!allocReported) { + allocReported = true runCatching { Platform.screen().showMessage( "OneConfig couldn't allocate GPU memory for its UI (${w}x$h). " + diff --git a/modules/config-impl/api/config-impl.api b/modules/config-impl/api/config-impl.api index 2e4fed921..68f4d084d 100644 --- a/modules/config-impl/api/config-impl.api +++ b/modules/config-impl/api/config-impl.api @@ -28,6 +28,7 @@ public final class org/polyfrost/oneconfig/api/config/v1/CompatSnapshots : org/p public static fun register (Lorg/polyfrost/oneconfig/api/config/v1/Tree;)Lorg/polyfrost/oneconfig/api/config/v1/Tree; public static fun setDispatcher (Ljava/util/function/Consumer;)V public static fun track (Lorg/polyfrost/oneconfig/api/config/v1/Tree;)Lorg/polyfrost/oneconfig/api/config/v1/Tree; + public static fun untrack (Ljava/lang/String;)V } public abstract class org/polyfrost/oneconfig/api/config/v1/Config { diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/CompatSnapshots.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/CompatSnapshots.java index 608965921..747274d1b 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/CompatSnapshots.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/CompatSnapshots.java @@ -33,6 +33,7 @@ import java.util.Collections; import java.util.IdentityHashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -79,6 +80,12 @@ public static Tree track(Tree registered) { return INSTANCE.track0(registered); } + public static void untrack(String treeId) { + if (treeId == null) return; + INSTANCE.known.remove(treeId); + INSTANCE.defaults.remove(treeId); + } + private Tree register0(Tree tree) { tree.addMetadata(Backend.UI_ONLY_METADATA, Boolean.TRUE); dropStaleRegistration(tree.getID()); @@ -144,7 +151,11 @@ public void onProfileSaving(String profile) { } dispatchAndWait(() -> { for (Tree tree : known.values()) { - captureAll(tree, profile); + try { + captureAll(tree, profile); + } catch (Throwable t) { + ConfigManager.LOGGER.error("Failed to capture compat snapshot for '{}'", tree.getID(), t); + } } }); flushSnapshotThenBaseline(store, profile, baselineStore, BASELINE_BUCKET); @@ -184,7 +195,11 @@ public void onProfileDeleted(String profile) { currentProfile = ""; dispatchAndWait(() -> { for (Tree tree : known.values()) { - applyProfile(tree, ""); + try { + applyProfile(tree, ""); + } catch (Throwable t) { + ConfigManager.LOGGER.error("Failed to clear compat snapshot for '{}'", tree.getID(), t); + } } }); } @@ -224,6 +239,7 @@ private void applyProfile(Tree tree, String profile) { String treeId = tree.getID(); Map snap = store.load(profile).get(treeId); boolean[] changed = {false}; + Map pending = new LinkedHashMap<>(); forEachProp(tree, p -> { if (!isValueProp(p)) return; String key = keyOf(p); @@ -231,17 +247,13 @@ private void applyProfile(Tree tree, String profile) { Object baseline = getBaseline(treeId, key); if (baseline != null && liveSer != null && !valuesEqual(liveSer, baseline)) { - store.putValue(profile, treeId, key, liveSer); - setBaseline(treeId, key, liveSer); + pending.put(key, liveSer); return; } Object stored = snap == null ? null : snap.get(key); if (stored == null) { - if (liveSer != null) { - store.putValue(profile, treeId, key, liveSer); - setBaseline(treeId, key, liveSer); - } + if (liveSer != null) pending.put(key, liveSer); return; } @@ -250,10 +262,7 @@ private void applyProfile(Tree tree, String profile) { value = deserialize(stored); } catch (Throwable t) { ConfigManager.LOGGER.warn("Failed to deserialize compat value for '{}', re-snapshotting from live value", key, t); - if (liveSer != null) { - store.putValue(profile, treeId, key, liveSer); - setBaseline(treeId, key, liveSer); - } + if (liveSer != null) pending.put(key, liveSer); return; } if (value instanceof OneConfigKeybind && ((OneConfigKeybind) value).getHasUnresolvedInputs()) { @@ -279,12 +288,19 @@ private void applyProfile(Tree tree, String profile) { applying.remove(p); } }); + boolean ownerStillThere = !gateClosed(tree); + if (ownerStillThere) { + pending.forEach((key, serialized) -> { + store.putValue(profile, treeId, key, serialized); + setBaseline(treeId, key, serialized); + }); + } // Persist the profile snapshot before its baseline. If the first write fails, keeping an // older baseline is safe: the next load treats the live value as an external change and // repairs the snapshot. The opposite order could make a stale snapshot look current and // roll a setting back after a restart. flushSnapshotThenBaseline(store, profile, baselineStore, BASELINE_BUCKET); - if (changed[0]) runSave(tree); + if (changed[0] && ownerStillThere) runSave(tree); } public static void capture(Tree tree) { @@ -297,14 +313,16 @@ private void captureAll(Tree tree, String profile) { captureDefaults(tree); ensureKeys(tree); String treeId = tree.getID(); + Map pending = new LinkedHashMap<>(); forEachProp(tree, p -> { if (!isValueProp(p)) return; - String key = keyOf(p); Object serialized = trySerialize(p.get()); - if (serialized != null) { - store.putValue(profile, treeId, key, serialized); - setBaseline(treeId, key, serialized); - } + if (serialized != null) pending.put(keyOf(p), serialized); + }); + if (gateClosed(tree)) return; + pending.forEach((key, serialized) -> { + store.putValue(profile, treeId, key, serialized); + setBaseline(treeId, key, serialized); }); } diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java index 3ab5dc607..1f2d2e491 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/Config.java @@ -348,23 +348,30 @@ protected void addDependency(String option, String name, Supplier getProperty(Tree tree, String option) { public void save() { if (tree == null) return; - ConfigManager.active().save(tree); + ConfigManager manager = ConfigManager.active(); + if (!manager.save(tree)) ConfigManager.notifyWriteFailed(this, manager.backend.lastSaveFailure()); } /** diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java index 6d4feb22f..bb0214aca 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java @@ -145,7 +145,6 @@ public static Path profileDir(String profile) { final FileBackend backend; private volatile boolean shutdown = false; - @SuppressWarnings("unchecked") private ConfigManager(Path onto, FileSerializer... serializers) { backend = new FileBackend(onto, (FileSerializer[]) serializers); @@ -199,6 +198,15 @@ public static void initialize() { config.initialize(true); } catch (Throwable t) { failedInitialization.add(config.id); + Tree half = config.tree; + config.tree = null; + if (half != null && half.getID() != null) { + try { + active().unregister(half.getID()); + } catch (Throwable u) { + LOGGER.warn("failed to unregister the half-built tree for config {}", config.id, u); + } + } LOGGER.error("failed to initialize config {}, skipping it", config.id, t); } } @@ -271,16 +279,22 @@ private static void notifyResetOptions(Config config, List options) { private static final AtomicBoolean writeFailureNotified = new AtomicBoolean(); - static void notifyWriteFailed(Config config, Throwable cause) { + static void notifyWriteFailed(Config config, @Nullable Throwable cause) { if (!writeFailureNotified.compareAndSet(false, true)) return; try { - Throwable root = cause.getCause() != null ? cause.getCause() : cause; - String reason = root.getMessage(); - Notifications.error("OneConfig: could not write to the config folder", - "Saving the defaults backup for '" + (config.title != null ? config.title : config.id) + "' failed" - + (reason != null ? " (" + reason + ")" : "") - + ". Your settings still load and save normally, but resetting an option to its default" - + " may not work. Check that your disk is not full and the config folder is writable."); + String name = config.title != null ? config.title : config.id; + String reason = null; + if (cause != null) { + Throwable root = cause.getCause() != null ? cause.getCause() : cause; + reason = root.getMessage(); + } + String tail = reason != null + ? " (" + reason + ")." + : ". This is usually a full disk or a config folder OneConfig cannot write to."; + Notifications.error(name + ": could not save config", + "OneConfig could not save this config" + tail + + " Your settings still work this session but will not be kept;" + + " the log has the exact cause."); } catch (Throwable t) { LOGGER.error("failed to notify about the write failure for config {}", config.id, t); } diff --git a/modules/config/api/config.api b/modules/config/api/config.api index fad55b773..14b6b151c 100644 --- a/modules/config/api/config.api +++ b/modules/config/api/config.api @@ -146,6 +146,7 @@ public abstract class org/polyfrost/oneconfig/api/config/v1/backend/Backend { public fun exists (Ljava/lang/String;)Z public final fun get (Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/Tree; public final fun getTrees ()Ljava/util/Collection; + public final fun lastSaveFailure ()Ljava/lang/Exception; public final fun load (Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/Tree; public final fun load (Lorg/polyfrost/oneconfig/api/config/v1/Tree;)Z protected abstract fun load0 (Ljava/lang/String;)Lorg/polyfrost/oneconfig/api/config/v1/Tree; diff --git a/modules/config/src/main/java/org/polyfrost/oneconfig/api/config/v1/backend/Backend.java b/modules/config/src/main/java/org/polyfrost/oneconfig/api/config/v1/backend/Backend.java index 17b438c9c..285fa043f 100644 --- a/modules/config/src/main/java/org/polyfrost/oneconfig/api/config/v1/backend/Backend.java +++ b/modules/config/src/main/java/org/polyfrost/oneconfig/api/config/v1/backend/Backend.java @@ -249,6 +249,7 @@ public final void saveAll() { public final boolean save(Tree tree) { if (tree.getID() == null) throw new IllegalArgumentException("tree must be master (have a valid ID)"); putSafe(tree); + LAST_SAVE_FAILURE.remove(); try { Object customSave = tree.getMetadata("custom_save"); if (customSave != null) { @@ -261,10 +262,17 @@ public final boolean save(Tree tree) { return save0(tree); } catch (Exception e) { LOGGER.error("error saving tree with ID {}!", tree.getID(), e); + LAST_SAVE_FAILURE.set(e); return false; } } + private static final ThreadLocal LAST_SAVE_FAILURE = new ThreadLocal<>(); + + public final @Nullable Exception lastSaveFailure() { + return LAST_SAVE_FAILURE.get(); + } + protected abstract boolean delete0(@NotNull Tree tree) throws Exception; /** @@ -354,7 +362,6 @@ protected void putSafe(Tree in) { } } - public static final class RegistrationResult { public final Tree tree; public final byte state; diff --git a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt index 76d23e509..87b3ff9d3 100644 --- a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt +++ b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt @@ -1178,11 +1178,8 @@ abstract class Hud(id: String, title: String, val category: Category) : Cloneabl /** * Whether this exact HUD can be deleted right now - * - * It must be a real instance (providers have nothing to delete) of a type the user is allowed - * to delete ([deletable]) */ - fun canDelete(): Boolean = isReal && deletable() + fun canDelete(): Boolean = if (isReal) deletable() else this in HudManager.activeInstances internal open val profileLocalTree: Boolean get() = true diff --git a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt index e69ca60df..dbd9c9d2c 100644 --- a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt +++ b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/HudManager.kt @@ -36,6 +36,7 @@ import org.jetbrains.annotations.ApiStatus import org.polyfrost.compose.node.RootNode import org.polyfrost.compose.render.RenderContext import org.polyfrost.compose.runtime.PolyComposeHost +import org.polyfrost.oneconfig.api.config.v1.CompatSnapshots import org.polyfrost.oneconfig.api.config.v1.ConfigManager import org.polyfrost.oneconfig.api.config.v1.Properties import org.polyfrost.oneconfig.api.config.v1.Tree @@ -380,7 +381,7 @@ object HudManager { } fun removeHud(hud: Hud, delete: Boolean = false) { - require(hud.isReal) { "Tried to remove a non-real HUD - use unregister() instead." } + if (!hud.isReal) LOGGER.warn("Removing HUD ${hud.title}, which has no config tree") activeInstances.remove(hud) disposeHudLogging(hud, delete) } @@ -431,6 +432,7 @@ object HudManager { LOGGER.warn("refusing to delete the config of ${hud.title}, which is marked as not user-deletable") } else if (delete && treeId != null) { cleanup { ConfigManager.active().delete(treeId) } + cleanup { CompatSnapshots.untrack(treeId) } } // back to being a plain provider so a single-instance HUD can be made again later cleanup { hud.detachTree() } @@ -538,13 +540,12 @@ object HudManager { } private fun shouldDraw(hud: Hud): Boolean { - if (hud is LegacyHudMarker) return false if (hud.hidden && !isEditing) return false return isShown(hud) } private fun keepsBackgroundOnly(hud: Hud): Boolean = - hud !is LegacyHudMarker && hud.hidden && !isEditing && hud.keepsHiddenBackground && isShown(hud) + hud.hidden && !isEditing && hud.keepsHiddenBackground && isShown(hud) /** Everything [shouldDraw] checks apart from the HUD's own hidden flag */ private fun isShown(hud: Hud): Boolean { diff --git a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt index e175eb7cc..251d8b4c3 100644 --- a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt +++ b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/OneConfigHudCompat.kt @@ -6,6 +6,7 @@ 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 java.util.concurrent.ConcurrentHashMap private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : Hud(wrapper.id, wrapper.name, Category.COMPAT), LegacyHudMarker { @@ -16,21 +17,25 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : private val hiddenRevision = mutableStateOf(0) - private val wrapperId = id - private val wrapperModId = runCatching { wrapper.modId }.getOrNull() ?: "unknown" - @Volatile private var faulted = false + private val loggedFailures = ConcurrentHashMap.newKeySet() private fun fault(member: String, error: Throwable) { + if (faulted) return faulted = true HudManager.LOGGER.error( - "Disabling compat HUD '$wrapperId' from '$wrapperModId': $member failed, so " + + "Disabling compat HUD '${wrapper.id}' from '${wrapper.modId ?: "unknown"}': $member failed, so " + "that mod is probably a different version than OneConfig was built against", error, ) } + private fun reportTransient(member: String, error: Throwable) { + if (!loggedFailures.add(member)) return + HudManager.LOGGER.error("Compat HUD '${wrapper.id}': $member threw, using a fallback for it", error) + } + private inline fun guard(member: String, fallback: T, block: () -> T): T { if (faulted) return fallback return try { @@ -38,8 +43,8 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : } catch (e: LinkageError) { fault(member, e) fallback - } catch (e: RuntimeException) { - fault(member, e) + } catch (e: Exception) { + reportTransient(member, e) fallback } } @@ -50,26 +55,14 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : return guard("hidden", true) { wrapper.hidden } } set(value) { - val changed = guard("hidden", false) { - if (wrapper.hidden == value) false else { + guard("hidden", Unit) { + if (wrapper.hidden != value) { wrapper.hidden = value - true + hiddenRevision.value++ } } - if (changed) hiddenRevision.value++ } - fun guardedPlacementReady(): Boolean = guard("placementReady", false) { wrapper.placementReady } - - fun guardedOwnsPlacement(): Boolean = guard("ownsPlacement", false) { wrapper.ownsPlacement } - - fun guardedLinkedProperties(): List> = - guard("linkedProperties", emptyList()) { wrapper.linkedProperties() } - - fun guardedSave() { - guard("save", Unit) { wrapper.save() } - } - override val persistOwnState: Boolean get() = false override val profileLocalTree: Boolean get() = false @@ -77,16 +70,20 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : override fun update(): Boolean = false override fun multipleInstancesAllowed(): Boolean = false - override fun deletable(): Boolean = false + override fun deletable(): Boolean = faulted - override val supportsScale: Boolean get() = guard("supportsScale", true) { wrapper.supportsScale } + override val supportsScale: Boolean get() = guard("supportsScale", false) { wrapper.supportsScale } + + private var lastX = 0f + private var lastY = 0f + private var lastScale = 1f override var x: Float - get() = guard("x", 0f) { wrapper.x } - set(value) { guard("x", Unit) { wrapper.x = value } } + get() = guard("x", null) { wrapper.x }?.also { lastX = it } ?: lastX + set(value) { guard("x", Unit) { wrapper.x = value; lastX = value } } override var y: Float - get() = guard("y", 0f) { wrapper.y } - set(value) { guard("y", Unit) { wrapper.y = value } } + get() = guard("y", null) { wrapper.y }?.also { lastY = it } ?: lastY + set(value) { guard("y", Unit) { wrapper.y = value; lastY = value } } override var relativeX: Float get() = x set(value) { x = value } @@ -95,8 +92,8 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : set(value) { y = value } override var customScale: Float - get() = guard("scale", 1f) { wrapper.scale } - set(value) { guard("scale", Unit) { wrapper.scale = value } } + get() = guard("scale", null) { wrapper.scale }?.also { lastScale = it } ?: lastScale + set(value) { guard("scale", Unit) { wrapper.scale = value; lastScale = value } } private var lastW = 0f private var lastH = 0f @@ -130,7 +127,7 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : get() = sizeH() set(_) {} - override val resizeAxes: HudResize get() = guard("resizeAxes", HudResize.Both) { wrapper.resizeAxes } + override val resizeAxes: HudResize get() = guard("resizeAxes", HudResize.None) { wrapper.resizeAxes } override fun applyEditorWidth(width: Float) { guard("scaledWidth", Unit) { wrapper.scaledWidth = width } @@ -147,6 +144,57 @@ private class OneConfigHudCompat(val wrapper: OneConfigHudWrapper) : guard("onDragEnd", Unit) { wrapper.onDragEnd() } CompatSnapshots.capture(tree) } + + private val placementReady: Boolean get() = guard("placementReady", false) { wrapper.placementReady } + + private val ownsPlacement: Boolean get() = guard("ownsPlacement", true) { wrapper.ownsPlacement } + + fun linkedPropertiesGuarded(): List> = + guard("linkedProperties", emptyList()) { wrapper.linkedProperties() } + + private fun saveWrapper() { + guard("save", Unit) { wrapper.save() } + } + + fun trackPlacementPerProfile(tree: Tree) { + excludeFromSnapshots(tree) + tree.addMetadata(CompatSnapshots.GATE_METADATA, java.util.function.BooleanSupplier { placementReady }) + if (!ownsPlacement) { + tree["oc_compat_x"] = placementProperty("x", "X Position", { x }, { x = it }) + tree["oc_compat_y"] = placementProperty("y", "Y Position", { y }, { y = it }) + if (supportsScale) { + tree["oc_compat_scale"] = placementProperty("scale", "Scale", { customScale }, { customScale = it }) + } + } + tree.addMetadata("custom_save", Runnable { saveWrapper() }) + CompatSnapshots.track(tree) + } + + private fun placementProperty( + key: String, + name: String, + getter: () -> Float, + setter: (Float) -> Unit, + ): Property = Properties.functional( + { getter() }, + { value -> setter(value) }, + "oc_compat_$key", + name, + null, + Float::class.java, + ).apply { + addMetadata(CompatSnapshots.KEY_METADATA, "oc_compat_$key") + addDisplayCondition { Property.Display.HIDDEN } + } + + private fun excludeFromSnapshots(tree: Tree) { + for (node in tree.map.values) { + when (node) { + is Property<*> -> node.addMetadata(CompatSnapshots.NO_SNAPSHOT_META, true) + is Tree -> excludeFromSnapshots(node) + } + } + } } interface OneConfigHudWrapper { @@ -192,51 +240,10 @@ interface OneConfigHudWrapper { hud.setup() val tree = hud.tree if (tree != null) { - for (prop in hud.guardedLinkedProperties()) tree.put(prop) - trackPlacementPerProfile(hud, tree) + for (prop in hud.linkedPropertiesGuarded()) tree.put(prop) + hud.trackPlacementPerProfile(tree) } hud.captureStaticSizeDefaults() hud.capturePositionDefaults() } - - private fun trackPlacementPerProfile(hud: OneConfigHudCompat, tree: Tree) { - excludeFromSnapshots(tree) - tree.addMetadata(CompatSnapshots.GATE_METADATA, java.util.function.BooleanSupplier { hud.guardedPlacementReady() }) - if (!hud.guardedOwnsPlacement()) { - tree["oc_compat_x"] = placementProperty("x", "X Position", { hud.x }, { hud.x = it }) - tree["oc_compat_y"] = placementProperty("y", "Y Position", { hud.y }, { hud.y = it }) - if (hud.supportsScale) { - tree["oc_compat_scale"] = - placementProperty("scale", "Scale", { hud.customScale }, { hud.customScale = it }) - } - } - tree.addMetadata("custom_save", Runnable { hud.guardedSave() }) - CompatSnapshots.track(tree) - } - - private fun placementProperty( - key: String, - name: String, - getter: () -> Float, - setter: (Float) -> Unit, - ): Property = Properties.functional( - { getter() }, - { value -> setter(value) }, - "oc_compat_$key", - name, - null, - Float::class.java, - ).apply { - addMetadata(CompatSnapshots.KEY_METADATA, "oc_compat_$key") - addDisplayCondition { Property.Display.HIDDEN } - } - - private fun excludeFromSnapshots(tree: Tree) { - for (node in tree.map.values) { - when (node) { - is Property<*> -> node.addMetadata(CompatSnapshots.NO_SNAPSHOT_META, true) - is Tree -> excludeFromSnapshots(node) - } - } - } } diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt index 2e1fc87c9..35352cabc 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/hud/screens/HudDesignStudio.kt @@ -765,7 +765,6 @@ private fun DrawScope.drawHudSizeBadge(label: String, centerX: Float, topY: Floa } } - @OptIn(ExperimentalComposeUiApi::class) @Composable private fun HudActionButton( @@ -967,20 +966,18 @@ fun HudDesignStudio(onReturnToOneConfig: (() -> Unit)? = null) { val panelHud: Hud? = if (panelOpen) primaryHud() else null val deleteHuds: (Collection) -> Unit = { huds -> - val (live, stale) = huds.partition { it.isReal } - val removed = live.filter { it.deletable() } - val dropped = removed + stale - if (dropped.isNotEmpty()) { + val removed = huds.filter { it.canDelete() } + if (removed.isNotEmpty()) { Snapshot.withMutableSnapshot { - val droppedSet = dropped.toSet() - selectedHuds = selectedHuds - droppedSet - dropped.forEach { hud -> + val removedSet = removed.toSet() + selectedHuds = selectedHuds - removedSet + removed.forEach { hud -> if (hoveredHud === hud) hoveredHud = null - if (hud.isReal) HudManager.removeHud(hud, delete = true) + HudManager.removeHud(hud, delete = true) HudDesignSession.forget(hud) } } - if (removed.isNotEmpty()) UiSounds.play(UiSoundEvent.CLICK) + UiSounds.play(UiSoundEvent.CLICK) } } @@ -2584,7 +2581,6 @@ fun HudDragLayer(modifier: Modifier = Modifier) { } } - @Composable private fun DesignStudioPanel( modifier: Modifier = Modifier, diff --git a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/runtime/PolyComposeHost.kt b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/runtime/PolyComposeHost.kt index 261828c52..b48967e27 100644 --- a/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/runtime/PolyComposeHost.kt +++ b/modules/poly-compose/src/main/kotlin/org/polyfrost/compose/runtime/PolyComposeHost.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean class PolyComposeClock { private val clock = BroadcastFrameClock() @@ -21,21 +22,18 @@ class PolyComposeClock { internal val recomposer: CompositionContext get() = recomposerImpl - private var inFrame = false + private val inFrame = AtomicBoolean(false) fun frame(nanos: Long = System.nanoTime(), notify: Boolean = true): Boolean { - if (inFrame) return recomposerImpl.hasPendingWork - inFrame = true + if (!inFrame.compareAndSet(false, true)) return recomposerImpl.hasPendingWork try { - return Snapshot.global { - if (notify) Snapshot.sendApplyNotifications() - val appliedBefore = recomposerImpl.changeCount - clock.sendFrame(nanos) - appliedChange = recomposerImpl.changeCount != appliedBefore - appliedChange || recomposerImpl.hasPendingWork - } + if (notify) Snapshot.sendApplyNotifications() + val appliedBefore = recomposerImpl.changeCount + clock.sendFrame(nanos) + appliedChange = recomposerImpl.changeCount != appliedBefore + return appliedChange || recomposerImpl.hasPendingWork } finally { - inFrame = false + inFrame.set(false) } } From b7133569f708e9e98a6054d908edf09c530d100a Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Thu, 10 Sep 2026 05:26:00 -0400 Subject: [PATCH 16/16] even more fixes --- .../item/MinecraftItemCatalogService.kt | 17 ++++++++++- .../internal/ui/compose/ComposeScreen.kt | 14 +++++++++- .../oneconfig/internal/ui/compose/SkiaCtx.kt | 28 +++++++++++-------- .../api/config/v1/ConfigManager.java | 11 ++++---- .../org/polyfrost/oneconfig/api/hud/v1/Hud.kt | 2 +- .../oneconfig/internal/ui/api/ModOrder.kt | 6 ++-- 6 files changed, 56 insertions(+), 22 deletions(-) diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/item/MinecraftItemCatalogService.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/item/MinecraftItemCatalogService.kt index 41e6998b0..d78719e84 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/item/MinecraftItemCatalogService.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/components/item/MinecraftItemCatalogService.kt @@ -116,6 +116,10 @@ class MinecraftItemCatalogService : ItemCatalogService { } private fun renderPendingBatch() { + if (Minecraft.getInstance().player == null) { + retryRenderLater() + return + } val guiWidth = Platform.screen().guiWidth() val guiHeight = Platform.screen().guiHeight() val windowWidth = Platform.screen().windowWidth() @@ -123,7 +127,6 @@ class MinecraftItemCatalogService : ItemCatalogService { val viewportWidth = Platform.screen().viewportWidth() val viewportHeight = Platform.screen().viewportHeight() if ( - Minecraft.getInstance().player == null || guiWidth <= 0 || guiHeight <= 0 || windowWidth <= 0 || windowHeight <= 0 || viewportWidth <= 0 || viewportHeight <= 0 @@ -586,6 +589,18 @@ class MinecraftItemCatalogService : ItemCatalogService { } } + private fun retryRenderLater() { + val retry = synchronized(requestLock) { + if (waiting.isEmpty()) { + renderScheduled = false + false + } else { + true + } + } + if (retry) scheduleRender() + } + private fun markRenderFinished() { var scheduleNext = false synchronized(requestLock) { diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt index 5273e30ef..33fd0fd83 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/ComposeScreen.kt @@ -691,7 +691,7 @@ abstract class ComposeScreen( //? >= 1.21.10 { override fun charTyped(event: CharacterEvent): Boolean { - val char = if (Character.isBmpCodePoint(event.codepoint)) Char(event.codepoint) else KeyEvent.CHAR_UNDEFINED + val char = if (Character.isBmpCodePoint(event.codepoint)) Char(event.codepoint) else Char(0) val codepoint = event.codepoint //? >= 26.1 { val modifiers = 0 //dropped from the event in 26.1 because glfw no longer passes them @@ -824,6 +824,18 @@ abstract class ComposeScreen( } private fun sendCharacterEvent(char: Char, codePoint: Int, modifiers: Int): Boolean { + if (!Character.isBmpCodePoint(codePoint)) { + var handled = false + for (part in Character.toChars(codePoint)) { + handled = sendCharKeyEvent(part, codePoint, modifiers) || handled + } + return handled + } + if (char == KeyEvent.CHAR_UNDEFINED) return false + return sendCharKeyEvent(char, codePoint, modifiers) + } + + private fun sendCharKeyEvent(char: Char, codePoint: Int, modifiers: Int): Boolean { return sendKeyEventSafely { androidx.compose.ui.input.key.KeyEvent( key = Key(KeyEvent.VK_UNDEFINED), diff --git a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt index ed7eff7b1..66a573145 100644 --- a/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt +++ b/minecraft/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/compose/SkiaCtx.kt @@ -751,16 +751,20 @@ object SkiaCtx { /*RenderSystem.maxSupportedTextureSize() *///? } - private var maxTextureSizeCache = -1 + private var maxTextureSizeCache = 0 private fun cachedMaxTextureSize(): Int { - if (maxTextureSizeCache <= 0) maxTextureSizeCache = runCatching { maxTextureSize() }.getOrDefault(0) + if (maxTextureSizeCache == 0) { + maxTextureSizeCache = runCatching { maxTextureSize() }.getOrNull() + ?.takeIf { it > 0 } + ?: Int.MAX_VALUE + } return maxTextureSizeCache } private fun viewportFitsTexture(w: Int, h: Int): Boolean { val max = cachedMaxTextureSize() - if (max <= 0 || (w <= max && h <= max)) { + if (w <= max && h <= max) { oversizeReported = false return true } @@ -795,7 +799,7 @@ object SkiaCtx { /*TextureTarget(w, h, true, Minecraft.ON_OSX) *///? } } catch (e: Throwable) { - onAllocFailure("hud", w, h, e) + onAllocFailure(HUD_TARGET, w, h, e) return null } hudTarget = rt @@ -819,10 +823,10 @@ object SkiaCtx { hudRealIsGeneral = false val svc = vulkanService ?: return null val (brt, colorFmt) = try { - svc.makeOffscreenBRT(rt!!, w, h) + svc.makeOffscreenBRT(rt, w, h) } catch (e: Throwable) { destroyHudTarget() - onAllocFailure("hud", w, h, e) + onAllocFailure(HUD_TARGET, w, h, e) return null } hudBrt = brt @@ -853,6 +857,9 @@ object SkiaCtx { private const val ALLOC_RETRY_COOLDOWN_MS = 2000L + private const val HUD_TARGET = "hud" + private const val COMPOSE_TARGET = "compose" + // bottom left lets OpenGL do a plain copy which is faster // compensated in drawComposeBlit because GuiGraphics always samples top left private val composeOrigin get() = if (isVulkanMode) SurfaceOrigin.TOP_LEFT else SurfaceOrigin.BOTTOM_LEFT @@ -879,7 +886,7 @@ object SkiaCtx { /*TextureTarget(w, h, true, Minecraft.ON_OSX) *///? } } catch (e: Throwable) { - onAllocFailure("compose", w, h, e) + onAllocFailure(COMPOSE_TARGET, w, h, e) return null } composeTarget = rt @@ -903,10 +910,10 @@ object SkiaCtx { composeRealIsGeneral = false val svc = vulkanService ?: return null val brt = try { - svc.makeOffscreenBRT(rt!!, w, h) + svc.makeOffscreenBRT(rt, w, h) } catch (e: Throwable) { destroyComposeTarget() - onAllocFailure("compose", w, h, e) + onAllocFailure(COMPOSE_TARGET, w, h, e) return null } composeBrt = brt.first @@ -927,8 +934,7 @@ object SkiaCtx { private fun onAllocFailure(what: String, w: Int, h: Int, error: Throwable) { allocFailedAt = System.currentTimeMillis() - destroyComposeTarget() - destroyHudTarget() + if (what == COMPOSE_TARGET) destroyComposeTarget() else destroyHudTarget() org.polyfrost.oneconfig.internal.ui.SkiaOffscreenTarget.destroyAll() if (isVulkanMode) invalidateVkSurfaces() runCatching { directContext.flush() } diff --git a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java index bb0214aca..f2783ce98 100644 --- a/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java +++ b/modules/config-impl/src/main/java/org/polyfrost/oneconfig/api/config/v1/ConfigManager.java @@ -280,14 +280,15 @@ private static void notifyResetOptions(Config config, List options) { private static final AtomicBoolean writeFailureNotified = new AtomicBoolean(); static void notifyWriteFailed(Config config, @Nullable Throwable cause) { + if (cause == null) { + LOGGER.error("config {} reported an unsuccessful save with no underlying error", config.id); + return; + } if (!writeFailureNotified.compareAndSet(false, true)) return; try { String name = config.title != null ? config.title : config.id; - String reason = null; - if (cause != null) { - Throwable root = cause.getCause() != null ? cause.getCause() : cause; - reason = root.getMessage(); - } + Throwable root = cause.getCause() != null ? cause.getCause() : cause; + String reason = root.getMessage(); String tail = reason != null ? " (" + reason + ")." : ". This is usually a full disk or a config folder OneConfig cannot write to."; diff --git a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt index 87b3ff9d3..bb08a2b65 100644 --- a/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt +++ b/modules/hud/src/main/kotlin/org/polyfrost/oneconfig/api/hud/v1/Hud.kt @@ -1179,7 +1179,7 @@ abstract class Hud(id: String, title: String, val category: Category) : Cloneabl /** * Whether this exact HUD can be deleted right now */ - fun canDelete(): Boolean = if (isReal) deletable() else this in HudManager.activeInstances + fun canDelete(): Boolean = deletable() && (isReal || this in HudManager.activeInstances) internal open val profileLocalTree: Boolean get() = true diff --git a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/ModOrder.kt b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/ModOrder.kt index 3c2de4988..8949dbbcf 100644 --- a/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/ModOrder.kt +++ b/modules/internal/src/main/kotlin/org/polyfrost/oneconfig/internal/ui/api/ModOrder.kt @@ -13,7 +13,7 @@ import java.nio.file.StandardOpenOption /** * User-defined mod card order persisted one id per line * - * Mods that were never dragged are unknown here and sort alphabetically behind the ones that were + * Mods that were never dragged are unknown here and sort alphabetically ahead of the ones that were */ object ModOrder { private val LOGGER = LoggerFactory.getLogger("OneConfig/ModOrder") @@ -46,7 +46,7 @@ object ModOrder { fun indexOf(id: String): Int { ensureLoaded() val index = order.indexOf(id) - return if (index >= 0) index else Int.MAX_VALUE + return if (index >= 0) index else Int.MIN_VALUE } /** @@ -57,7 +57,7 @@ object ModOrder { */ fun reorder(visible: List, all: List) { ensureLoaded() - all.forEach { if (it !in order) order.add(it) } + order.addAll(0, all.filter { it !in order }) val slots = order.indices.filter { order[it] in visible } if (slots.size != visible.size) { LOGGER.warn("Mod order slots ({}) did not match visible mods ({})", slots.size, visible.size)