diff --git a/README.md b/README.md index df49e31..81aa87c 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,15 @@ [![CurseForge](https://img.shields.io/curseforge/dt/874633?logo=curseforge&label=CurseForge)](https://www.curseforge.com/minecraft/mc-mods/soundsbegone) [![](https://dcbadge.limes.pink/api/server/dvg3tcQCPW)](https://discord.gg/dvg3tcQCPW) -Inspired by my own misophonia, Sounds Be Gone keeps Minecraft playable when specific noises are overwhelming. The mod passively records every sound you have heard in the past minute and lets you mute any of them permanently with a couple of clicks. Everything is client-side, so your preferences affect only your game. +Inspired by my own misophonia, Sounds Be Gone keeps Minecraft playable when specific noises are overwhelming. The mod passively records every sound you have heard in the past minute and lets you reduce the volume or mute any of them permanently with a couple of clicks. Everything is client-side, so your preferences affect only your game. ### _Fabric is supported on releases below 1.20. NeoForge builds are available for Minecraft 1.20.4 and newer._ ## Why players use Sounds Be Gone -- Instantly silence any sound event (vanilla or modded) without memorizing filenames. -- The "recently played" list removes guesswork—scroll, select, and mute. -- Keeps an audit trail so you can easily unmute later when you change your mind. +- Reduce the volume or instantly silence any sound event (vanilla or modded) without memorizing filenames. +- The "recently played" list removes guesswork—scroll, select, and adjust. +- Keeps an audit trail so you can easily readjust volumes later when you change your mind. - Built for sensory accessibility: predictable UI, reversible actions, and zero server requirements. ## Infrequent Sounds! @@ -31,13 +31,13 @@ Use **Infrequent** for sounds where silence would remove useful feedback, but no 1. When you hear an unwanted sound, press `B` (default keybind) to open the Sound Sanctuary. 2. Pick the sound(s) from the "Played in the last 60 seconds" list. -3. Toggle them off to mute permanently; the change saves immediately to your local profile. +3. Use the volume slider to turn it down, or set it to 0% to mute it permanently; the change saves immediately. ### Minecraft 1.19 and below 1. Press `ESC`, choose `MODS`, then select **Sounds Be Gone**. 2. Click the settings cog in the top-right corner to open the configuration screen. -3. Use the list of recent sounds to disable whatever is bothering you. +3. Use the list of recent sounds to turn down or disable whatever is bothering you. ## Installation @@ -58,7 +58,6 @@ Use **Infrequent** for sounds where silence would remove useful feedback, but no ## Configuration tips - Rebind the `B` key if it clashes with another mod under `Options → Controls → Key Binds → Sounds Be Gone`. -- Sounds stay muted until you manually re-enable them, even if you swap worlds or restart the client. - Mod Menu makes it easy to open the config without jumping into a world, especially on older Minecraft versions. - Keep Cloth Config up to date—new releases occasionally add UX improvements for navigating large sound lists. @@ -83,4 +82,4 @@ Thanks to all the supporters who make this project possible! - + \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/gg/meza/soundsbegone/Config.java b/src/main/java/gg/meza/soundsbegone/Config.java index 99e6fde..0341b2a 100644 --- a/src/main/java/gg/meza/soundsbegone/Config.java +++ b/src/main/java/gg/meza/soundsbegone/Config.java @@ -13,7 +13,9 @@ import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static gg.meza.soundsbegone.SoundsBeGoneConfig.LOGGER; import static gg.meza.soundsbegone.SoundsBeGoneConfig.MOD_ID; @@ -41,23 +43,24 @@ public void toggleTelemetry(boolean toValue) { configData.telemetry = toValue; } - public void disableSound(String sound) { - resetSound(sound); - configData.sounds.add(sound); - } - - public void reduceSound(String sound) { - resetSound(sound); - configData.infrequent.add(sound); + public void setInfrequent(String sound, boolean value) { + if (value) { + configData.infrequent.add(sound); + } else { + configData.infrequent.remove(sound); + } } - public void resetSound(String sound) { - configData.infrequent.remove(sound); - configData.sounds.remove(sound); + public int getSoundVolume(String sound) { + return configData.soundVolumes.getOrDefault(sound, 100); } - public boolean isSoundDisabled(String sound) { - return configData.sounds.contains(sound); + public void setSoundVolume(String sound, int volume) { + if (volume == 100) { + configData.soundVolumes.remove(sound); + } else { + configData.soundVolumes.put(sound, volume); + } } public boolean isSoundInfrequent(String sound) { @@ -65,21 +68,19 @@ public boolean isSoundInfrequent(String sound) { } public Set disabledSounds() { - return configData.sounds; + return configData.soundVolumes.entrySet().stream() + .filter(e -> e.getValue() == 0) + .map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()); } - public Set infrequentSounds() { - return configData.infrequent; + public Set reducedSounds() { + return configData.soundVolumes.entrySet().stream() + .filter(e -> e.getValue() > 0 && e.getValue() < 100) + .map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()); } - public SoundState getSoundState(String sound) { - if (isSoundDisabled(sound)) { - return SoundState.DISABLED; - } else if (isSoundInfrequent(sound)) { - return SoundState.INFREQUENT; - } else { - return SoundState.ENABLED; - } + public Set infrequentSounds() { + return Set.copyOf(configData.infrequent); } public double getFrequencyPercentage() { @@ -98,14 +99,16 @@ public void initConfig() { configData = GSON.fromJson(reader, ConfigData.class); reader.close(); } catch (IOException | JsonParseException e) { - SoundsBeGoneConfig.LOGGER.error("Cause: " + e.getCause().getClass().getSimpleName()); - if (e.getCause().getClass() == IllegalStateException.class) { + Throwable cause = e.getCause(); + LOGGER.error("Failed to load config", e); + if (cause != null && cause.getClass() == IllegalStateException.class) { convertConfig(configPath); return; } throw new SerializationException(e); } } + migrateLegacySounds(); } public void saveConfig() { @@ -142,7 +145,11 @@ private void convertConfig(Path configPath) { }.getType(); Set disabledSounds = GSON.fromJson(reader, setType); reader.close(); - this.configData.sounds = disabledSounds; + if (disabledSounds != null) { + for (String sound : disabledSounds) { + this.configData.soundVolumes.putIfAbsent(sound, 0); + } + } BufferedWriter writer = Files.newBufferedWriter(configPath); GSON.toJson(configData, writer); @@ -152,4 +159,15 @@ private void convertConfig(Path configPath) { } } + private void migrateLegacySounds() { + if (configData.sounds != null && !configData.sounds.isEmpty()) { + SoundsBeGoneConfig.LOGGER.info("Migrating legacy disabled sounds to volume map"); + for (String sound : configData.sounds) { + configData.soundVolumes.putIfAbsent(sound, 0); + } + configData.sounds = null; + saveConfig(); + } + } + } diff --git a/src/main/java/gg/meza/soundsbegone/ConfigData.java b/src/main/java/gg/meza/soundsbegone/ConfigData.java index 43732e0..0461134 100644 --- a/src/main/java/gg/meza/soundsbegone/ConfigData.java +++ b/src/main/java/gg/meza/soundsbegone/ConfigData.java @@ -1,12 +1,15 @@ package gg.meza.soundsbegone; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; public class ConfigData { public String lastVersionSeen = ""; public boolean telemetry = true; public double frequencyPercentage = 10; - public Set sounds = new HashSet<>(); + public Map soundVolumes = new HashMap<>(); + public Set sounds = new HashSet<>(); // Legacy field, migrated to soundVolumes on load public Set infrequent = new HashSet<>(); } diff --git a/src/main/java/gg/meza/soundsbegone/SoundState.java b/src/main/java/gg/meza/soundsbegone/SoundState.java deleted file mode 100644 index 9c995ad..0000000 --- a/src/main/java/gg/meza/soundsbegone/SoundState.java +++ /dev/null @@ -1,7 +0,0 @@ -package gg.meza.soundsbegone; - -public enum SoundState { - ENABLED, - DISABLED, - INFREQUENT -} diff --git a/src/main/java/gg/meza/soundsbegone/client/ConfigScreen.java b/src/main/java/gg/meza/soundsbegone/client/ConfigScreen.java index 95fe884..7a981da 100644 --- a/src/main/java/gg/meza/soundsbegone/client/ConfigScreen.java +++ b/src/main/java/gg/meza/soundsbegone/client/ConfigScreen.java @@ -1,6 +1,5 @@ package gg.meza.soundsbegone.client; -import gg.meza.soundsbegone.SoundState; import gg.meza.soundsbegone.SoundsBeGoneConfig; import gg.meza.supporters.clothconfig.SupportCategory; @@ -12,6 +11,9 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; +import java.util.HashSet; +import java.util.Set; + public class ConfigScreen { public static Screen getConfigScreen(Screen parent) { ConfigBuilder builder = ConfigBuilder.create() @@ -21,12 +23,11 @@ public static Screen getConfigScreen(Screen parent) { SoundsBeGoneClient.config.saveConfig(); }); ConfigCategory general = builder.getOrCreateCategory(Component.translatable("soundsbegone.config.category.latest")); - ConfigCategory disabled = builder.getOrCreateCategory(Component.translatable("soundsbegone.config.category.disabled")); - ConfigCategory infrequent = builder.getOrCreateCategory(Component.translatable("soundsbegone.config.category.infrequent")); + ConfigCategory configured = builder.getOrCreateCategory(Component.translatable("soundsbegone.config.category.configured")); ConfigCategory settings = builder.getOrCreateCategory(Component.translatable("soundsbegone.config.category.settings")); ConfigEntryBuilder entryBuilder = builder.entryBuilder(); - infrequent.addEntry( + settings.addEntry( entryBuilder.startDoubleField(Component.translatable("soundsbegone.config.infrequent.frequency.title"), SoundsBeGoneClient.config.getFrequencyPercentage()) .setMin(0.001) .setMax(99.999) @@ -62,42 +63,64 @@ public static Screen getConfigScreen(Screen parent) { SoundsBeGoneClient.SoundMap .keySet() .stream() - .filter(s -> !SoundsBeGoneClient.config.isSoundDisabled(s)) + .filter(s -> SoundsBeGoneClient.config.getSoundVolume(s) == 100 && !SoundsBeGoneClient.config.isSoundInfrequent(s)) .sorted() .forEach((key) -> general.addEntry(constructOption(entryBuilder, key))); - SoundsBeGoneClient.config.infrequentSounds().stream().sorted().forEach((key) -> infrequent.addEntry(constructOption(entryBuilder, key))); - SoundsBeGoneClient.config.disabledSounds().stream().sorted().forEach((key) -> disabled.addEntry(constructOption(entryBuilder, key))); + Set allModifiedSounds = new HashSet<>(); + allModifiedSounds.addAll(SoundsBeGoneClient.config.disabledSounds()); + allModifiedSounds.addAll(SoundsBeGoneClient.config.reducedSounds()); + allModifiedSounds.addAll(SoundsBeGoneClient.config.infrequentSounds()); + + allModifiedSounds.stream() + .sorted() + .forEach((key) -> configured.addEntry(constructOption(entryBuilder, key))); + SupportCategory.add(builder, entryBuilder); return builder.build(); } private static AbstractConfigListEntry constructOption(ConfigEntryBuilder builder, String key) { - return builder - .startEnumSelector(Component.translatable(key), SoundState.class, SoundsBeGoneClient.config.getSoundState(key)) - .setDefaultValue(SoundsBeGoneClient.config.getSoundState(key)) - .setEnumNameProvider((state) -> Component.translatable("soundsbegone.config.sound.state." + state.name().toLowerCase())) + int currentVolume = SoundsBeGoneClient.config.getSoundVolume(key); + boolean isInfrequent = SoundsBeGoneClient.config.isSoundInfrequent(key); + + SubCategoryBuilder sub = builder.startSubCategory(Component.translatable(key)); + sub.setExpanded(false); + + sub.add(builder.startIntSlider( + Component.translatable("soundsbegone.config.sound.volume"), + currentVolume, 0, 100) + .setDefaultValue(100) + .setSaveConsumer(newVolume -> { + int oldVolume = SoundsBeGoneClient.config.getSoundVolume(key); + if (oldVolume == 0 && newVolume > 0) { + SoundsBeGoneClient.telemetry.unmutedSound(key); + } else if (oldVolume > 0 && newVolume == 0) { + SoundsBeGoneClient.telemetry.mutedSound(key); + } + SoundsBeGoneClient.config.setSoundVolume(key, newVolume); + }) + .setTextGetter(value -> Component.literal(value + "%")) + .build() + ); + + sub.add(builder.startBooleanToggle( + Component.translatable("soundsbegone.config.sound.infrequent"), + isInfrequent) + .setDefaultValue(false) .setSaveConsumer(newValue -> { - boolean hasChanged = SoundsBeGoneClient.config.getSoundState(key) != newValue; - if (!hasChanged) return; - switch (newValue) { - case DISABLED -> { - SoundsBeGoneConfig.LOGGER.info("Disabling sound: " + key); - SoundsBeGoneClient.config.disableSound(key); - SoundsBeGoneClient.telemetry.mutedSound(key); - } - case INFREQUENT -> { - SoundsBeGoneConfig.LOGGER.info("Setting sound to infrequent: " + key); - SoundsBeGoneClient.config.reduceSound(key); - SoundsBeGoneClient.telemetry.setInfrequentSound(key, SoundsBeGoneClient.config.getFrequencyPercentage()); - } - case ENABLED -> { - SoundsBeGoneConfig.LOGGER.info("Enabling sound: " + key); - SoundsBeGoneClient.config.resetSound(key); - SoundsBeGoneClient.telemetry.unmutedSound(key); - } + boolean wasInfrequent = SoundsBeGoneClient.config.isSoundInfrequent(key); + SoundsBeGoneClient.config.setInfrequent(key, newValue); + if (!wasInfrequent && newValue) { + SoundsBeGoneConfig.LOGGER.info("Setting sound to infrequent: {}", key); + SoundsBeGoneClient.telemetry.setInfrequentSound(key, SoundsBeGoneClient.config.getFrequencyPercentage()); + } else if (wasInfrequent && !newValue) { + SoundsBeGoneConfig.LOGGER.info("Removing sound from infrequent: {}", key); } }) - .build(); + .build() + ); + + return sub.build(); } -} +} \ No newline at end of file diff --git a/src/main/java/gg/meza/soundsbegone/client/mixin/AbstractSoundInstanceMixin.java b/src/main/java/gg/meza/soundsbegone/client/mixin/AbstractSoundInstanceMixin.java index cd3f3e2..5010f58 100644 --- a/src/main/java/gg/meza/soundsbegone/client/mixin/AbstractSoundInstanceMixin.java +++ b/src/main/java/gg/meza/soundsbegone/client/mixin/AbstractSoundInstanceMixin.java @@ -7,6 +7,7 @@ import net.minecraft.resources.Identifier; import net.minecraft.util.RandomSource; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -17,36 +18,62 @@ @Mixin(AbstractSoundInstance.class) public class AbstractSoundInstanceMixin { + @Shadow protected float volume; + @Unique private final RandomSource random = RandomSource.create(); + @Unique + private boolean soundsBeGone$telemetryReported; @Inject( method = "getVolume()F", at = @At("HEAD"), cancellable = true) private void getVolume(CallbackInfoReturnable cir) { + String soundId = soundsBeGone$sound().toString(); - if (SoundsBeGoneClient.config.isSoundDisabled(soundsBeGone$sound().toString())) { - SoundsBeGoneConfig.LOGGER.debug("Disabling the sound: {}", soundsBeGone$sound()); - SoundsBeGoneClient.telemetry.blockedSound(soundsBeGone$sound().toString()); - cir.setReturnValue(0.0F); - cir.cancel(); - } else if (SoundsBeGoneClient.config.isSoundInfrequent(soundsBeGone$sound().toString())) { - double value = random.nextDouble()*100; - double weightedChance = soundEmissionRegulator.weightedChance(soundsBeGone$sound().toString(), SoundsBeGoneClient.config.getFrequencyPercentage()); + if (SoundsBeGoneClient.config.isSoundInfrequent(soundId)) { + double value = random.nextDouble() * 100; + double weightedChance = soundEmissionRegulator.weightedChance(soundId, SoundsBeGoneClient.config.getFrequencyPercentage()); boolean playSound = value < weightedChance; if (!playSound) { - SoundsBeGoneConfig.LOGGER.debug("Reducing the sound: {}", soundsBeGone$sound()); - SoundsBeGoneClient.telemetry.blockedSound(soundsBeGone$sound().toString()); + SoundsBeGoneConfig.LOGGER.debug("Reducing the sound: {}", soundId); + if (!soundsBeGone$telemetryReported) { + SoundsBeGoneClient.telemetry.blockedSound(soundId); + soundsBeGone$telemetryReported = true; + } cir.setReturnValue(0.0F); cir.cancel(); + track(soundId); + return; } } - if (Minecraft.getInstance().level != null) { - SoundsBeGoneClient.SoundMap.put(soundsBeGone$sound().toString(), new java.util.Date()); + int volumePercent = SoundsBeGoneClient.config.getSoundVolume(soundId); + if (volumePercent == 0) { + if (!soundsBeGone$telemetryReported) { + SoundsBeGoneClient.telemetry.blockedSound(soundId); + soundsBeGone$telemetryReported = true; + } + cir.setReturnValue(0.0F); + cir.cancel(); + track(soundId); + return; + } else if (volumePercent != 100) { + track(soundId); + float newVolume = this.volume * (volumePercent / 100.0f); + cir.setReturnValue(newVolume); + cir.cancel(); } + track(soundId); + } + + @Unique + private void track(String soundId) { + if (Minecraft.getInstance().level != null) { + SoundsBeGoneClient.SoundMap.put(soundId, new java.util.Date()); + } } @Unique diff --git a/src/main/resources/assets/soundsbegone/lang/en_us.json b/src/main/resources/assets/soundsbegone/lang/en_us.json index bdc8216..9cc635c 100644 --- a/src/main/resources/assets/soundsbegone/lang/en_us.json +++ b/src/main/resources/assets/soundsbegone/lang/en_us.json @@ -13,15 +13,13 @@ "soundsbegone.config.title" : "Sounds Be Gone Config", "soundsbegone.config.category.latest" : "Played in the last 60 seconds", - "soundsbegone.config.category.disabled" : "Disabled sounds", - "soundsbegone.config.category.infrequent" : "Infrequent sounds", + "soundsbegone.config.category.configured" : "Configured sounds", "soundsbegone.config.category.settings" : "Settings", "soundsbegone.config.infrequent.frequency.title" : "Frequency of infrequent sounds (percentage)", "soundsbegone.config.infrequent.frequency.tooltip" : "What percentage of infrequent sounds should be played? (0.001 - 99.999)", - "soundsbegone.config.sound.state.enabled" : "Enabled", - "soundsbegone.config.sound.state.disabled" : "Disabled", - "soundsbegone.config.sound.state.infrequent" : "Infrequent", + "soundsbegone.config.sound.volume" : "Volume", + "soundsbegone.config.sound.infrequent" : "Infrequent", "soundsbegone.config.telemetry.switch" : "Enable telemetry", "soundsbegone.config.telemetry.description" : "Enabling telemetry will send anonymous data to help us understand how we can make things better. We DO NOT send any data that would identify you.", @@ -37,4 +35,4 @@ "soundsbegone.config.telemetry.data.os": "Operating system used", "soundsbegone.config.telemetry.data.time": "Local time", "soundsbegone.config.telemetry.data.language": "Minecraft language" -} +} \ No newline at end of file diff --git a/versions/dependencies/1.21.11.properties b/versions/dependencies/1.21.11.properties index 92680ac..f9f04c3 100644 --- a/versions/dependencies/1.21.11.properties +++ b/versions/dependencies/1.21.11.properties @@ -6,6 +6,6 @@ fabric_version=0.139.5+1.21.11 neoforge_version=21.11.3-beta modmenu_version=17.0.0-alpha.1 -cloth_version=21.11.151 +cloth_version=21.11.153 meza_core_version=1.2.2 posthog_version=1.2.0 diff --git a/versions/dependencies/1.21.properties b/versions/dependencies/1.21.properties index ab701f5..afd2c5d 100644 --- a/versions/dependencies/1.21.properties +++ b/versions/dependencies/1.21.properties @@ -6,6 +6,6 @@ fabric_version=0.102.0+1.21 neoforge_version=21.0.42-beta modmenu_version=11.0.1 -cloth_version=15.0.127 +cloth_version=15.0.140 posthog_version=1.2.0 meza_core_version=1.1.2