From 300f2c9868ea7aa3b11455000b26cef62ba9a7fb Mon Sep 17 00:00:00 2001 From: LostLuma Date: Sun, 1 Oct 2023 04:26:46 +0200 Subject: [PATCH 01/19] Bump Loom version, clean up build script --- build.gradle | 6 +++--- gradle/libs.versions.toml | 3 +-- settings.gradle | 4 ---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 4271c4b4..0fdcfdf6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'maven-publish' + id "maven-publish" alias libs.plugins.fabric.loom } @@ -19,8 +19,8 @@ dependencies { minecraft libs.minecraft mappings loom.officialMojangMappings() + modImplementation libs.fabric.api modImplementation libs.fabric.loader - modImplementation libs.bundles.fabric.api modApi libs.modmenu modApi libs.cloth.config @@ -42,7 +42,7 @@ java { } tasks.withType(JavaCompile).configureEach { - it.options.release = 17 + it.options.release.set(17) it.options.encoding = "UTF-8" it.options.compilerArgs += ["-Xlint:deprecation"] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac0a3c77..0924635a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,6 @@ fabric_api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fab cloth_config = { module = "me.shedaniel.cloth:cloth-config-fabric", version.ref = "cloth_config" } [bundles] -fabric_api = ["fabric_api"] [plugins] -fabric_loom = { id = "fabric-loom", version = "1.3.8" } +fabric_loom = { id = "fabric-loom", version = "1.4.1" } diff --git a/settings.gradle b/settings.gradle index 1f5e4657..a7add4a7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,10 +4,6 @@ pluginManagement { name = 'Fabric' url = 'https://maven.fabricmc.net/' } - maven { - name = 'Quilt' - url = 'https://maven.quiltmc.org/repository/release' - } gradlePluginPortal() } } From d13b27f187fe4021d9c9f35b9fd679c92402e606 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Wed, 4 Oct 2023 14:48:03 +0200 Subject: [PATCH 02/19] Add tooltips and named slider options to config screen --- .../java/dynamic_fps/impl/GraphicsState.java | 7 ++- .../dynamic_fps/impl/compat/ClothConfig.java | 56 ++++++++++++++++--- .../dynamic_fps/impl/util/Localization.java | 3 +- .../assets/dynamic_fps/lang/de_at.json | 10 +++- .../assets/dynamic_fps/lang/de_ch.json | 10 +++- .../assets/dynamic_fps/lang/de_de.json | 10 +++- .../assets/dynamic_fps/lang/en_us.json | 10 +++- 7 files changed, 92 insertions(+), 14 deletions(-) diff --git a/src/main/java/dynamic_fps/impl/GraphicsState.java b/src/main/java/dynamic_fps/impl/GraphicsState.java index 3f749711..2b1e748e 100644 --- a/src/main/java/dynamic_fps/impl/GraphicsState.java +++ b/src/main/java/dynamic_fps/impl/GraphicsState.java @@ -29,7 +29,7 @@ public enum GraphicsState { public static final Codec CODEC = new PrimitiveCodec() { @Override public T write(DynamicOps ops, GraphicsState value) { - return ops.createString(value.toString().toLowerCase(Locale.ROOT)); + return ops.createString(value.toString()); } @Override @@ -43,4 +43,9 @@ public DataResult read(DynamicOps ops, T input) { } } }; + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ROOT); + } } diff --git a/src/main/java/dynamic_fps/impl/compat/ClothConfig.java b/src/main/java/dynamic_fps/impl/compat/ClothConfig.java index c83a53fc..c5b327ce 100644 --- a/src/main/java/dynamic_fps/impl/compat/ClothConfig.java +++ b/src/main/java/dynamic_fps/impl/compat/ClothConfig.java @@ -2,10 +2,14 @@ import me.shedaniel.clothconfig2.api.ConfigBuilder; import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import net.minecraft.ChatFormatting; import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; import static dynamic_fps.impl.util.Localization.localized; +import java.util.Optional; + import dynamic_fps.impl.DynamicFPSMod; import dynamic_fps.impl.GraphicsState; import dynamic_fps.impl.PowerState; @@ -30,17 +34,14 @@ public static Screen genConfigScreen(Screen parent) { builder.getOrCreateCategory( localized("config", "category." + state.toString().toLowerCase())) - .addEntry( - entryBuilder - .startTextDescription( - localized("config", "frame_rate_target_description")).build()) .addEntry(entryBuilder .startIntSlider( localized("config", "frame_rate_target"), - config.frameRateTarget(), - -1, 60) - .setDefaultValue(standard.frameRateTarget()) - .setSaveConsumer(config::setFrameRateTarget) + fromConfigFpsTarget(config.frameRateTarget()), + 0, 61) + .setDefaultValue(fromConfigFpsTarget(standard.frameRateTarget())) + .setSaveConsumer(value -> config.setFrameRateTarget(toConfigFpsTarget(value))) + .setTextGetter(ClothConfig::fpsTargetMessage) .build()) .addEntry(entryBuilder .startIntSlider( @@ -49,6 +50,7 @@ public static Screen genConfigScreen(Screen parent) { 0, 100) .setDefaultValue((int) (standard.volumeMultiplier() * 100)) .setSaveConsumer(value -> config.setVolumeMultiplier(value / 100f)) + .setTextGetter(ClothConfig::volumeMultiplierMessage) .build()) .addEntry(entryBuilder .startEnumSelector( @@ -57,6 +59,8 @@ public static Screen genConfigScreen(Screen parent) { config.graphicsState()) .setDefaultValue(standard.graphicsState()) .setSaveConsumer(config::setGraphicsState) + .setEnumNameProvider(ClothConfig::graphicsStateMessage) + .setTooltipSupplier(ClothConfig::graphicsStateTooltip) .build()) .addEntry(entryBuilder .startBooleanToggle( @@ -64,6 +68,7 @@ public static Screen genConfigScreen(Screen parent) { config.showToasts()) .setDefaultValue(standard.showToasts()) .setSaveConsumer(config::setShowToasts) + .setTooltip(localized("config", "show_toasts_tooltip")) .build()) .addEntry(entryBuilder .startBooleanToggle( @@ -71,9 +76,44 @@ public static Screen genConfigScreen(Screen parent) { config.runGarbageCollector()) .setDefaultValue(standard.runGarbageCollector()) .setSaveConsumer(config::setRunGarbageCollector) + .setTooltip(localized("config", "run_garbage_collector_tooltip")) .build()); } return builder.build(); } + + // Convert magic -1 number to 61 (and reverse) + // So the "unlocked" FPS value is on the right + private static int toConfigFpsTarget(int value) { + return value == 61 ? -1 : value; + } + + private static int fromConfigFpsTarget(int value) { + return value == -1 ? 61 : value; + } + + private static Component fpsTargetMessage(int value) { + if (toConfigFpsTarget(value) != -1) { + return Component.translatable("options.framerate", value); + } else { + return Component.translatable("options.framerateLimit.max"); + } + } + + private static Component volumeMultiplierMessage(int value) { + return Component.literal(Integer.toString(value) + "%"); + } + + private static Component graphicsStateMessage(Enum graphicsState) { + return localized("config", "graphics_state_" + graphicsState.toString()); + } + + private static Optional graphicsStateTooltip(GraphicsState graphicsState) { + if (!graphicsState.equals(GraphicsState.MINIMAL)) { + return Optional.empty(); + } + + return Optional.of(new Component[]{ localized("config", "graphics_state_minimal_tooltip").withStyle(ChatFormatting.RED) }); + } } diff --git a/src/main/java/dynamic_fps/impl/util/Localization.java b/src/main/java/dynamic_fps/impl/util/Localization.java index 7d47bb1d..93fc0d1b 100644 --- a/src/main/java/dynamic_fps/impl/util/Localization.java +++ b/src/main/java/dynamic_fps/impl/util/Localization.java @@ -2,6 +2,7 @@ import dynamic_fps.impl.DynamicFPSMod; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; public final class Localization { /** e.g. keyString("title", "config") -> "title.dynamic_fps.config") */ @@ -9,7 +10,7 @@ public static String translationKey(String domain, String path) { return domain + "." + DynamicFPSMod.MOD_ID + "." + path; } - public static Component localized(String domain, String path, Object... args) { + public static MutableComponent localized(String domain, String path, Object... args) { return Component.translatable(translationKey(domain, path), args); } diff --git a/src/main/resources/assets/dynamic_fps/lang/de_at.json b/src/main/resources/assets/dynamic_fps/lang/de_at.json index 669392ba..27f1c84e 100644 --- a/src/main/resources/assets/dynamic_fps/lang/de_at.json +++ b/src/main/resources/assets/dynamic_fps/lang/de_at.json @@ -6,11 +6,19 @@ "config.dynamic_fps.category.invisible": "Unsichtbar", "config.dynamic_fps.frame_rate_target": "Zielbildrate", - "config.dynamic_fps.frame_rate_target_description": "Setze die Zielbildrate auf -1, um die Bildrate nicht zu reduzieren.", "config.dynamic_fps.volume_multiplier": "Lautstärke", + "config.dynamic_fps.graphics_state": "Grafikoptionen", + "config.dynamic_fps.graphics_state_default": "Standard", + "config.dynamic_fps.graphics_state_reduced": "Reduziert", + "config.dynamic_fps.graphics_state_minimal": "Minimal", + "config.dynamic_fps.graphics_state_minimal_tooltip": "Minimale Grafik bewirkt, dass die Welt neu lädt!", + "config.dynamic_fps.show_toasts": "Toasts Anzeigen", + "config.dynamic_fps.show_toasts_tooltip": "Toast Benachrichtigungen weiterhin anzeigen oder verzögern", + "config.dynamic_fps.run_garbage_collector": "GC auslösen", + "config.dynamic_fps.run_garbage_collector_tooltip": "Ungenutzen Arbeitsspeicher frei machen, wenn dieser Status aktiviert wird", "key.dynamic_fps.toggle_forced": "Unfokussierten Modus forcieren (Toggle)", "key.dynamic_fps.toggle_disabled": "Dynamic FPS deaktivieren (Toggle)", diff --git a/src/main/resources/assets/dynamic_fps/lang/de_ch.json b/src/main/resources/assets/dynamic_fps/lang/de_ch.json index 669392ba..27f1c84e 100644 --- a/src/main/resources/assets/dynamic_fps/lang/de_ch.json +++ b/src/main/resources/assets/dynamic_fps/lang/de_ch.json @@ -6,11 +6,19 @@ "config.dynamic_fps.category.invisible": "Unsichtbar", "config.dynamic_fps.frame_rate_target": "Zielbildrate", - "config.dynamic_fps.frame_rate_target_description": "Setze die Zielbildrate auf -1, um die Bildrate nicht zu reduzieren.", "config.dynamic_fps.volume_multiplier": "Lautstärke", + "config.dynamic_fps.graphics_state": "Grafikoptionen", + "config.dynamic_fps.graphics_state_default": "Standard", + "config.dynamic_fps.graphics_state_reduced": "Reduziert", + "config.dynamic_fps.graphics_state_minimal": "Minimal", + "config.dynamic_fps.graphics_state_minimal_tooltip": "Minimale Grafik bewirkt, dass die Welt neu lädt!", + "config.dynamic_fps.show_toasts": "Toasts Anzeigen", + "config.dynamic_fps.show_toasts_tooltip": "Toast Benachrichtigungen weiterhin anzeigen oder verzögern", + "config.dynamic_fps.run_garbage_collector": "GC auslösen", + "config.dynamic_fps.run_garbage_collector_tooltip": "Ungenutzen Arbeitsspeicher frei machen, wenn dieser Status aktiviert wird", "key.dynamic_fps.toggle_forced": "Unfokussierten Modus forcieren (Toggle)", "key.dynamic_fps.toggle_disabled": "Dynamic FPS deaktivieren (Toggle)", diff --git a/src/main/resources/assets/dynamic_fps/lang/de_de.json b/src/main/resources/assets/dynamic_fps/lang/de_de.json index 669392ba..27f1c84e 100644 --- a/src/main/resources/assets/dynamic_fps/lang/de_de.json +++ b/src/main/resources/assets/dynamic_fps/lang/de_de.json @@ -6,11 +6,19 @@ "config.dynamic_fps.category.invisible": "Unsichtbar", "config.dynamic_fps.frame_rate_target": "Zielbildrate", - "config.dynamic_fps.frame_rate_target_description": "Setze die Zielbildrate auf -1, um die Bildrate nicht zu reduzieren.", "config.dynamic_fps.volume_multiplier": "Lautstärke", + "config.dynamic_fps.graphics_state": "Grafikoptionen", + "config.dynamic_fps.graphics_state_default": "Standard", + "config.dynamic_fps.graphics_state_reduced": "Reduziert", + "config.dynamic_fps.graphics_state_minimal": "Minimal", + "config.dynamic_fps.graphics_state_minimal_tooltip": "Minimale Grafik bewirkt, dass die Welt neu lädt!", + "config.dynamic_fps.show_toasts": "Toasts Anzeigen", + "config.dynamic_fps.show_toasts_tooltip": "Toast Benachrichtigungen weiterhin anzeigen oder verzögern", + "config.dynamic_fps.run_garbage_collector": "GC auslösen", + "config.dynamic_fps.run_garbage_collector_tooltip": "Ungenutzen Arbeitsspeicher frei machen, wenn dieser Status aktiviert wird", "key.dynamic_fps.toggle_forced": "Unfokussierten Modus forcieren (Toggle)", "key.dynamic_fps.toggle_disabled": "Dynamic FPS deaktivieren (Toggle)", diff --git a/src/main/resources/assets/dynamic_fps/lang/en_us.json b/src/main/resources/assets/dynamic_fps/lang/en_us.json index 20a81af4..19694d7a 100644 --- a/src/main/resources/assets/dynamic_fps/lang/en_us.json +++ b/src/main/resources/assets/dynamic_fps/lang/en_us.json @@ -6,11 +6,19 @@ "config.dynamic_fps.category.invisible": "Invisible", "config.dynamic_fps.frame_rate_target": "Frame Rate Target", - "config.dynamic_fps.frame_rate_target_description": "Set Frame Rate Target to -1 to disable reducing FPS.", "config.dynamic_fps.volume_multiplier": "Volume Multiplier", + "config.dynamic_fps.graphics_state": "Graphics Options", + "config.dynamic_fps.graphics_state_default": "Default", + "config.dynamic_fps.graphics_state_reduced": "Reduced", + "config.dynamic_fps.graphics_state_minimal": "Minimal", + "config.dynamic_fps.graphics_state_minimal_tooltip": "Minimal graphics cause the world to reload!", + "config.dynamic_fps.show_toasts": "Show Toasts", + "config.dynamic_fps.show_toasts_tooltip": "Whether to keep displaying or delay toast notifications", + "config.dynamic_fps.run_garbage_collector": "Invoke Garbage Collector", + "config.dynamic_fps.run_garbage_collector_tooltip": "Free up unused memory when switching to this status", "key.dynamic_fps.toggle_forced": "Force Unfocused Mode (Toggle)", "key.dynamic_fps.toggle_disabled": "Disable Dynamic FPS (Toggle)", From d77156673797c07d4bba2af27c11dd821fe358df Mon Sep 17 00:00:00 2001 From: LostLuma Date: Thu, 5 Oct 2023 16:08:28 +0200 Subject: [PATCH 03/19] Remove chained config creation for readability --- .../dynamic_fps/impl/compat/ClothConfig.java | 112 ++++++++++-------- 1 file changed, 64 insertions(+), 48 deletions(-) diff --git a/src/main/java/dynamic_fps/impl/compat/ClothConfig.java b/src/main/java/dynamic_fps/impl/compat/ClothConfig.java index c5b327ce..c1606dd2 100644 --- a/src/main/java/dynamic_fps/impl/compat/ClothConfig.java +++ b/src/main/java/dynamic_fps/impl/compat/ClothConfig.java @@ -17,12 +17,12 @@ public final class ClothConfig { public static Screen genConfigScreen(Screen parent) { - ConfigBuilder builder = ConfigBuilder.create() + var builder = ConfigBuilder.create() .setParentScreen(parent) .setTitle(localized("config", "title")) .setSavingRunnable(DynamicFPSMod.modConfig::save); - ConfigEntryBuilder entryBuilder = builder.entryBuilder(); + var entryBuilder = builder.entryBuilder(); for (var state : PowerState.values()) { if (!state.configurable) { @@ -32,52 +32,68 @@ public static Screen genConfigScreen(Screen parent) { var config = DynamicFPSMod.modConfig.get(state); var standard = DynamicFPSConfig.getDefaultConfig(state); - builder.getOrCreateCategory( - localized("config", "category." + state.toString().toLowerCase())) - .addEntry(entryBuilder - .startIntSlider( - localized("config", "frame_rate_target"), - fromConfigFpsTarget(config.frameRateTarget()), - 0, 61) - .setDefaultValue(fromConfigFpsTarget(standard.frameRateTarget())) - .setSaveConsumer(value -> config.setFrameRateTarget(toConfigFpsTarget(value))) - .setTextGetter(ClothConfig::fpsTargetMessage) - .build()) - .addEntry(entryBuilder - .startIntSlider( - localized("config", "volume_multiplier"), - (int) (config.volumeMultiplier() * 100), - 0, 100) - .setDefaultValue((int) (standard.volumeMultiplier() * 100)) - .setSaveConsumer(value -> config.setVolumeMultiplier(value / 100f)) - .setTextGetter(ClothConfig::volumeMultiplierMessage) - .build()) - .addEntry(entryBuilder - .startEnumSelector( - localized("config", "graphics_state"), - GraphicsState.class, - config.graphicsState()) - .setDefaultValue(standard.graphicsState()) - .setSaveConsumer(config::setGraphicsState) - .setEnumNameProvider(ClothConfig::graphicsStateMessage) - .setTooltipSupplier(ClothConfig::graphicsStateTooltip) - .build()) - .addEntry(entryBuilder - .startBooleanToggle( - localized("config", "show_toasts"), - config.showToasts()) - .setDefaultValue(standard.showToasts()) - .setSaveConsumer(config::setShowToasts) - .setTooltip(localized("config", "show_toasts_tooltip")) - .build()) - .addEntry(entryBuilder - .startBooleanToggle( - localized("config", "run_garbage_collector"), - config.runGarbageCollector()) - .setDefaultValue(standard.runGarbageCollector()) - .setSaveConsumer(config::setRunGarbageCollector) - .setTooltip(localized("config", "run_garbage_collector_tooltip")) - .build()); + var category = builder.getOrCreateCategory( + localized("config", "category." + state.toString().toLowerCase()) + ); + + category.addEntry( + entryBuilder.startIntSlider( + localized("config", "frame_rate_target"), + fromConfigFpsTarget(config.frameRateTarget()), + 0, 61 + ) + .setDefaultValue(fromConfigFpsTarget(standard.frameRateTarget())) + .setSaveConsumer(value -> config.setFrameRateTarget(toConfigFpsTarget(value))) + .setTextGetter(ClothConfig::fpsTargetMessage) + .build() + ); + + category.addEntry( + entryBuilder.startIntSlider( + localized("config", "volume_multiplier"), + (int) (config.volumeMultiplier() * 100), + 0, 100 + ) + .setDefaultValue((int) (standard.volumeMultiplier() * 100)) + .setSaveConsumer(value -> config.setVolumeMultiplier(value / 100f)) + .setTextGetter(ClothConfig::volumeMultiplierMessage) + .build() + ); + + category.addEntry( + entryBuilder.startEnumSelector( + localized("config", "graphics_state"), + GraphicsState.class, + config.graphicsState() + ) + .setDefaultValue(standard.graphicsState()) + .setSaveConsumer(config::setGraphicsState) + .setEnumNameProvider(ClothConfig::graphicsStateMessage) + .setTooltipSupplier(ClothConfig::graphicsStateTooltip) + .build() + ); + + category.addEntry( + entryBuilder.startBooleanToggle( + localized("config", "show_toasts"), + config.showToasts() + ) + .setDefaultValue(standard.showToasts()) + .setSaveConsumer(config::setShowToasts) + .setTooltip(localized("config", "show_toasts_tooltip")) + .build() + ); + + category.addEntry( + entryBuilder.startBooleanToggle( + localized("config", "run_garbage_collector"), + config.runGarbageCollector() + ) + .setDefaultValue(standard.runGarbageCollector()) + .setSaveConsumer(config::setRunGarbageCollector) + .setTooltip(localized("config", "run_garbage_collector_tooltip")) + .build() + ); } return builder.build(); From 0d2ecfcd3a5d8cbf5e28e212e3e5f0e9a27d4143 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Thu, 5 Oct 2023 16:09:10 +0200 Subject: [PATCH 04/19] Use vanilla translations for graphics state value --- .../java/dynamic_fps/impl/compat/ClothConfig.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/dynamic_fps/impl/compat/ClothConfig.java b/src/main/java/dynamic_fps/impl/compat/ClothConfig.java index c1606dd2..fc726278 100644 --- a/src/main/java/dynamic_fps/impl/compat/ClothConfig.java +++ b/src/main/java/dynamic_fps/impl/compat/ClothConfig.java @@ -122,7 +122,18 @@ private static Component volumeMultiplierMessage(int value) { } private static Component graphicsStateMessage(Enum graphicsState) { - return localized("config", "graphics_state_" + graphicsState.toString()); + String key; + + if (graphicsState.equals(GraphicsState.DEFAULT)) { + key = "options.gamma.default"; + } else if (graphicsState.equals(GraphicsState.MINIMAL)) { + key = "options.particles.minimal"; + } else { + key = "options.particles.decreased"; + } + + return Component.translatable(key); + // return localized("config", "graphics_state_" + graphicsState.toString()); } private static Optional graphicsStateTooltip(GraphicsState graphicsState) { From 7a3aedaf167936287c16f07e6edcfc99674e4261 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Mon, 30 Oct 2023 14:10:59 +0100 Subject: [PATCH 05/19] Remove suspended power state entirely --- src/main/java/dynamic_fps/impl/DynamicFPSMod.java | 10 +--------- src/main/java/dynamic_fps/impl/PowerState.java | 7 +------ src/main/java/dynamic_fps/impl/config/Config.java | 1 - .../dynamic_fps/impl/config/DynamicFPSConfig.java | 14 ++++---------- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java index 8acbdaa0..121518aa 100644 --- a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java +++ b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java @@ -127,10 +127,6 @@ private static boolean isDisabledInternal() { return isDisabled || FREX.isFlawlessFramesActive(); } - private static boolean isPauseScreenOpened() { - return minecraft.screen instanceof PauseScreen; - } - private static boolean isLevelCoveredByScreen() { return minecraft.screen != null && minecraft.screen.dynamic_fps$rendersBackground(); } @@ -183,11 +179,7 @@ private static void checkForStateChanges() { } else if (isForcingLowFPS) { current = PowerState.UNFOCUSED; } else if (window.isFocused()) { - if (!isPauseScreenOpened()) { - current = PowerState.FOCUSED; - } else { - current = PowerState.SUSPENDED; - } + current = PowerState.FOCUSED; } else if (window.isHovered()) { current = PowerState.HOVERED; } else if (!window.isIconified()) { diff --git a/src/main/java/dynamic_fps/impl/PowerState.java b/src/main/java/dynamic_fps/impl/PowerState.java index 00a6d530..2fc1cee4 100644 --- a/src/main/java/dynamic_fps/impl/PowerState.java +++ b/src/main/java/dynamic_fps/impl/PowerState.java @@ -31,12 +31,7 @@ public enum PowerState { /* * Window minimized or otherwise hidden. */ - INVISIBLE(true), - - /* - * User is currently on the pause screen. - */ - SUSPENDED(false); + INVISIBLE(true); public final boolean configurable; diff --git a/src/main/java/dynamic_fps/impl/config/Config.java b/src/main/java/dynamic_fps/impl/config/Config.java index f6186d06..d01a3844 100644 --- a/src/main/java/dynamic_fps/impl/config/Config.java +++ b/src/main/java/dynamic_fps/impl/config/Config.java @@ -21,7 +21,6 @@ public final class Config { ).apply(instance, Config::new)); public static final Config ACTIVE = new Config(-1, 1.0f, GraphicsState.DEFAULT, true, false); - public static final Config SUSPENDED = new Config(60, 1.0f, GraphicsState.DEFAULT, true, false); public Config(int frameRateTarget, float volumeMultiplier, GraphicsState graphicsState, boolean showToasts, boolean runGarbageCollector) { this.frameRateTarget = frameRateTarget; diff --git a/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java b/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java index b030b860..747f5bff 100644 --- a/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java +++ b/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java @@ -39,16 +39,10 @@ private DynamicFPSConfig(Map configs) { } public Config get(PowerState state) { - switch (state) { - case FOCUSED: { - return Config.ACTIVE; - } - case SUSPENDED: { - return Config.SUSPENDED; - } - default: { - return configs.get(state); - } + if (state == PowerState.FOCUSED) { + return Config.ACTIVE; + } else { + return configs.get(state); } } From 961fe9cd0ea9f648b4c04b2426c5fa63c1ce0110 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Mon, 30 Oct 2023 14:32:41 +0100 Subject: [PATCH 06/19] Always run state change checks and tasks on main thread As far as I'm aware this has not caused any issues, but I'd like the implementation to be correct just in case. --- src/main/java/dynamic_fps/impl/DynamicFPSMod.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java index 8acbdaa0..558b95ac 100644 --- a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java +++ b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java @@ -176,6 +176,15 @@ private static void checkForStateChanges() { minecraft = Minecraft.getInstance(); } + if (minecraft.isSameThread()) { + checkForStateChanges0(); + } else { + // Schedule check for the beginning of the next frame + minecraft.tell(DynamicFPSMod::checkForStateChanges0); + } + } + + private static void checkForStateChanges0() { PowerState current; if (isDisabledInternal()) { From 6aad3c467b8a7bccc39d33a5bee5131af998e567 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Fri, 15 Dec 2023 12:19:13 +0100 Subject: [PATCH 07/19] Gradle 8.5 --- gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 28216 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac72c34e..1af9e093 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fcb6fca1..0adc8e1a 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum From 49e768ad834b2965e775290f6a29b587386f36e6 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Fri, 15 Dec 2023 12:19:34 +0100 Subject: [PATCH 08/19] Loom 1.4.5 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0924635a..78463213 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,4 +19,4 @@ cloth_config = { module = "me.shedaniel.cloth:cloth-config-fabric", version.ref [bundles] [plugins] -fabric_loom = { id = "fabric-loom", version = "1.4.1" } +fabric_loom = { id = "fabric-loom", version = "1.4.5" } From c1cc71adb73503b8cbc6cf59cff6d509c4608c75 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Fri, 15 Dec 2023 12:19:47 +0100 Subject: [PATCH 09/19] Use Java 21 in CI --- .github/workflows/build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0b9c9f8a..e484cb76 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,7 +8,7 @@ jobs: matrix: java: [ 17, - 20, + 21, ] os: [ ubuntu-22.04, @@ -34,7 +34,7 @@ jobs: arguments: build --warning-mode=all cache-read-only: ${{ github.ref != 'refs/heads/main' }} - name: capture build artifacts - if: ${{ runner.os == 'Linux' && matrix.java == '20' }} # Upload artifacts from one job, ignore the rest + if: ${{ runner.os == 'Linux' && matrix.java == '21' }} # Upload artifacts from one job, ignore the rest uses: actions/upload-artifact@v3 with: name: dynamic-fps-artifacts From dc8d0d56d3cf3958325c327ec7e3865a07086012 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Fri, 15 Dec 2023 13:00:45 +0100 Subject: [PATCH 10/19] Remove unusued import --- src/main/java/dynamic_fps/impl/DynamicFPSMod.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java index 7d020cbd..b3f7fd5b 100644 --- a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java +++ b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java @@ -15,7 +15,6 @@ import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.LoadingOverlay; -import net.minecraft.client.gui.screens.PauseScreen; import net.minecraft.sounds.SoundSource; import static dynamic_fps.impl.util.Localization.translationKey; From 2dccc281cc9d2cdba58423860793b0ade7a8e80f Mon Sep 17 00:00:00 2001 From: LostLuma Date: Fri, 15 Dec 2023 13:05:04 +0100 Subject: [PATCH 11/19] Only write config to disk on startup if it doesn't exist --- src/main/java/dynamic_fps/impl/DynamicFPSMod.java | 2 -- src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java index b3f7fd5b..03bc38a8 100644 --- a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java +++ b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java @@ -62,8 +62,6 @@ public class DynamicFPSMod implements ClientModInitializer { @Override public void onInitializeClient() { - modConfig.save(); // Force create file on disk - toggleForcedKeyBinding.register(); toggleDisabledKeyBinding.register(); diff --git a/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java b/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java index 747f5bff..5f075e1e 100644 --- a/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java +++ b/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java @@ -56,7 +56,9 @@ public static DynamicFPSConfig load() { try { data = Files.readString(PATH); } catch (NoSuchFileException e) { - return new DynamicFPSConfig(new EnumMap<>(PowerState.class)); + var config = new DynamicFPSConfig(new EnumMap<>(PowerState.class)); + config.save(); + return config; } catch (IOException e) { throw new RuntimeException("Failed to load Dynamic FPS config.", e); } From 18b2c08a3c44f9d6864d483c3cb6524cc357501c Mon Sep 17 00:00:00 2001 From: LostLuma Date: Fri, 15 Dec 2023 13:26:30 +0100 Subject: [PATCH 12/19] Save config atomically to prevent corruption --- .../impl/config/DynamicFPSConfig.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java b/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java index 5f075e1e..b256cf95 100644 --- a/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java +++ b/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java @@ -5,6 +5,7 @@ import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.EnumMap; import java.util.Map; @@ -21,7 +22,9 @@ public final class DynamicFPSConfig { private Map configs; - private static final Path PATH = FabricLoader.getInstance().getConfigDir().resolve(DynamicFPSMod.MOD_ID + ".json"); + private static final Path CONFIGS = FabricLoader.getInstance().getConfigDir(); + private static final Path CONFIG_FILE = CONFIGS.resolve(DynamicFPSMod.MOD_ID + ".json"); + private static final Codec> STATES_CODEC = Codec.unboundedMap(PowerState.CODEC, Config.CODEC); private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( @@ -54,7 +57,7 @@ public static DynamicFPSConfig load() { String data; try { - data = Files.readString(PATH); + data = Files.readString(CONFIG_FILE); } catch (NoSuchFileException e) { var config = new DynamicFPSConfig(new EnumMap<>(PowerState.class)); config.save(); @@ -74,10 +77,14 @@ public void save() { var root = data.getOrThrow(false, RuntimeException::new); try { - Files.writeString(PATH, root.toString(), StandardCharsets.UTF_8); + var temp = Files.createTempFile(CONFIGS, "dynamic_fps", ".json"); + Files.writeString(temp, root.toString(), StandardCharsets.UTF_8); + + Files.deleteIfExists(CONFIG_FILE); + Files.move(temp, CONFIG_FILE, StandardCopyOption.ATOMIC_MOVE); } catch (IOException e) { - // Cloth Config's automatic saving does not support catching exceptions - throw new RuntimeException("Failed to save Dynamic FPS config.", e); + // Cloth Config's built-in saving does not support catching exceptions :( + throw new RuntimeException("Failed to save or modify Dynamic FPS config!", e); } } From 2e95acad27182885d0ddef71a53f174e068b88f6 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Fri, 15 Dec 2023 18:45:43 +0100 Subject: [PATCH 13/19] Add support for setting a volume multiplier per sound category --- .../java/dynamic_fps/impl/DynamicFPSMod.java | 33 ++----- .../java/dynamic_fps/impl/GraphicsState.java | 30 ------- .../java/dynamic_fps/impl/PowerState.java | 25 ------ .../dynamic_fps/impl/compat/ClothConfig.java | 32 ++++--- .../java/dynamic_fps/impl/config/Config.java | 29 ++++--- .../impl/config/DynamicFPSConfig.java | 63 +++++++++++++- .../impl/mixin/LoadingOverlayMixin.java | 5 +- .../dynamic_fps/impl/mixin/ScreenMixin.java | 4 +- .../impl/mixin/SoundEngineMixin.java | 87 ++++++++++++++++++- .../impl/mixin/SoundManagerMixin.java | 21 +++++ .../impl/util/DynamicFPSScreen.java | 11 --- .../impl/util/DynamicFPSSplashOverlay.java | 7 -- .../java/dynamic_fps/impl/util/EnumCodec.java | 47 ++++++++++ .../impl/util/duck/DuckScreen.java | 11 +++ .../impl/util/duck/DuckSoundManager.java | 9 ++ .../impl/util/duck/DuckSplashOverlay.java | 7 ++ src/main/resources/dynamic_fps.mixins.json | 1 + src/main/resources/fabric.mod.json | 6 +- 18 files changed, 293 insertions(+), 135 deletions(-) create mode 100644 src/main/java/dynamic_fps/impl/mixin/SoundManagerMixin.java delete mode 100644 src/main/java/dynamic_fps/impl/util/DynamicFPSScreen.java delete mode 100644 src/main/java/dynamic_fps/impl/util/DynamicFPSSplashOverlay.java create mode 100644 src/main/java/dynamic_fps/impl/util/EnumCodec.java create mode 100644 src/main/java/dynamic_fps/impl/util/duck/DuckScreen.java create mode 100644 src/main/java/dynamic_fps/impl/util/duck/DuckSoundManager.java create mode 100644 src/main/java/dynamic_fps/impl/util/duck/DuckSplashOverlay.java diff --git a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java index 03bc38a8..f039aa1f 100644 --- a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java +++ b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java @@ -106,8 +106,8 @@ public static int targetFrameRate() { return config.frameRateTarget(); } - public static float volumeMultiplier() { - return config.volumeMultiplier(); + public static float volumeMultiplier(SoundSource source) { + return config.volumeMultiplier(source); } public static boolean shouldShowToasts() { @@ -147,8 +147,10 @@ public static void handleStateChange(PowerState previous, PowerState current) { System.gc(); } - if (before.volumeMultiplier() != config.volumeMultiplier()) { - setVolumeMultiplier(config.volumeMultiplier()); + for (var source : SoundSource.values()) { + if (before.volumeMultiplier(source) != config.volumeMultiplier(source)) { + minecraft.getSoundManager().dynamic_fps$updateVolume(source); + } } if (before.graphicsState() != config.graphicsState()) { @@ -202,29 +204,6 @@ private static void checkForStateChanges0() { } } - private static void setVolumeMultiplier(float multiplier) { - // Set the sound engine to a new volume multiplier, - // Or instead pause it when the multiplier is zero. - - // We can not set the sound engine to a zero volume - // Because it stops all actively playing sounds and - // Makes for a rather jarring experience when music - // Is stopped. Also fixes now-playing compatibility - - var manager = minecraft.getSoundManager(); - - if (multiplier == 0) { - manager.pause(); - } else { - manager.resume(); - - manager.updateSourceVolume( - SoundSource.MASTER, - minecraft.options.getSoundSourceVolume(SoundSource.MASTER) * multiplier - ); - } - } - private static boolean checkForRender(long timeSinceLastRender) { int frameRateTarget = config.frameRateTarget(); diff --git a/src/main/java/dynamic_fps/impl/GraphicsState.java b/src/main/java/dynamic_fps/impl/GraphicsState.java index 2b1e748e..597e9cff 100644 --- a/src/main/java/dynamic_fps/impl/GraphicsState.java +++ b/src/main/java/dynamic_fps/impl/GraphicsState.java @@ -1,12 +1,5 @@ package dynamic_fps.impl; -import java.util.Locale; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; -import com.mojang.serialization.DynamicOps; -import com.mojang.serialization.codecs.PrimitiveCodec; - /* * Graphics settings to apply within a given power state. */ @@ -25,27 +18,4 @@ public enum GraphicsState { * Reduce graphics settings to minimal values, this will reload the world! */ MINIMAL; - - public static final Codec CODEC = new PrimitiveCodec() { - @Override - public T write(DynamicOps ops, GraphicsState value) { - return ops.createString(value.toString()); - } - - @Override - public DataResult read(DynamicOps ops, T input) { - var value = ops.getStringValue(input).get().left(); - - if (value.isEmpty()) { - return DataResult.error(() -> "Graphics state must not be empty!"); - } else { - return DataResult.success(GraphicsState.valueOf(value.get().toUpperCase(Locale.ROOT))); - } - } - }; - - @Override - public String toString() { - return super.toString().toLowerCase(Locale.ROOT); - } } diff --git a/src/main/java/dynamic_fps/impl/PowerState.java b/src/main/java/dynamic_fps/impl/PowerState.java index 2fc1cee4..4b475cd2 100644 --- a/src/main/java/dynamic_fps/impl/PowerState.java +++ b/src/main/java/dynamic_fps/impl/PowerState.java @@ -1,12 +1,5 @@ package dynamic_fps.impl; -import java.util.Locale; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; -import com.mojang.serialization.DynamicOps; -import com.mojang.serialization.codecs.PrimitiveCodec; - /** * An analog for device power states, applied to the Minecraft window. * @@ -35,24 +28,6 @@ public enum PowerState { public final boolean configurable; - public static final Codec CODEC = new PrimitiveCodec() { - @Override - public T write(DynamicOps ops, PowerState value) { - return ops.createString(value.toString().toLowerCase(Locale.ROOT)); - } - - @Override - public DataResult read(DynamicOps ops, T input) { - var value = ops.getStringValue(input).get().left(); - - if (value.isEmpty()) { - return DataResult.error(() -> "Power state must not be empty!"); - } else { - return DataResult.success(PowerState.valueOf(value.get().toUpperCase(Locale.ROOT))); - } - } - }; - private PowerState(boolean configurable) { this.configurable = configurable; } diff --git a/src/main/java/dynamic_fps/impl/compat/ClothConfig.java b/src/main/java/dynamic_fps/impl/compat/ClothConfig.java index fc726278..a47a3c84 100644 --- a/src/main/java/dynamic_fps/impl/compat/ClothConfig.java +++ b/src/main/java/dynamic_fps/impl/compat/ClothConfig.java @@ -1,10 +1,10 @@ package dynamic_fps.impl.compat; import me.shedaniel.clothconfig2.api.ConfigBuilder; -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundSource; import static dynamic_fps.impl.util.Localization.localized; @@ -48,17 +48,25 @@ public static Screen genConfigScreen(Screen parent) { .build() ); - category.addEntry( - entryBuilder.startIntSlider( - localized("config", "volume_multiplier"), - (int) (config.volumeMultiplier() * 100), - 0, 100 - ) - .setDefaultValue((int) (standard.volumeMultiplier() * 100)) - .setSaveConsumer(value -> config.setVolumeMultiplier(value / 100f)) - .setTextGetter(ClothConfig::volumeMultiplierMessage) - .build() - ); + var volumes = entryBuilder.startSubCategory(localized("config", "volume_multiplier")); + + for (var source : SoundSource.values()) { + var name = source.getName(); + + volumes.add( + entryBuilder.startIntSlider( + Component.translatable("soundCategory." + name), + (int) (config.volumeMultiplier(source) * 100), + 0, 100 + ) + .setDefaultValue((int) (standard.volumeMultiplier(source) * 100)) + .setSaveConsumer(value -> config.setVolumeMultiplier(source, value / 100f)) + .setTextGetter(ClothConfig::volumeMultiplierMessage) + .build() + ); + } + + category.addEntry(volumes.build()); category.addEntry( entryBuilder.startEnumSelector( diff --git a/src/main/java/dynamic_fps/impl/config/Config.java b/src/main/java/dynamic_fps/impl/config/Config.java index d01a3844..530328df 100644 --- a/src/main/java/dynamic_fps/impl/config/Config.java +++ b/src/main/java/dynamic_fps/impl/config/Config.java @@ -1,30 +1,35 @@ package dynamic_fps.impl.config; +import java.util.HashMap; +import java.util.Map; + import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import dynamic_fps.impl.GraphicsState; +import dynamic_fps.impl.util.EnumCodec; +import net.minecraft.sounds.SoundSource; public final class Config { private int frameRateTarget; - private float volumeMultiplier; + private Map volumeMultipliers; private GraphicsState graphicsState; private boolean showToasts; private boolean runGarbageCollector; public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.INT.fieldOf("frame_rate_target").forGetter(Config::frameRateTarget), - Codec.FLOAT.fieldOf("volume_multiplier").forGetter(Config::volumeMultiplier), - GraphicsState.CODEC.fieldOf("graphics_state").forGetter(Config::graphicsState), + Codec.unboundedMap(new EnumCodec<>(SoundSource.values()), Codec.FLOAT).fieldOf("volume_multipliers").forGetter(Config::volumeMultipliers), + new EnumCodec<>(GraphicsState.values()).fieldOf("graphics_state").forGetter(Config::graphicsState), Codec.BOOL.fieldOf("show_toasts").forGetter(Config::showToasts), Codec.BOOL.fieldOf("run_garbage_collector").forGetter(Config::runGarbageCollector) ).apply(instance, Config::new)); - public static final Config ACTIVE = new Config(-1, 1.0f, GraphicsState.DEFAULT, true, false); + public static final Config ACTIVE = new Config(-1, new HashMap<>(), GraphicsState.DEFAULT, true, false); - public Config(int frameRateTarget, float volumeMultiplier, GraphicsState graphicsState, boolean showToasts, boolean runGarbageCollector) { + public Config(int frameRateTarget, Map volumeMultipliers, GraphicsState graphicsState, boolean showToasts, boolean runGarbageCollector) { this.frameRateTarget = frameRateTarget; - this.volumeMultiplier = volumeMultiplier; + this.volumeMultipliers = new HashMap<>(volumeMultipliers); // Ensure the map is mutable this.graphicsState = graphicsState; this.showToasts = showToasts; this.runGarbageCollector = runGarbageCollector; @@ -38,12 +43,16 @@ public void setFrameRateTarget(int value) { this.frameRateTarget = value; } - public float volumeMultiplier() { - return this.volumeMultiplier; + public Map volumeMultipliers() { + return this.volumeMultipliers; + } + + public float volumeMultiplier(SoundSource category) { + return this.volumeMultipliers.getOrDefault(category, 1.0f); } - public void setVolumeMultiplier(float value) { - this.volumeMultiplier = value; + public void setVolumeMultiplier(SoundSource category, float value) { + this.volumeMultipliers.put(category, value); } public GraphicsState graphicsState() { diff --git a/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java b/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java index b256cf95..d55f27b4 100644 --- a/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java +++ b/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java @@ -7,9 +7,12 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.EnumMap; +import java.util.HashMap; import java.util.Map; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; @@ -17,7 +20,9 @@ import dynamic_fps.impl.DynamicFPSMod; import dynamic_fps.impl.GraphicsState; import dynamic_fps.impl.PowerState; +import dynamic_fps.impl.util.EnumCodec; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.sounds.SoundSource; public final class DynamicFPSConfig { private Map configs; @@ -25,7 +30,7 @@ public final class DynamicFPSConfig { private static final Path CONFIGS = FabricLoader.getInstance().getConfigDir(); private static final Path CONFIG_FILE = CONFIGS.resolve(DynamicFPSMod.MOD_ID + ".json"); - private static final Codec> STATES_CODEC = Codec.unboundedMap(PowerState.CODEC, Config.CODEC); + private static final Codec> STATES_CODEC = Codec.unboundedMap(new EnumCodec<>(PowerState.values()), Config.CODEC); private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( STATES_CODEC.fieldOf("states").forGetter(DynamicFPSConfig::configs) @@ -67,6 +72,8 @@ public static DynamicFPSConfig load() { } var root = JsonParser.parseString(data); + + upgradeConfig((JsonObject) root); var parsed = CODEC.parse(JsonOps.INSTANCE, root); return parsed.getOrThrow(false, RuntimeException::new); @@ -91,17 +98,65 @@ public void save() { public static Config getDefaultConfig(PowerState state) { switch (state) { case HOVERED: { - return new Config(60, 1.0f, GraphicsState.DEFAULT, true, false); + return new Config(60, withMasterVolume(1.0f), GraphicsState.DEFAULT, true, false); } case UNFOCUSED: { - return new Config(1, 0.25f, GraphicsState.DEFAULT, false, false); + return new Config(1, withMasterVolume(0.25f), GraphicsState.DEFAULT, false, false); } case INVISIBLE: { - return new Config(0, 0.0f, GraphicsState.DEFAULT, false, false); + return new Config(0, withMasterVolume(0.0f), GraphicsState.DEFAULT, false, false); } default: { throw new RuntimeException("Getting default configuration for unhandled power state " + state.toString()); } } } + + private static Map withMasterVolume(float value) { + var volumes = new HashMap(); + volumes.put(SoundSource.MASTER, value); + return volumes; + } + + private static void upgradeConfig(JsonObject root) { + upgradeVolumeMultiplier(root); + } + + private static void upgradeVolumeMultiplier(JsonObject root) { + // Convert each old power state config + // - { "volume_multiplier": 0.0, ... } + // + { "volume_multipliers": { "master": 0.0 }, ... } + if (!root.has("states")) { + return; + } + + var states = root.getAsJsonObject("states"); + + if (!states.isJsonObject()) { + return; + } + + for (var key : states.keySet()) { + var element = states.getAsJsonObject(key); + + if (!element.isJsonObject()) { + continue; + } + + if (!element.has("volume_multiplier")) { + continue; + } + + var multiplier = element.get("volume_multiplier"); + + if (!multiplier.isJsonPrimitive() || !((JsonPrimitive)multiplier).isNumber()) { + continue; + } + + var multipliers = new JsonObject(); + multipliers.add("master", multiplier); + + element.add("volume_multipliers", multipliers); + } + } } diff --git a/src/main/java/dynamic_fps/impl/mixin/LoadingOverlayMixin.java b/src/main/java/dynamic_fps/impl/mixin/LoadingOverlayMixin.java index a83c5012..5b61bd22 100644 --- a/src/main/java/dynamic_fps/impl/mixin/LoadingOverlayMixin.java +++ b/src/main/java/dynamic_fps/impl/mixin/LoadingOverlayMixin.java @@ -1,13 +1,14 @@ package dynamic_fps.impl.mixin; -import dynamic_fps.impl.util.DynamicFPSSplashOverlay; import net.minecraft.client.gui.screens.LoadingOverlay; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import dynamic_fps.impl.util.duck.DuckSplashOverlay; + @Mixin(LoadingOverlay.class) -public class LoadingOverlayMixin implements DynamicFPSSplashOverlay { +public class LoadingOverlayMixin implements DuckSplashOverlay { @Shadow private long fadeOutStart; diff --git a/src/main/java/dynamic_fps/impl/mixin/ScreenMixin.java b/src/main/java/dynamic_fps/impl/mixin/ScreenMixin.java index aad68ac2..89683877 100644 --- a/src/main/java/dynamic_fps/impl/mixin/ScreenMixin.java +++ b/src/main/java/dynamic_fps/impl/mixin/ScreenMixin.java @@ -6,12 +6,12 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import dynamic_fps.impl.util.DynamicFPSScreen; import dynamic_fps.impl.util.ModCompatibility; +import dynamic_fps.impl.util.duck.DuckScreen; import net.minecraft.client.gui.screens.Screen; @Mixin(Screen.class) -public class ScreenMixin implements DynamicFPSScreen { +public class ScreenMixin implements DuckScreen { @Unique private boolean dynamic_fps$canOptimize = false; diff --git a/src/main/java/dynamic_fps/impl/mixin/SoundEngineMixin.java b/src/main/java/dynamic_fps/impl/mixin/SoundEngineMixin.java index d99aa446..ce1e3880 100644 --- a/src/main/java/dynamic_fps/impl/mixin/SoundEngineMixin.java +++ b/src/main/java/dynamic_fps/impl/mixin/SoundEngineMixin.java @@ -1,15 +1,83 @@ package dynamic_fps.impl.mixin; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; 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; + +import com.mojang.blaze3d.audio.Listener; import dynamic_fps.impl.DynamicFPSMod; +import dynamic_fps.impl.util.duck.DuckSoundManager; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.client.sounds.ChannelAccess; import net.minecraft.client.sounds.SoundEngine; +import net.minecraft.sounds.SoundSource; @Mixin(SoundEngine.class) -public class SoundEngineMixin { +public class SoundEngineMixin implements DuckSoundManager { + @Shadow + private boolean loaded; + + @Shadow + @Final + private Listener listener; + + @Shadow + @Final + private Map instanceToChannel; + + @Shadow + private float getVolume(@Nullable SoundSource source) { + throw new RuntimeException("Failed to find SoundEngine#getVolume."); + } + + @Shadow + private float calculateVolume(SoundInstance instance) { + throw new RuntimeException("Failed to find SoundEngine#calculateVolume."); + }; + + public void dynamic_fps$updateVolume(SoundSource source) { + if (!this.loaded) { + return; + } + + if (source.equals(SoundSource.MASTER)) { + this.listener.setGain(this.getVolume(source)); + return; + } + + // When setting the volume to zero we pause music but cancel other types of sounds + // This results in a less jarring experience when quickly tabbing out and back in. + // Also fixes this compat bug: https://github.com/juliand665/Dynamic-FPS/issues/55 + var isMusic = source.equals(SoundSource.MUSIC) || source.equals(SoundSource.RECORDS); + + this.instanceToChannel.forEach((instance, handle) -> { + float volume = this.calculateVolume((SoundInstance) instance); + + if (instance.getSource().equals(source)) { + handle.execute(channel -> { + if (volume <= 0.0f) { + if (!isMusic) { + channel.stop(); + } else { + channel.pause(); + } + } else { + channel.unpause(); + channel.setVolume(volume); + } + }); + } + }); + } + /** * Cancels playing sounds while we are overwriting the volume to be off. * @@ -17,9 +85,22 @@ public class SoundEngineMixin { * Allows pausing and resuming the sound engine without cancelling active sounds. */ @Inject(method = { "play", "playDelayed" }, at = @At("HEAD"), cancellable = true) - private void play(CallbackInfo callbackInfo) { - if (DynamicFPSMod.volumeMultiplier() == 0.0f) { + private void play(SoundInstance instance, CallbackInfo callbackInfo) { + var master = DynamicFPSMod.volumeMultiplier(SoundSource.MASTER); + var source = DynamicFPSMod.volumeMultiplier(instance.getSource()); + + if (master == 0.0f || source == 0.0f) { callbackInfo.cancel(); } } + + /** + * Applies the user's requested volume multiplier to any newly played sounds. + */ + @Inject(method = "getVolume", at = @At("RETURN"), cancellable = true) + private void getVolume(@Nullable SoundSource source, CallbackInfoReturnable callbackInfo) { + if (source != null) { + callbackInfo.setReturnValue(callbackInfo.getReturnValue() * DynamicFPSMod.volumeMultiplier(source)); + } + } } diff --git a/src/main/java/dynamic_fps/impl/mixin/SoundManagerMixin.java b/src/main/java/dynamic_fps/impl/mixin/SoundManagerMixin.java new file mode 100644 index 00000000..f087c307 --- /dev/null +++ b/src/main/java/dynamic_fps/impl/mixin/SoundManagerMixin.java @@ -0,0 +1,21 @@ +package dynamic_fps.impl.mixin; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import dynamic_fps.impl.util.duck.DuckSoundManager; +import net.minecraft.client.sounds.SoundEngine; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.sounds.SoundSource; + +@Mixin(SoundManager.class) +public class SoundManagerMixin implements DuckSoundManager { + @Shadow + @Final + private SoundEngine soundEngine; + + public void dynamic_fps$updateVolume(SoundSource source) { + this.soundEngine.dynamic_fps$updateVolume(source); + } +} diff --git a/src/main/java/dynamic_fps/impl/util/DynamicFPSScreen.java b/src/main/java/dynamic_fps/impl/util/DynamicFPSScreen.java deleted file mode 100644 index aa773535..00000000 --- a/src/main/java/dynamic_fps/impl/util/DynamicFPSScreen.java +++ /dev/null @@ -1,11 +0,0 @@ -package dynamic_fps.impl.util; - -public interface DynamicFPSScreen { - public default boolean dynamic_fps$rendersBackground() { - throw new RuntimeException("Dynamic FPS' Screen mixin was not applied."); - } - - public default void dynamic_fps$setRendersBackground() { - throw new RuntimeException("Dynamic FPS' Screen mixin was not applied."); - } -} diff --git a/src/main/java/dynamic_fps/impl/util/DynamicFPSSplashOverlay.java b/src/main/java/dynamic_fps/impl/util/DynamicFPSSplashOverlay.java deleted file mode 100644 index 57043224..00000000 --- a/src/main/java/dynamic_fps/impl/util/DynamicFPSSplashOverlay.java +++ /dev/null @@ -1,7 +0,0 @@ -package dynamic_fps.impl.util; - -public interface DynamicFPSSplashOverlay { - public default boolean dynamic_fps$isReloadComplete() { - throw new RuntimeException("Dynamic FPS' SplashOverlay mixin was not applied."); - } -} diff --git a/src/main/java/dynamic_fps/impl/util/EnumCodec.java b/src/main/java/dynamic_fps/impl/util/EnumCodec.java new file mode 100644 index 00000000..8ba84eed --- /dev/null +++ b/src/main/java/dynamic_fps/impl/util/EnumCodec.java @@ -0,0 +1,47 @@ +package dynamic_fps.impl.util; + +import java.util.Locale; + +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.codecs.PrimitiveCodec; + +public class EnumCodec> implements PrimitiveCodec { + private final E[] members; + + public EnumCodec(E[] members) { + this.members = members; + + if (members.length == 0) { + throw new RuntimeException("EnumCodec has no members!"); + } + } + + @Override + public T write(DynamicOps ops, E value) { + return ops.createString(value.toString().toLowerCase(Locale.ROOT)); + } + + @Override + public DataResult read(DynamicOps ops, T input) { + var value = ops.getStringValue(input).get().left(); + + if (value.isEmpty()) { + return DataResult.error(() -> this.getTypeName() + " must not be empty!"); + } + + var inner = value.get().toUpperCase(Locale.ROOT); + + for (var member : this.members) { + if (member.name().equals(inner)) { + return DataResult.success(member); + } + } + + return DataResult.error(() -> this.getTypeName() + " has no value " + inner + "!"); + } + + private String getTypeName() { + return this.members[0].getDeclaringClass().getName(); + } +} diff --git a/src/main/java/dynamic_fps/impl/util/duck/DuckScreen.java b/src/main/java/dynamic_fps/impl/util/duck/DuckScreen.java new file mode 100644 index 00000000..6d560e40 --- /dev/null +++ b/src/main/java/dynamic_fps/impl/util/duck/DuckScreen.java @@ -0,0 +1,11 @@ +package dynamic_fps.impl.util.duck; + +public interface DuckScreen { + public default boolean dynamic_fps$rendersBackground() { + throw new RuntimeException("No implementation for dynamic_fps$rendersBackground was found."); + } + + public default void dynamic_fps$setRendersBackground() { + throw new RuntimeException("No implementation for dynamic_fps$rendersBackground was found."); + } +} diff --git a/src/main/java/dynamic_fps/impl/util/duck/DuckSoundManager.java b/src/main/java/dynamic_fps/impl/util/duck/DuckSoundManager.java new file mode 100644 index 00000000..41c8f00a --- /dev/null +++ b/src/main/java/dynamic_fps/impl/util/duck/DuckSoundManager.java @@ -0,0 +1,9 @@ +package dynamic_fps.impl.util.duck; + +import net.minecraft.sounds.SoundSource; + +public interface DuckSoundManager { + public default void dynamic_fps$updateVolume(SoundSource source) { + throw new RuntimeException("No implementation for dynamic_fps$updateVolume was found."); + } +} diff --git a/src/main/java/dynamic_fps/impl/util/duck/DuckSplashOverlay.java b/src/main/java/dynamic_fps/impl/util/duck/DuckSplashOverlay.java new file mode 100644 index 00000000..bdedb888 --- /dev/null +++ b/src/main/java/dynamic_fps/impl/util/duck/DuckSplashOverlay.java @@ -0,0 +1,7 @@ +package dynamic_fps.impl.util.duck; + +public interface DuckSplashOverlay { + public default boolean dynamic_fps$isReloadComplete() { + throw new RuntimeException("No implementation for dynamic_fps$isReloadComplete was found."); + } +} diff --git a/src/main/resources/dynamic_fps.mixins.json b/src/main/resources/dynamic_fps.mixins.json index da9d1887..6b661ccc 100644 --- a/src/main/resources/dynamic_fps.mixins.json +++ b/src/main/resources/dynamic_fps.mixins.json @@ -11,6 +11,7 @@ "MinecraftMixin", "ScreenMixin", "SoundEngineMixin", + "SoundManagerMixin", "StatsScreenMixin", "ToastComponentMixin", "WindowMixin", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 98698870..c7103d0c 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -43,8 +43,10 @@ "custom": { "loom:injected_interfaces": { - "net/minecraft/class_437": ["dynamic_fps/impl/util/DynamicFPSScreen"], - "net/minecraft/class_425": ["dynamic_fps/impl/util/DynamicFPSSplashOverlay"] + "net/minecraft/class_437": ["dynamic_fps/impl/util/duck/DuckScreen"], + "net/minecraft/class_425": ["dynamic_fps/impl/util/duck/DuckSplashOverlay"], + "net/minecraft/class_1140": ["dynamic_fps/impl/util/duck/DuckSoundManager"], + "net/minecraft/class_1144": ["dynamic_fps/impl/util/duck/DuckSoundManager"] }, "dynamic_fps": { "optimized_screens": { From ab6ceac42caf2f0f31a47b5e2a26a77338a3201e Mon Sep 17 00:00:00 2001 From: LostLuma Date: Sat, 16 Dec 2023 12:17:16 +0100 Subject: [PATCH 14/19] Add idle detection and mode --- build.gradle | 4 + .../java/dynamic_fps/impl/DynamicFPSMod.java | 55 ++++++++++- .../java/dynamic_fps/impl/PowerState.java | 13 ++- .../dynamic_fps/impl/compat/ClothConfig.java | 27 ++++- .../impl/config/DynamicFPSConfig.java | 26 ++++- .../impl/mixin/MinecraftMixin.java | 13 +++ .../dynamic_fps/impl/mixin/WindowMixin.java | 12 --- .../impl/util/event/InputObserver.java | 99 +++++++++++++++++++ .../impl/util/{ => event}/WindowObserver.java | 20 ++-- .../assets/dynamic_fps/lang/de_at.json | 2 + .../assets/dynamic_fps/lang/de_ch.json | 2 + .../assets/dynamic_fps/lang/de_de.json | 2 + .../assets/dynamic_fps/lang/en_us.json | 8 ++ .../assets/dynamic_fps/lang/et_ee.json | 2 + .../assets/dynamic_fps/lang/fr_ca.json | 1 + .../assets/dynamic_fps/lang/fr_fr.json | 1 + .../assets/dynamic_fps/lang/it_it.json | 3 +- .../assets/dynamic_fps/lang/ko_kr.json | 2 + .../assets/dynamic_fps/lang/pl_pl.json | 2 + .../assets/dynamic_fps/lang/pt_br.json | 2 + .../assets/dynamic_fps/lang/pt_pt.json | 2 + .../assets/dynamic_fps/lang/ru_ru.json | 1 + .../assets/dynamic_fps/lang/sv_se.json | 5 +- .../assets/dynamic_fps/lang/tr_tr.json | 2 + .../assets/dynamic_fps/lang/uk_ua.json | 2 + .../assets/dynamic_fps/lang/vi_vn.json | 2 + .../assets/dynamic_fps/lang/zh_cn.json | 1 + .../assets/dynamic_fps/lang/zh_tw.json | 1 + src/main/resources/dynamic_fps.accesswidener | 3 + src/main/resources/fabric.mod.json | 1 + 30 files changed, 282 insertions(+), 34 deletions(-) create mode 100644 src/main/java/dynamic_fps/impl/util/event/InputObserver.java rename src/main/java/dynamic_fps/impl/util/{ => event}/WindowObserver.java (70%) create mode 100644 src/main/resources/dynamic_fps.accesswidener diff --git a/build.gradle b/build.gradle index 0fdcfdf6..06cab9b5 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,10 @@ dependencies { modApi libs.cloth.config } +loom { + accessWidenerPath = file("src/main/resources/dynamic_fps.accesswidener") +} + processResources { inputs.property "version", generateVersion() diff --git a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java index f039aa1f..899e7f80 100644 --- a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java +++ b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java @@ -8,8 +8,10 @@ import dynamic_fps.impl.util.Logging; import dynamic_fps.impl.util.ModCompatibility; import dynamic_fps.impl.util.OptionsHolder; -import dynamic_fps.impl.util.WindowObserver; +import dynamic_fps.impl.util.event.InputObserver; +import dynamic_fps.impl.util.event.WindowObserver; import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.Util; @@ -32,10 +34,15 @@ public class DynamicFPSMod implements ClientModInitializer { private static boolean isForcingLowFPS = false; private static Minecraft minecraft; + private static WindowObserver window; + private static InputObserver devices; private static long lastRender; + private static boolean wasIdle = false; + private static boolean tickEventRegistered = false; + // we always render one last frame before actually reducing FPS, so the hud text // shows up instantly when forcing low fps. // additionally, this would enable mods which render differently while mc is @@ -65,6 +72,7 @@ public void onInitializeClient() { toggleForcedKeyBinding.register(); toggleDisabledKeyBinding.register(); + registerTickEvent(); HudRenderCallback.EVENT.register(new HudInfoRenderer()); } @@ -78,6 +86,11 @@ public static void onStatusChanged() { checkForStateChanges(); } + public static void onConfigChanged() { + modConfig.save(); + registerTickEvent(); + } + public static PowerState powerState() { return state; } @@ -88,6 +101,7 @@ public static boolean isForcingLowFPS() { public static void setWindow(long address) { window = new WindowObserver(address); + devices = new InputObserver(address); } public static boolean checkForRender() { @@ -128,10 +142,43 @@ private static boolean isLevelCoveredByScreen() { return minecraft.screen != null && minecraft.screen.dynamic_fps$rendersBackground(); } + private static boolean isIdle() { + var idleTime = modConfig.idleTime(); + + if (idleTime == 0) { + return false; + } + + return (Util.getEpochMillis() - devices.lastActionTime()) >= idleTime * 1000; + } + private static boolean isLevelCoveredByOverlay() { return OVERLAY_OPTIMIZATION_ACTIVE && minecraft.getOverlay() instanceof LoadingOverlay loadingOverlay && loadingOverlay.dynamic_fps$isReloadComplete(); } + private static void registerTickEvent() { + if (tickEventRegistered) { + return; + } + + if (modConfig.idleTime() == -1) { + return; + } + + tickEventRegistered = true; + + // I assume world ticks only happen 20 times a second + // Instead of whatever amount of FPS we get at the moment? + ClientTickEvents.START_WORLD_TICK.register((minecraft) -> { + var idle = isIdle(); + + if (idle != wasIdle) { + wasIdle = idle; + onStatusChanged(); + } + }); + } + @SuppressWarnings("squid:S1215") // Garbage collector call public static void handleStateChange(PowerState previous, PowerState current) { if (DEBUG) { @@ -187,7 +234,11 @@ private static void checkForStateChanges0() { } else if (isForcingLowFPS) { current = PowerState.UNFOCUSED; } else if (window.isFocused()) { - current = PowerState.FOCUSED; + if (!isIdle()) { + current = PowerState.FOCUSED; + } else { + current = PowerState.ABANDONED; + } } else if (window.isHovered()) { current = PowerState.HOVERED; } else if (!window.isIconified()) { diff --git a/src/main/java/dynamic_fps/impl/PowerState.java b/src/main/java/dynamic_fps/impl/PowerState.java index 4b475cd2..fd1ad563 100644 --- a/src/main/java/dynamic_fps/impl/PowerState.java +++ b/src/main/java/dynamic_fps/impl/PowerState.java @@ -6,22 +6,27 @@ * Power states are prioritized based on their order here, see DynamicFPSMod.checkForStateChanges for impl details. */ public enum PowerState { - /* + /** * Window is currently focused. */ FOCUSED(false), - /* + /** * Mouse positioned over unfocused window. */ HOVERED(true), - /* + /** * Another application is focused. */ UNFOCUSED(true), - /* + /** + * User hasn't sent input for some time. + */ + ABANDONED(true), + + /** * Window minimized or otherwise hidden. */ INVISIBLE(true); diff --git a/src/main/java/dynamic_fps/impl/compat/ClothConfig.java b/src/main/java/dynamic_fps/impl/compat/ClothConfig.java index a47a3c84..8f54694d 100644 --- a/src/main/java/dynamic_fps/impl/compat/ClothConfig.java +++ b/src/main/java/dynamic_fps/impl/compat/ClothConfig.java @@ -20,10 +20,27 @@ public static Screen genConfigScreen(Screen parent) { var builder = ConfigBuilder.create() .setParentScreen(parent) .setTitle(localized("config", "title")) - .setSavingRunnable(DynamicFPSMod.modConfig::save); + .setSavingRunnable(DynamicFPSMod::onConfigChanged); var entryBuilder = builder.entryBuilder(); + var general = builder.getOrCreateCategory( + localized("config", "category.general") + ); + + general.addEntry( + entryBuilder.startIntSlider( + localized("config", "idle_time"), + DynamicFPSMod.modConfig.idleTime() / 60, + 0, 30 + ) + .setDefaultValue(0) + .setSaveConsumer(value -> DynamicFPSMod.modConfig.setIdleTime(value * 60)) + .setTextGetter(ClothConfig::idleTimeMessage) + .setTooltip(localized("config", "idle_time_tooltip")) + .build() + ); + for (var state : PowerState.values()) { if (!state.configurable) { continue; @@ -107,6 +124,14 @@ public static Screen genConfigScreen(Screen parent) { return builder.build(); } + private static Component idleTimeMessage(int value) { + if (value == 0) { + return localized("config", "disabled"); + } else { + return localized("config", "minutes", value); + } + } + // Convert magic -1 number to 61 (and reverse) // So the "unlocked" FPS value is on the right private static int toConfigFpsTarget(int value) { diff --git a/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java b/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java index d55f27b4..421440a9 100644 --- a/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java +++ b/src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java @@ -25,6 +25,7 @@ import net.minecraft.sounds.SoundSource; public final class DynamicFPSConfig { + private int idleTime; // Seconds private Map configs; private static final Path CONFIGS = FabricLoader.getInstance().getConfigDir(); @@ -33,10 +34,12 @@ public final class DynamicFPSConfig { private static final Codec> STATES_CODEC = Codec.unboundedMap(new EnumCodec<>(PowerState.values()), Config.CODEC); private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.intRange(0, 30 * 60).fieldOf("idle_time").forGetter(DynamicFPSConfig::idleTime), STATES_CODEC.fieldOf("states").forGetter(DynamicFPSConfig::configs) ).apply(instance, DynamicFPSConfig::new)); - private DynamicFPSConfig(Map configs) { + private DynamicFPSConfig(int abandonTime, Map configs) { + this.idleTime = abandonTime; this.configs = new EnumMap<>(configs); for (var state : PowerState.values()) { @@ -54,6 +57,14 @@ public Config get(PowerState state) { } } + public int idleTime() { + return this.idleTime; + } + + public void setIdleTime(int value) { + this.idleTime = value; + } + private Map configs() { return this.configs; } @@ -64,7 +75,7 @@ public static DynamicFPSConfig load() { try { data = Files.readString(CONFIG_FILE); } catch (NoSuchFileException e) { - var config = new DynamicFPSConfig(new EnumMap<>(PowerState.class)); + var config = new DynamicFPSConfig(0, new EnumMap<>(PowerState.class)); config.save(); return config; } catch (IOException e) { @@ -103,6 +114,9 @@ public static Config getDefaultConfig(PowerState state) { case UNFOCUSED: { return new Config(1, withMasterVolume(0.25f), GraphicsState.DEFAULT, false, false); } + case ABANDONED: { + return new Config(10, withMasterVolume(1.0f), GraphicsState.DEFAULT, false, false); + } case INVISIBLE: { return new Config(0, withMasterVolume(0.0f), GraphicsState.DEFAULT, false, false); } @@ -119,9 +133,17 @@ private static Map withMasterVolume(float value) { } private static void upgradeConfig(JsonObject root) { + upgradeIdleTime(root); upgradeVolumeMultiplier(root); } + private static void upgradeIdleTime(JsonObject root) { + // Add idle_time field if it's missing + if (!root.has("idle_time")) { + root.addProperty("idle_time", 0); + } + } + private static void upgradeVolumeMultiplier(JsonObject root) { // Convert each old power state config // - { "volume_multiplier": 0.0, ... } diff --git a/src/main/java/dynamic_fps/impl/mixin/MinecraftMixin.java b/src/main/java/dynamic_fps/impl/mixin/MinecraftMixin.java index 7f40e2db..0b5ab38b 100644 --- a/src/main/java/dynamic_fps/impl/mixin/MinecraftMixin.java +++ b/src/main/java/dynamic_fps/impl/mixin/MinecraftMixin.java @@ -1,15 +1,28 @@ package dynamic_fps.impl.mixin; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import com.mojang.blaze3d.platform.Window; + import dynamic_fps.impl.DynamicFPSMod; import net.minecraft.client.Minecraft; @Mixin(Minecraft.class) public class MinecraftMixin { + @Shadow + @Final + private Window window; + + @Inject(method = "", at = @At("TAIL")) + private void onInit(CallbackInfo callbackInfo) { + DynamicFPSMod.setWindow(this.window.window); + } + @Inject(method = "setScreen", at = @At("TAIL")) private void onSetScreen(CallbackInfo callbackInfo) { DynamicFPSMod.onStatusChanged(); diff --git a/src/main/java/dynamic_fps/impl/mixin/WindowMixin.java b/src/main/java/dynamic_fps/impl/mixin/WindowMixin.java index 1550fcf4..3663a496 100644 --- a/src/main/java/dynamic_fps/impl/mixin/WindowMixin.java +++ b/src/main/java/dynamic_fps/impl/mixin/WindowMixin.java @@ -1,11 +1,8 @@ package dynamic_fps.impl.mixin; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; 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; import com.mojang.blaze3d.platform.Window; @@ -14,15 +11,6 @@ @Mixin(Window.class) public class WindowMixin { - @Shadow - @Final - private long window; - - @Inject(method = "", at = @At("TAIL")) - private void postinit(CallbackInfo callbackInfo) { - DynamicFPSMod.setWindow(this.window); - } - /** * Sets a frame rate limit while we're cancelling some or all rendering. */ diff --git a/src/main/java/dynamic_fps/impl/util/event/InputObserver.java b/src/main/java/dynamic_fps/impl/util/event/InputObserver.java new file mode 100644 index 00000000..3038c0b5 --- /dev/null +++ b/src/main/java/dynamic_fps/impl/util/event/InputObserver.java @@ -0,0 +1,99 @@ +package dynamic_fps.impl.util.event; + +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWCharModsCallback; +import org.lwjgl.glfw.GLFWCursorPosCallback; +import org.lwjgl.glfw.GLFWDropCallback; +import org.lwjgl.glfw.GLFWKeyCallback; +import org.lwjgl.glfw.GLFWMouseButtonCallback; +import org.lwjgl.glfw.GLFWScrollCallback; + +import net.minecraft.Util; + +public class InputObserver { + private final long window; + + private long lastAction = Util.getEpochMillis(); + + // Keyboard + private final GLFWKeyCallback previousKeyCallback; + private final GLFWCharModsCallback previousCharModsCallback; + + // Mouse / Trackpad etc. + private final GLFWDropCallback previousDropCallback; + private final GLFWScrollCallback previousScrollCallback; + private final GLFWCursorPosCallback previousCursorPosCallback; + private final GLFWMouseButtonCallback previousMouseClickCallback; + + public InputObserver(long address) { + this.window = address; + + this.previousKeyCallback = GLFW.glfwSetKeyCallback(this.window, this::onKey); + this.previousCharModsCallback = GLFW.glfwSetCharModsCallback(this.window, this::onCharMods); + + this.previousDropCallback = GLFW.glfwSetDropCallback(this.window, this::onDrop); + this.previousScrollCallback = GLFW.glfwSetScrollCallback(this.window, this::onScroll); + this.previousCursorPosCallback = GLFW.glfwSetCursorPosCallback(this.window, this::onMove); + this.previousMouseClickCallback = GLFW.glfwSetMouseButtonCallback(this.window, this::onPress); + } + + public long lastActionTime() { + return this.lastAction; + } + + private void updateTime() { + this.lastAction = Util.getEpochMillis(); + } + + // Keyboard events + + private void onKey(long address, int key, int scancode, int action, int mods) { + this.updateTime(); + + if (this.previousKeyCallback != null) { + this.previousKeyCallback.invoke(address, key, scancode, action, mods); + } + } + + private void onCharMods(long address, int codepoint, int mods) { + this.updateTime(); + + if (this.previousCharModsCallback != null) { + this.previousCharModsCallback.invoke(address, codepoint, mods); + } + } + + // Mouse events + + private void onDrop(long address, int count, long names) { + this.updateTime(); + + if (this.previousDropCallback != null) { + this.previousDropCallback.invoke(address, count, names); + } + } + + private void onScroll(long address, double xoffset, double yoffset) { + this.updateTime(); + + if (this.previousScrollCallback != null) { + this.previousScrollCallback.invoke(address, xoffset, yoffset); + } + } + + private void onMove(long address, double x, double y) { + this.updateTime(); + + if (this.previousCursorPosCallback != null) { + this.previousCursorPosCallback.invoke(address, x, y); + } + } + + private void onPress(long address, int button, int action, int mods) { + this.updateTime(); + + if (this.previousMouseClickCallback != null) { + this.previousMouseClickCallback.invoke(address, button, action, mods); + } + } +} diff --git a/src/main/java/dynamic_fps/impl/util/WindowObserver.java b/src/main/java/dynamic_fps/impl/util/event/WindowObserver.java similarity index 70% rename from src/main/java/dynamic_fps/impl/util/WindowObserver.java rename to src/main/java/dynamic_fps/impl/util/event/WindowObserver.java index a0885ee9..6257bcaa 100644 --- a/src/main/java/dynamic_fps/impl/util/WindowObserver.java +++ b/src/main/java/dynamic_fps/impl/util/event/WindowObserver.java @@ -1,4 +1,4 @@ -package dynamic_fps.impl.util; +package dynamic_fps.impl.util.event; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWCursorEnterCallback; @@ -22,11 +22,11 @@ public class WindowObserver { public WindowObserver(long address) { this.window = address; - previousFocusCallback = GLFW.glfwSetWindowFocusCallback(this.window, this::onFocusChanged); - previousMouseCallback = GLFW.glfwSetCursorEnterCallback(this.window, this::onMouseChanged); + this.previousFocusCallback = GLFW.glfwSetWindowFocusCallback(this.window, this::onFocusChanged); + this.previousMouseCallback = GLFW.glfwSetCursorEnterCallback(this.window, this::onMouseChanged); // Vanilla doesn't use this (currently), other mods might register this callback though ... - previousIconifyCallback = GLFW.glfwSetWindowIconifyCallback(this.window, this::onIconifyChanged); + this.previousIconifyCallback = GLFW.glfwSetWindowIconifyCallback(this.window, this::onIconifyChanged); } private boolean isCurrentWindow(long address) { @@ -43,8 +43,8 @@ private void onFocusChanged(long address, boolean focused) { DynamicFPSMod.onStatusChanged(); } - if (previousFocusCallback != null) { - previousFocusCallback.invoke(address, focused); + if (this.previousFocusCallback != null) { + this.previousFocusCallback.invoke(address, focused); } } @@ -58,8 +58,8 @@ private void onMouseChanged(long address, boolean hovered) { DynamicFPSMod.onStatusChanged(); } - if (previousMouseCallback != null) { - previousMouseCallback.invoke(address, hovered); + if (this.previousMouseCallback != null) { + this.previousMouseCallback.invoke(address, hovered); } } @@ -73,8 +73,8 @@ private void onIconifyChanged(long address, boolean iconified) { DynamicFPSMod.onStatusChanged(); } - if (previousIconifyCallback != null) { - previousIconifyCallback.invoke(address, iconified); + if (this.previousIconifyCallback != null) { + this.previousIconifyCallback.invoke(address, iconified); } } } diff --git a/src/main/resources/assets/dynamic_fps/lang/de_at.json b/src/main/resources/assets/dynamic_fps/lang/de_at.json index 27f1c84e..69e747a3 100644 --- a/src/main/resources/assets/dynamic_fps/lang/de_at.json +++ b/src/main/resources/assets/dynamic_fps/lang/de_at.json @@ -1,8 +1,10 @@ { "config.dynamic_fps.title": "Dynamic FPS Konfigurieren", + "config.dynamic_fps.category.general": "Allgemein", "config.dynamic_fps.category.hovered": "Hover", "config.dynamic_fps.category.unfocused": "Unfokussiert", + "config.dynamic_fps.category.abandoned": "Untätig", "config.dynamic_fps.category.invisible": "Unsichtbar", "config.dynamic_fps.frame_rate_target": "Zielbildrate", diff --git a/src/main/resources/assets/dynamic_fps/lang/de_ch.json b/src/main/resources/assets/dynamic_fps/lang/de_ch.json index 27f1c84e..69e747a3 100644 --- a/src/main/resources/assets/dynamic_fps/lang/de_ch.json +++ b/src/main/resources/assets/dynamic_fps/lang/de_ch.json @@ -1,8 +1,10 @@ { "config.dynamic_fps.title": "Dynamic FPS Konfigurieren", + "config.dynamic_fps.category.general": "Allgemein", "config.dynamic_fps.category.hovered": "Hover", "config.dynamic_fps.category.unfocused": "Unfokussiert", + "config.dynamic_fps.category.abandoned": "Untätig", "config.dynamic_fps.category.invisible": "Unsichtbar", "config.dynamic_fps.frame_rate_target": "Zielbildrate", diff --git a/src/main/resources/assets/dynamic_fps/lang/de_de.json b/src/main/resources/assets/dynamic_fps/lang/de_de.json index 27f1c84e..69e747a3 100644 --- a/src/main/resources/assets/dynamic_fps/lang/de_de.json +++ b/src/main/resources/assets/dynamic_fps/lang/de_de.json @@ -1,8 +1,10 @@ { "config.dynamic_fps.title": "Dynamic FPS Konfigurieren", + "config.dynamic_fps.category.general": "Allgemein", "config.dynamic_fps.category.hovered": "Hover", "config.dynamic_fps.category.unfocused": "Unfokussiert", + "config.dynamic_fps.category.abandoned": "Untätig", "config.dynamic_fps.category.invisible": "Unsichtbar", "config.dynamic_fps.frame_rate_target": "Zielbildrate", diff --git a/src/main/resources/assets/dynamic_fps/lang/en_us.json b/src/main/resources/assets/dynamic_fps/lang/en_us.json index 19694d7a..db9cc49c 100644 --- a/src/main/resources/assets/dynamic_fps/lang/en_us.json +++ b/src/main/resources/assets/dynamic_fps/lang/en_us.json @@ -1,10 +1,18 @@ { "config.dynamic_fps.title": "Configure Dynamic FPS", + "config.dynamic_fps.category.general": "General", "config.dynamic_fps.category.hovered": "Hovered", "config.dynamic_fps.category.unfocused": "Unfocused", + "config.dynamic_fps.category.abandoned": "Idle", "config.dynamic_fps.category.invisible": "Invisible", + "config.dynamic_fps.disabled": "Disabled", + "config.dynamic_fps.minutes": "%d Minute(s)", + + "config.dynamic_fps.idle_time": "Idle time", + "config.dynamic_fps.idle_time_tooltip": "Minutes without input until Dynamic FPS activates the Idle state while the game is focused.", + "config.dynamic_fps.frame_rate_target": "Frame Rate Target", "config.dynamic_fps.volume_multiplier": "Volume Multiplier", diff --git a/src/main/resources/assets/dynamic_fps/lang/et_ee.json b/src/main/resources/assets/dynamic_fps/lang/et_ee.json index 8cbaa4b5..3b8eb698 100644 --- a/src/main/resources/assets/dynamic_fps/lang/et_ee.json +++ b/src/main/resources/assets/dynamic_fps/lang/et_ee.json @@ -1,6 +1,8 @@ { "config.dynamic_fps.title": "Dynamic FPS seadistus", + "config.dynamic_fps.category.general": "Üldine", + "key.dynamic_fps.toggle_forced": "Sunni vähendatud ks (lüliti)", "key.dynamic_fps.toggle_disabled": "Keela Dynamic FPS (lüliti)", "gui.dynamic_fps.hud.reducing": "Dynamic FPS: sunnin vähendatud kaadrisagedust", diff --git a/src/main/resources/assets/dynamic_fps/lang/fr_ca.json b/src/main/resources/assets/dynamic_fps/lang/fr_ca.json index e38ac54a..8b41381f 100644 --- a/src/main/resources/assets/dynamic_fps/lang/fr_ca.json +++ b/src/main/resources/assets/dynamic_fps/lang/fr_ca.json @@ -1,6 +1,7 @@ { "config.dynamic_fps.title": "Configuration de Dynamic FPS", + "config.dynamic_fps.category.general": "Général", "config.dynamic_fps.category.hovered": "Survolé", "config.dynamic_fps.category.unfocused": "Non-focalisé", "config.dynamic_fps.category.invisible": "Invisible", diff --git a/src/main/resources/assets/dynamic_fps/lang/fr_fr.json b/src/main/resources/assets/dynamic_fps/lang/fr_fr.json index e38ac54a..8b41381f 100644 --- a/src/main/resources/assets/dynamic_fps/lang/fr_fr.json +++ b/src/main/resources/assets/dynamic_fps/lang/fr_fr.json @@ -1,6 +1,7 @@ { "config.dynamic_fps.title": "Configuration de Dynamic FPS", + "config.dynamic_fps.category.general": "Général", "config.dynamic_fps.category.hovered": "Survolé", "config.dynamic_fps.category.unfocused": "Non-focalisé", "config.dynamic_fps.category.invisible": "Invisible", diff --git a/src/main/resources/assets/dynamic_fps/lang/it_it.json b/src/main/resources/assets/dynamic_fps/lang/it_it.json index 49910133..9258e51a 100644 --- a/src/main/resources/assets/dynamic_fps/lang/it_it.json +++ b/src/main/resources/assets/dynamic_fps/lang/it_it.json @@ -1,6 +1,7 @@ { "config.dynamic_fps.title": "Configura Dynamic FPS", + "config.dynamic_fps.category.general": "Generale", "config.dynamic_fps.category.hovered": "Sopra il cursore", "config.dynamic_fps.category.unfocused": "Non in primo piano", "config.dynamic_fps.category.invisible": "Invisibile", @@ -18,4 +19,4 @@ "gui.dynamic_fps.hud.disabled": "Dynamic FPS disabilitato", "modmenu.descriptionTranslation.dynamic_fps": "Regola dinamicamente il FPS in modo che Minecraft non consumi risorse in background." -} \ No newline at end of file +} diff --git a/src/main/resources/assets/dynamic_fps/lang/ko_kr.json b/src/main/resources/assets/dynamic_fps/lang/ko_kr.json index a10cc222..1ecf817e 100644 --- a/src/main/resources/assets/dynamic_fps/lang/ko_kr.json +++ b/src/main/resources/assets/dynamic_fps/lang/ko_kr.json @@ -1,6 +1,8 @@ { "config.dynamic_fps.title": "Dynamic FPS 구성", + "config.dynamic_fps.category.general": "일반", + "key.dynamic_fps.toggle_forced": "강제로 FPS 감소 (토글)", "key.dynamic_fps.toggle_disabled": "Dynamic FPS 비활성화 (Toggle)", "gui.dynamic_fps.hud.reducing": "Dynamic FPS: 강제로 FPS 감소중", diff --git a/src/main/resources/assets/dynamic_fps/lang/pl_pl.json b/src/main/resources/assets/dynamic_fps/lang/pl_pl.json index 272a6d77..87c9cf88 100644 --- a/src/main/resources/assets/dynamic_fps/lang/pl_pl.json +++ b/src/main/resources/assets/dynamic_fps/lang/pl_pl.json @@ -1,6 +1,8 @@ { "config.dynamic_fps.title": "Skonfiguruj Dynamic FPS", + "config.dynamic_fps.category.general": "Ogólne", + "key.dynamic_fps.toggle_forced": "Wymuś obniżenie FPS (przełącznik)", "key.dynamic_fps.toggle_disabled": "Wyłącz Dynamic FPS (przełącznik)", "gui.dynamic_fps.hud.reducing": "Dynamic FPS: wymuszono obniżenie FPS", diff --git a/src/main/resources/assets/dynamic_fps/lang/pt_br.json b/src/main/resources/assets/dynamic_fps/lang/pt_br.json index 21475c90..ff7ce802 100644 --- a/src/main/resources/assets/dynamic_fps/lang/pt_br.json +++ b/src/main/resources/assets/dynamic_fps/lang/pt_br.json @@ -1,6 +1,8 @@ { "config.dynamic_fps.title": "Definições do Dynamic FPS", + "config.dynamic_fps.category.general": "Geral", + "key.dynamic_fps.toggle_forced": "Forçar redução da taxa de quadros (alternável)", "key.dynamic_fps.toggle_disabled": "Desativar o Disable Dynamic (alternável)", "gui.dynamic_fps.hud.reducing": "Dynamic FPS: forçando a redução da taxa de quadros", diff --git a/src/main/resources/assets/dynamic_fps/lang/pt_pt.json b/src/main/resources/assets/dynamic_fps/lang/pt_pt.json index 21475c90..ff7ce802 100644 --- a/src/main/resources/assets/dynamic_fps/lang/pt_pt.json +++ b/src/main/resources/assets/dynamic_fps/lang/pt_pt.json @@ -1,6 +1,8 @@ { "config.dynamic_fps.title": "Definições do Dynamic FPS", + "config.dynamic_fps.category.general": "Geral", + "key.dynamic_fps.toggle_forced": "Forçar redução da taxa de quadros (alternável)", "key.dynamic_fps.toggle_disabled": "Desativar o Disable Dynamic (alternável)", "gui.dynamic_fps.hud.reducing": "Dynamic FPS: forçando a redução da taxa de quadros", diff --git a/src/main/resources/assets/dynamic_fps/lang/ru_ru.json b/src/main/resources/assets/dynamic_fps/lang/ru_ru.json index ae9046cb..84dc6c83 100644 --- a/src/main/resources/assets/dynamic_fps/lang/ru_ru.json +++ b/src/main/resources/assets/dynamic_fps/lang/ru_ru.json @@ -1,6 +1,7 @@ { "config.dynamic_fps.title": "Настройки Dynamic FPS", + "config.dynamic_fps.category.general": "Основные", "config.dynamic_fps.category.hovered": "Наведён", "config.dynamic_fps.category.unfocused": "Расфокусирован", "config.dynamic_fps.category.invisible": "Свёрнут", diff --git a/src/main/resources/assets/dynamic_fps/lang/sv_se.json b/src/main/resources/assets/dynamic_fps/lang/sv_se.json index 9ccc4c14..dbaf2303 100644 --- a/src/main/resources/assets/dynamic_fps/lang/sv_se.json +++ b/src/main/resources/assets/dynamic_fps/lang/sv_se.json @@ -1,9 +1,10 @@ { "config.dynamic_fps.title": "Konfigurera Dynamic FPS", + "config.dynamic_fps.category.general": "Allmänt", "config.dynamic_fps.category.hovered": "Hovrande", "config.dynamic_fps.category.unfocused": "Ofokuserad", - "config.dynamic_fps.category.invisible": "Osynlig", + "config.dynamic_fps.category.invisible": "Osynlig", "config.dynamic_fps.frame_rate_target": "Bildfrekvensmål", "config.dynamic_fps.frame_rate_target_description": "Ställ in bildfrekvensmålet till -1 för att stänga av bildfrekvens reducering.", @@ -11,7 +12,7 @@ "config.dynamic_fps.graphics_state": "Bildskärmsinställningar", "config.dynamic_fps.show_toasts": "Visa Toasts", "config.dynamic_fps.run_garbage_collector": "Starta GC", - + "key.dynamic_fps.toggle_forced": "Tvinga Reducerad FPS (Växla)", "key.dynamic_fps.toggle_disabled": "Inaktivera Dynamic FPS (Växla)", "gui.dynamic_fps.hud.reducing": "Dynamic FPS: Tvingar Reducerad BPS", diff --git a/src/main/resources/assets/dynamic_fps/lang/tr_tr.json b/src/main/resources/assets/dynamic_fps/lang/tr_tr.json index e80f5f30..c4bbf9ea 100644 --- a/src/main/resources/assets/dynamic_fps/lang/tr_tr.json +++ b/src/main/resources/assets/dynamic_fps/lang/tr_tr.json @@ -1,6 +1,8 @@ { "config.dynamic_fps.title": "Dinamik FPS Ayarları", + "config.dynamic_fps.category.general": "Genel", + "key.dynamic_fps.toggle_forced": "Zorla Azaltılmış FPS (Aç / kapat)", "key.dynamic_fps.toggle_disabled": "Dinamik FPS'yi Devre Dışı Bırak (Aç / kapat)", "gui.dynamic_fps.hud.reducing": "Dinamik FPS: Azaltılmış FPS Zorlanıyor", diff --git a/src/main/resources/assets/dynamic_fps/lang/uk_ua.json b/src/main/resources/assets/dynamic_fps/lang/uk_ua.json index 0d04bd75..0f86ec37 100644 --- a/src/main/resources/assets/dynamic_fps/lang/uk_ua.json +++ b/src/main/resources/assets/dynamic_fps/lang/uk_ua.json @@ -1,6 +1,8 @@ { "config.dynamic_fps.title": "Параметри Dynamic FPS", + "config.dynamic_fps.category.general": "Загальні", + "key.dynamic_fps.toggle_forced": "Примусово знижувати частоту кадрів (перемикання)", "key.dynamic_fps.toggle_disabled": "Вимкнути Dynamic FPS (перемикання)", "gui.dynamic_fps.hud.reducing": "Dynamic FPS: Увімкнено примусове зниження частоти кадрів", diff --git a/src/main/resources/assets/dynamic_fps/lang/vi_vn.json b/src/main/resources/assets/dynamic_fps/lang/vi_vn.json index 991dd378..52bd7bd1 100644 --- a/src/main/resources/assets/dynamic_fps/lang/vi_vn.json +++ b/src/main/resources/assets/dynamic_fps/lang/vi_vn.json @@ -1,6 +1,8 @@ { "config.dynamic_fps.title": "Định cấu hình Dynamic FPS", + "config.dynamic_fps.category.general": "Chung", + "key.dynamic_fps.toggle_forced": "Buộc giảm FPS (Đổi)", "key.dynamic_fps.toggle_disabled": "Vô hiệu hoá Dynamic FPS (Đổi)", "gui.dynamic_fps.hud.reducing": "Dynamic FPS: Buộc giảm FPS", diff --git a/src/main/resources/assets/dynamic_fps/lang/zh_cn.json b/src/main/resources/assets/dynamic_fps/lang/zh_cn.json index f54e3669..ff72aca7 100644 --- a/src/main/resources/assets/dynamic_fps/lang/zh_cn.json +++ b/src/main/resources/assets/dynamic_fps/lang/zh_cn.json @@ -1,6 +1,7 @@ { "config.dynamic_fps.title": "Dynamic FPS 动态帧率配置", + "config.dynamic_fps.category.general": "预设", "config.dynamic_fps.category.hovered": "悬停", "config.dynamic_fps.category.unfocused": "失焦", "config.dynamic_fps.category.invisible": "不可见", diff --git a/src/main/resources/assets/dynamic_fps/lang/zh_tw.json b/src/main/resources/assets/dynamic_fps/lang/zh_tw.json index 96938631..bf83dd9d 100644 --- a/src/main/resources/assets/dynamic_fps/lang/zh_tw.json +++ b/src/main/resources/assets/dynamic_fps/lang/zh_tw.json @@ -1,6 +1,7 @@ { "config.dynamic_fps.title": "設定 Dynamic FPS", + "config.dynamic_fps.category.general": "一般", "config.dynamic_fps.category.hovered": "聚焦時", "config.dynamic_fps.category.unfocused": "未聚焦時", "config.dynamic_fps.category.invisible": "不可見時", diff --git a/src/main/resources/dynamic_fps.accesswidener b/src/main/resources/dynamic_fps.accesswidener new file mode 100644 index 00000000..aa31532c --- /dev/null +++ b/src/main/resources/dynamic_fps.accesswidener @@ -0,0 +1,3 @@ +accessWidener v2 named + +accessible field com/mojang/blaze3d/platform/Window window J diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index c7103d0c..58c9eca4 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -31,6 +31,7 @@ "mixins": [ "dynamic_fps.mixins.json" ], + "accessWidener": "dynamic_fps.accesswidener", "depends": { "minecraft": ">=1.20.0", From 16db344e23c97b946f894341765307ecb482f12a Mon Sep 17 00:00:00 2001 From: LostLuma Date: Sat, 16 Dec 2023 12:51:45 +0100 Subject: [PATCH 15/19] Simplify SoundEngine access --- .../java/dynamic_fps/impl/DynamicFPSMod.java | 2 +- .../impl/mixin/SoundEngineMixin.java | 4 ++-- .../impl/mixin/SoundManagerMixin.java | 21 ------------------- ...SoundManager.java => DuckSoundEngine.java} | 2 +- src/main/resources/dynamic_fps.accesswidener | 1 + src/main/resources/dynamic_fps.mixins.json | 1 - src/main/resources/fabric.mod.json | 3 +-- 7 files changed, 6 insertions(+), 28 deletions(-) delete mode 100644 src/main/java/dynamic_fps/impl/mixin/SoundManagerMixin.java rename src/main/java/dynamic_fps/impl/util/duck/{DuckSoundManager.java => DuckSoundEngine.java} (87%) diff --git a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java index 899e7f80..f0469799 100644 --- a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java +++ b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java @@ -196,7 +196,7 @@ public static void handleStateChange(PowerState previous, PowerState current) { for (var source : SoundSource.values()) { if (before.volumeMultiplier(source) != config.volumeMultiplier(source)) { - minecraft.getSoundManager().dynamic_fps$updateVolume(source); + minecraft.getSoundManager().soundEngine.dynamic_fps$updateVolume(source); } } diff --git a/src/main/java/dynamic_fps/impl/mixin/SoundEngineMixin.java b/src/main/java/dynamic_fps/impl/mixin/SoundEngineMixin.java index ce1e3880..6eb53dd7 100644 --- a/src/main/java/dynamic_fps/impl/mixin/SoundEngineMixin.java +++ b/src/main/java/dynamic_fps/impl/mixin/SoundEngineMixin.java @@ -14,14 +14,14 @@ import com.mojang.blaze3d.audio.Listener; import dynamic_fps.impl.DynamicFPSMod; -import dynamic_fps.impl.util.duck.DuckSoundManager; +import dynamic_fps.impl.util.duck.DuckSoundEngine; import net.minecraft.client.resources.sounds.SoundInstance; import net.minecraft.client.sounds.ChannelAccess; import net.minecraft.client.sounds.SoundEngine; import net.minecraft.sounds.SoundSource; @Mixin(SoundEngine.class) -public class SoundEngineMixin implements DuckSoundManager { +public class SoundEngineMixin implements DuckSoundEngine { @Shadow private boolean loaded; diff --git a/src/main/java/dynamic_fps/impl/mixin/SoundManagerMixin.java b/src/main/java/dynamic_fps/impl/mixin/SoundManagerMixin.java deleted file mode 100644 index f087c307..00000000 --- a/src/main/java/dynamic_fps/impl/mixin/SoundManagerMixin.java +++ /dev/null @@ -1,21 +0,0 @@ -package dynamic_fps.impl.mixin; - -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; - -import dynamic_fps.impl.util.duck.DuckSoundManager; -import net.minecraft.client.sounds.SoundEngine; -import net.minecraft.client.sounds.SoundManager; -import net.minecraft.sounds.SoundSource; - -@Mixin(SoundManager.class) -public class SoundManagerMixin implements DuckSoundManager { - @Shadow - @Final - private SoundEngine soundEngine; - - public void dynamic_fps$updateVolume(SoundSource source) { - this.soundEngine.dynamic_fps$updateVolume(source); - } -} diff --git a/src/main/java/dynamic_fps/impl/util/duck/DuckSoundManager.java b/src/main/java/dynamic_fps/impl/util/duck/DuckSoundEngine.java similarity index 87% rename from src/main/java/dynamic_fps/impl/util/duck/DuckSoundManager.java rename to src/main/java/dynamic_fps/impl/util/duck/DuckSoundEngine.java index 41c8f00a..3b305d41 100644 --- a/src/main/java/dynamic_fps/impl/util/duck/DuckSoundManager.java +++ b/src/main/java/dynamic_fps/impl/util/duck/DuckSoundEngine.java @@ -2,7 +2,7 @@ import net.minecraft.sounds.SoundSource; -public interface DuckSoundManager { +public interface DuckSoundEngine { public default void dynamic_fps$updateVolume(SoundSource source) { throw new RuntimeException("No implementation for dynamic_fps$updateVolume was found."); } diff --git a/src/main/resources/dynamic_fps.accesswidener b/src/main/resources/dynamic_fps.accesswidener index aa31532c..0298cddf 100644 --- a/src/main/resources/dynamic_fps.accesswidener +++ b/src/main/resources/dynamic_fps.accesswidener @@ -1,3 +1,4 @@ accessWidener v2 named accessible field com/mojang/blaze3d/platform/Window window J +accessible field net/minecraft/client/sounds/SoundManager soundEngine Lnet/minecraft/client/sounds/SoundEngine; diff --git a/src/main/resources/dynamic_fps.mixins.json b/src/main/resources/dynamic_fps.mixins.json index 6b661ccc..da9d1887 100644 --- a/src/main/resources/dynamic_fps.mixins.json +++ b/src/main/resources/dynamic_fps.mixins.json @@ -11,7 +11,6 @@ "MinecraftMixin", "ScreenMixin", "SoundEngineMixin", - "SoundManagerMixin", "StatsScreenMixin", "ToastComponentMixin", "WindowMixin", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 58c9eca4..1021b674 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -46,8 +46,7 @@ "loom:injected_interfaces": { "net/minecraft/class_437": ["dynamic_fps/impl/util/duck/DuckScreen"], "net/minecraft/class_425": ["dynamic_fps/impl/util/duck/DuckSplashOverlay"], - "net/minecraft/class_1140": ["dynamic_fps/impl/util/duck/DuckSoundManager"], - "net/minecraft/class_1144": ["dynamic_fps/impl/util/duck/DuckSoundManager"] + "net/minecraft/class_1140": ["dynamic_fps/impl/util/duck/DuckSoundEngine"] }, "dynamic_fps": { "optimized_screens": { From 09c16cac6c73c5d705f074acfe637f70231483a7 Mon Sep 17 00:00:00 2001 From: EuropaYou <53128780+EuropaYou@users.noreply.github.com> Date: Sat, 16 Dec 2023 14:53:00 +0300 Subject: [PATCH 16/19] Update tr_tr.json --- .../assets/dynamic_fps/lang/tr_tr.json | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/resources/assets/dynamic_fps/lang/tr_tr.json b/src/main/resources/assets/dynamic_fps/lang/tr_tr.json index c4bbf9ea..53bc3857 100644 --- a/src/main/resources/assets/dynamic_fps/lang/tr_tr.json +++ b/src/main/resources/assets/dynamic_fps/lang/tr_tr.json @@ -2,9 +2,36 @@ "config.dynamic_fps.title": "Dinamik FPS Ayarları", "config.dynamic_fps.category.general": "Genel", + "config.dynamic_fps.category.hovered": "Üzerinde Durulan", + "config.dynamic_fps.category.unfocused": "Odaklanmamış", + "config.dynamic_fps.category.abandoned": "Boşta", + "config.dynamic_fps.category.invisible": "Görünmez", + "config.dynamic_fps.disabled": "Devre Dışı", + "config.dynamic_fps.minutes": "%d Dakika", + + "config.dynamic_fps.idle_time": "Boşta Geçen Süre", + "config.dynamic_fps.idle_time_tooltip": "Oyun odaklıyken giriş olmadan geçen dakikalar boyunca Dinamik FPS'nin Boşta durumunu etkinleştirmesi.", + + "config.dynamic_fps.frame_rate_target": "Kare Hızı Hedefi", + "config.dynamic_fps.volume_multiplier": "Ses Çarpanı", + + "config.dynamic_fps.graphics_state": "Grafik Seçenekleri", + "config.dynamic_fps.graphics_state_default": "Varsayılan", + "config.dynamic_fps.graphics_state_reduced": "Azaltılmış", + "config.dynamic_fps.graphics_state_minimal": "Minimal", + "config.dynamic_fps.graphics_state_minimal_tooltip": "Minimal grafikler dünyanın yeniden yüklenmesine neden olur!!", + + "config.dynamic_fps.show_toasts": "Bilgi Pencerelerini Göster", + "config.dynamic_fps.show_toasts_tooltip": "Bilgi pencerelerini göstermeye devam edip etmemeye veya ertelemeye karar verir", + + "config.dynamic_fps.run_garbage_collector": "Garbage Collector Çalıştır", + "config.dynamic_fps.run_garbage_collector_tooltip": "Bu duruma geçişte kullanılmayan belleği serbest bırak", + "key.dynamic_fps.toggle_forced": "Zorla Azaltılmış FPS (Aç / kapat)", "key.dynamic_fps.toggle_disabled": "Dinamik FPS'yi Devre Dışı Bırak (Aç / kapat)", "gui.dynamic_fps.hud.reducing": "Dinamik FPS: Azaltılmış FPS Zorlanıyor", - "gui.dynamic_fps.hud.disabled": "Dinamik FPS Devre Dışı Bırakıldı" + "gui.dynamic_fps.hud.disabled": "Dinamik FPS Devre Dışı Bırakıldı", + + "modmenu.descriptionTranslation.dynamic_fps": "Minecraft'in arka planda kaynakları tüketmemesi için FPS'yi dinamik olarak ayarlar." } From 03e4cb967945203637f7897c31717108aceca643 Mon Sep 17 00:00:00 2001 From: LostLuma Date: Sat, 16 Dec 2023 13:56:57 +0100 Subject: [PATCH 17/19] Fix idling not working within screens --- src/main/java/dynamic_fps/impl/DynamicFPSMod.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java index f0469799..8147c666 100644 --- a/src/main/java/dynamic_fps/impl/DynamicFPSMod.java +++ b/src/main/java/dynamic_fps/impl/DynamicFPSMod.java @@ -167,9 +167,7 @@ private static void registerTickEvent() { tickEventRegistered = true; - // I assume world ticks only happen 20 times a second - // Instead of whatever amount of FPS we get at the moment? - ClientTickEvents.START_WORLD_TICK.register((minecraft) -> { + ClientTickEvents.START_CLIENT_TICK.register((minecraft) -> { var idle = isIdle(); if (idle != wasIdle) { From a3c0fb0579eefd4e1ee90f9afae6ec65c6667992 Mon Sep 17 00:00:00 2001 From: Tarek <13603398+Taarek@users.noreply.github.com> Date: Sat, 16 Dec 2023 16:03:06 +0100 Subject: [PATCH 18/19] Added new Swedish translations --- .../assets/dynamic_fps/lang/sv_se.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/resources/assets/dynamic_fps/lang/sv_se.json b/src/main/resources/assets/dynamic_fps/lang/sv_se.json index dbaf2303..ad5fb933 100644 --- a/src/main/resources/assets/dynamic_fps/lang/sv_se.json +++ b/src/main/resources/assets/dynamic_fps/lang/sv_se.json @@ -4,14 +4,29 @@ "config.dynamic_fps.category.general": "Allmänt", "config.dynamic_fps.category.hovered": "Hovrande", "config.dynamic_fps.category.unfocused": "Ofokuserad", + "config.dynamic_fps.category.abandoned": "Inaktiv", "config.dynamic_fps.category.invisible": "Osynlig", + "config.dynamic_fps.disabled": "Inaktiverad", + "config.dynamic_fps.minutes": "%d Minut(er)", + + "config.dynamic_fps.idle_time": "Inaktivitetstid", + "config.dynamic_fps.idle_time_tooltip": "Minuter utan inmatning tills Dynamic FPS aktiverar Inaktivetesläget medans spelet är fokuserat.", + "config.dynamic_fps.frame_rate_target": "Bildfrekvensmål", - "config.dynamic_fps.frame_rate_target_description": "Ställ in bildfrekvensmålet till -1 för att stänga av bildfrekvens reducering.", "config.dynamic_fps.volume_multiplier": "Volym Multiplikator", + "config.dynamic_fps.graphics_state": "Bildskärmsinställningar", + "config.dynamic_fps.graphics_state_default": "Standard", + "config.dynamic_fps.graphics_state_reduced": "Reducerad", + "config.dynamic_fps.graphics_state_minimal": "Minimalt", + "config.dynamic_fps.graphics_state_minimal_tooltip": "Minimal grafik orsakar att världen laddar om!", + "config.dynamic_fps.show_toasts": "Visa Toasts", + "config.dynamic_fps.show_toasts_tooltip": "Om du vill fortsätta visa eller fördröja Toast aviseringar", + "config.dynamic_fps.run_garbage_collector": "Starta GC", + "config.dynamic_fps.run_garbage_collector_tooltip": "Frigör oanvänt minne när du växlar till denna status", "key.dynamic_fps.toggle_forced": "Tvinga Reducerad FPS (Växla)", "key.dynamic_fps.toggle_disabled": "Inaktivera Dynamic FPS (Växla)", From fcdee8e58f4993dafce819587a1301c5fec5183b Mon Sep 17 00:00:00 2001 From: LostLuma Date: Sat, 16 Dec 2023 16:57:22 +0100 Subject: [PATCH 19/19] Bump mod version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c8d8a2d1..b066072f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,6 @@ org.gradle.parallel = true org.gradle.jvmargs = -Xmx1G # Mod Properties -mod_version = 3.2.1 +mod_version = 3.3.0 maven_group = juliand665 archives_base_name = dynamic-fps