Skip to content

Commit

Permalink
Add idle detection and mode
Browse files Browse the repository at this point in the history
  • Loading branch information
LostLuma committed Dec 16, 2023
1 parent 0fe7582 commit f9a941a
Show file tree
Hide file tree
Showing 30 changed files with 285 additions and 34 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Expand Up @@ -26,6 +26,10 @@ dependencies {
modApi libs.cloth.config
}

loom {
accessWidenerPath = file("src/main/resources/dynamic_fps.accesswidener")
}

processResources {
inputs.property "version", generateVersion()

Expand Down
55 changes: 53 additions & 2 deletions src/main/java/dynamic_fps/impl/DynamicFPSMod.java
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -65,6 +72,7 @@ public void onInitializeClient() {
toggleForcedKeyBinding.register();
toggleDisabledKeyBinding.register();

registerTickEvent();
HudRenderCallback.EVENT.register(new HudInfoRenderer());
}

Expand All @@ -78,6 +86,11 @@ public static void onStatusChanged() {
checkForStateChanges();
}

public static void onConfigChanged() {
modConfig.save();
registerTickEvent();
}

public static PowerState powerState() {
return state;
}
Expand All @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()) {
Expand Down
13 changes: 9 additions & 4 deletions src/main/java/dynamic_fps/impl/PowerState.java
Expand Up @@ -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);
Expand Down
29 changes: 28 additions & 1 deletion src/main/java/dynamic_fps/impl/compat/ClothConfig.java
Expand Up @@ -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;
Expand Down Expand Up @@ -107,6 +124,16 @@ public static Screen genConfigScreen(Screen parent) {
return builder.build();
}

private static Component idleTimeMessage(int value) {
if (value == 0) {
return localized("config", "idle_time_disabled");
} if (value == 1) {
return localized("config", "minute");
} 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) {
Expand Down
26 changes: 24 additions & 2 deletions src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java
Expand Up @@ -25,6 +25,7 @@
import net.minecraft.sounds.SoundSource;

public final class DynamicFPSConfig {
private int idleTime; // Seconds
private Map<PowerState, Config> configs;

private static final Path CONFIGS = FabricLoader.getInstance().getConfigDir();
Expand All @@ -33,10 +34,12 @@ public final class DynamicFPSConfig {
private static final Codec<Map<PowerState, Config>> STATES_CODEC = Codec.unboundedMap(new EnumCodec<>(PowerState.values()), Config.CODEC);

private static final Codec<DynamicFPSConfig> 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<PowerState, Config> configs) {
private DynamicFPSConfig(int abandonTime, Map<PowerState, Config> configs) {
this.idleTime = abandonTime;
this.configs = new EnumMap<>(configs);

for (var state : PowerState.values()) {
Expand All @@ -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<PowerState, Config> configs() {
return this.configs;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -119,9 +133,17 @@ private static Map<SoundSource, Float> 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, ... }
Expand Down
13 changes: 13 additions & 0 deletions 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 = "<init>", 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();
Expand Down
12 changes: 0 additions & 12 deletions 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;
Expand All @@ -14,15 +11,6 @@

@Mixin(Window.class)
public class WindowMixin {
@Shadow
@Final
private long window;

@Inject(method = "<init>", 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.
*/
Expand Down

0 comments on commit f9a941a

Please sign in to comment.