Skip to content

Commit

Permalink
Add support for setting a volume multiplier per sound category
Browse files Browse the repository at this point in the history
  • Loading branch information
LostLuma committed Dec 16, 2023
1 parent ec676e0 commit 2e95aca
Show file tree
Hide file tree
Showing 18 changed files with 293 additions and 135 deletions.
33 changes: 6 additions & 27 deletions src/main/java/dynamic_fps/impl/DynamicFPSMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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();

Expand Down
30 changes: 0 additions & 30 deletions src/main/java/dynamic_fps/impl/GraphicsState.java
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand All @@ -25,27 +18,4 @@ public enum GraphicsState {
* Reduce graphics settings to minimal values, this will reload the world!
*/
MINIMAL;

public static final Codec<GraphicsState> CODEC = new PrimitiveCodec<GraphicsState>() {
@Override
public <T> T write(DynamicOps<T> ops, GraphicsState value) {
return ops.createString(value.toString());
}

@Override
public <T> DataResult<GraphicsState> read(DynamicOps<T> 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);
}
}
25 changes: 0 additions & 25 deletions src/main/java/dynamic_fps/impl/PowerState.java
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -35,24 +28,6 @@ public enum PowerState {

public final boolean configurable;

public static final Codec<PowerState> CODEC = new PrimitiveCodec<PowerState>() {
@Override
public <T> T write(DynamicOps<T> ops, PowerState value) {
return ops.createString(value.toString().toLowerCase(Locale.ROOT));
}

@Override
public <T> DataResult<PowerState> read(DynamicOps<T> 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;
}
Expand Down
32 changes: 20 additions & 12 deletions src/main/java/dynamic_fps/impl/compat/ClothConfig.java
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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(
Expand Down
29 changes: 19 additions & 10 deletions src/main/java/dynamic_fps/impl/config/Config.java
Original file line number Diff line number Diff line change
@@ -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<SoundSource, Float> volumeMultipliers;
private GraphicsState graphicsState;
private boolean showToasts;
private boolean runGarbageCollector;

public static final Codec<Config> 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<SoundSource, Float> 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;
Expand All @@ -38,12 +43,16 @@ public void setFrameRateTarget(int value) {
this.frameRateTarget = value;
}

public float volumeMultiplier() {
return this.volumeMultiplier;
public Map<SoundSource, Float> 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() {
Expand Down
63 changes: 59 additions & 4 deletions src/main/java/dynamic_fps/impl/config/DynamicFPSConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,30 @@
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;

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<PowerState, Config> configs;

private static final Path CONFIGS = FabricLoader.getInstance().getConfigDir();
private static final Path CONFIG_FILE = CONFIGS.resolve(DynamicFPSMod.MOD_ID + ".json");

private static final Codec<Map<PowerState, Config>> STATES_CODEC = Codec.unboundedMap(PowerState.CODEC, Config.CODEC);
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(
STATES_CODEC.fieldOf("states").forGetter(DynamicFPSConfig::configs)
Expand Down Expand Up @@ -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);
Expand All @@ -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<SoundSource, Float> withMasterVolume(float value) {
var volumes = new HashMap<SoundSource, Float>();
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);
}
}
}
5 changes: 3 additions & 2 deletions src/main/java/dynamic_fps/impl/mixin/LoadingOverlayMixin.java
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/dynamic_fps/impl/mixin/ScreenMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading

0 comments on commit 2e95aca

Please sign in to comment.