package net.minecraft.client.sounds; import com.google.common.collect.Maps; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.mojang.blaze3d.audio.ListenerTransform; import com.mojang.logging.LogUtils; import java.io.IOException; import java.io.Reader; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.SharedConstants; import net.minecraft.client.Camera; import net.minecraft.client.Options; import net.minecraft.client.resources.sounds.Sound; import net.minecraft.client.resources.sounds.SoundEventRegistration; import net.minecraft.client.resources.sounds.SoundEventRegistrationSerializer; import net.minecraft.client.resources.sounds.SoundInstance; import net.minecraft.client.resources.sounds.TickableSoundInstance; import net.minecraft.client.resources.sounds.Sound.Type; import net.minecraft.client.sounds.SoundManager.Preparations.1; import net.minecraft.core.RegistryAccess; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentUtils; import net.minecraft.network.chat.Component.SerializerAdapter; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.ResourceProvider; import net.minecraft.server.packs.resources.SimplePreparableReloadListener; import net.minecraft.sounds.SoundSource; import net.minecraft.util.GsonHelper; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.util.profiling.Zone; import net.minecraft.util.valueproviders.ConstantFloat; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; /** * The SoundManager class is responsible for managing sound events and playing sounds. * It handles sound event registrations, caching of sound resources, and sound playback. */ @Environment(EnvType.CLIENT) public class SoundManager extends SimplePreparableReloadListener { public static final ResourceLocation EMPTY_SOUND_LOCATION = ResourceLocation.withDefaultNamespace("empty"); public static final Sound EMPTY_SOUND = new Sound(EMPTY_SOUND_LOCATION, ConstantFloat.of(1.0F), ConstantFloat.of(1.0F), 1, Type.FILE, false, false, 16); public static final ResourceLocation INTENTIONALLY_EMPTY_SOUND_LOCATION = ResourceLocation.withDefaultNamespace("intentionally_empty"); public static final WeighedSoundEvents INTENTIONALLY_EMPTY_SOUND_EVENT = new WeighedSoundEvents(INTENTIONALLY_EMPTY_SOUND_LOCATION, null); public static final Sound INTENTIONALLY_EMPTY_SOUND = new Sound( INTENTIONALLY_EMPTY_SOUND_LOCATION, ConstantFloat.of(1.0F), ConstantFloat.of(1.0F), 1, Type.FILE, false, false, 16 ); static final Logger LOGGER = LogUtils.getLogger(); private static final String SOUNDS_PATH = "sounds.json"; private static final Gson GSON = new GsonBuilder() .registerTypeHierarchyAdapter(Component.class, new SerializerAdapter(RegistryAccess.EMPTY)) .registerTypeAdapter(SoundEventRegistration.class, new SoundEventRegistrationSerializer()) .create(); private static final TypeToken> SOUND_EVENT_REGISTRATION_TYPE = new TypeToken>() {}; private final Map registry = Maps.newHashMap(); private final SoundEngine soundEngine; private final Map soundCache = new HashMap(); public SoundManager(Options options) { this.soundEngine = new SoundEngine(this, options, ResourceProvider.fromMap(this.soundCache)); } /** * Performs any reloading that can be done off-thread, such as file IO * * @param resourceManager The resource manager in-charge of loading sound files * @param profiler The sound profiler */ protected SoundManager.Preparations prepare(ResourceManager resourceManager, ProfilerFiller profiler) { SoundManager.Preparations preparations = new SoundManager.Preparations(); try (Zone zone = profiler.zone("list")) { preparations.listResources(resourceManager); } for (String string : resourceManager.getNamespaces()) { try (Zone zone2 = profiler.zone(string)) { for (Resource resource : resourceManager.getResourceStack(ResourceLocation.fromNamespaceAndPath(string, "sounds.json"))) { profiler.push(resource.sourcePackId()); try { Reader reader = resource.openAsReader(); try { profiler.push("parse"); Map map = GsonHelper.fromJson(GSON, reader, SOUND_EVENT_REGISTRATION_TYPE); profiler.popPush("register"); for (Entry entry : map.entrySet()) { preparations.handleRegistration(ResourceLocation.fromNamespaceAndPath(string, (String)entry.getKey()), (SoundEventRegistration)entry.getValue()); } profiler.pop(); } catch (Throwable var18) { if (reader != null) { try { reader.close(); } catch (Throwable var16) { var18.addSuppressed(var16); } } throw var18; } if (reader != null) { reader.close(); } } catch (RuntimeException var19) { LOGGER.warn("Invalid {} in resourcepack: '{}'", "sounds.json", resource.sourcePackId(), var19); } profiler.pop(); } } catch (IOException var21) { } } return preparations; } /** * Applies the prepared sound event registrations and caches to the sound manager. * * @param object The prepared sound event registrations and caches * @param resourceManager The resource manager * @param profiler The profiler */ protected void apply(SoundManager.Preparations object, ResourceManager resourceManager, ProfilerFiller profiler) { object.apply(this.registry, this.soundCache, this.soundEngine); if (SharedConstants.IS_RUNNING_IN_IDE) { for (ResourceLocation resourceLocation : this.registry.keySet()) { WeighedSoundEvents weighedSoundEvents = (WeighedSoundEvents)this.registry.get(resourceLocation); if (!ComponentUtils.isTranslationResolvable(weighedSoundEvents.getSubtitle()) && BuiltInRegistries.SOUND_EVENT.containsKey(resourceLocation)) { LOGGER.error("Missing subtitle {} for sound event: {}", weighedSoundEvents.getSubtitle(), resourceLocation); } } } if (LOGGER.isDebugEnabled()) { for (ResourceLocation resourceLocationx : this.registry.keySet()) { if (!BuiltInRegistries.SOUND_EVENT.containsKey(resourceLocationx)) { LOGGER.debug("Not having sound event for: {}", resourceLocationx); } } } this.soundEngine.reload(); } /** * Retrieves a list of available sound devices. */ public List getAvailableSoundDevices() { return this.soundEngine.getAvailableSoundDevices(); } public ListenerTransform getListenerTransform() { return this.soundEngine.getListenerTransform(); } /** * Validates a sound resource *

* @return {@code true} if the sound resource is valid, {@code false} otherwise * * @param sound The sound to validate * @param location The location of the sound event * @param resourceProvider The resource provider */ static boolean validateSoundResource(Sound sound, ResourceLocation location, ResourceProvider resourceProvider) { ResourceLocation resourceLocation = sound.getPath(); if (resourceProvider.getResource(resourceLocation).isEmpty()) { LOGGER.warn("File {} does not exist, cannot add it to event {}", resourceLocation, location); return false; } else { return true; } } /** * {@return The sound event associated with the specific {@linkplain ResourceLocation}, or {@code null} if not found} * * @param location The location of the sound event */ @Nullable public WeighedSoundEvents getSoundEvent(ResourceLocation location) { return (WeighedSoundEvents)this.registry.get(location); } /** * {@return The collection of available sound event locations} */ public Collection getAvailableSounds() { return this.registry.keySet(); } /** * Queues a ticking sound to be played. * * @param tickableSound The ticking sound instance */ public void queueTickingSound(TickableSoundInstance tickableSound) { this.soundEngine.queueTickingSound(tickableSound); } /** * Play a sound */ public void play(SoundInstance sound) { this.soundEngine.play(sound); } /** * Plays a sound with a delay in ticks. * * @param sound The sound instance to play * @param delay The delay in ticks before playing the sound */ public void playDelayed(SoundInstance sound, int delay) { this.soundEngine.playDelayed(sound, delay); } /** * Updates the sound source position based on the active render info. * * @param activeRenderInfo The active render info */ public void updateSource(Camera activeRenderInfo) { this.soundEngine.updateSource(activeRenderInfo); } public void pause() { this.soundEngine.pause(); } public void stop() { this.soundEngine.stopAll(); } public void destroy() { this.soundEngine.destroy(); } public void emergencyShutdown() { this.soundEngine.emergencyShutdown(); } /** * Updates the sound manager's tick state. * * @param isGamePaused {@code true} if the game is paused, {@code false} otherwise */ public void tick(boolean isGamePaused) { this.soundEngine.tick(isGamePaused); } public void resume() { this.soundEngine.resume(); } /** * Updates the volume of the specified sound source category. * * @param category The sound source category * @param volume The new volume */ public void updateSourceVolume(SoundSource category, float volume) { if (category == SoundSource.MASTER && volume <= 0.0F) { this.stop(); } this.soundEngine.updateCategoryVolume(category, volume); } public void stop(SoundInstance sound) { this.soundEngine.stop(sound); } public void setVolume(SoundInstance sound, float volume) { this.soundEngine.setVolume(sound, volume); } /** * Checks if the specified sound is active (playing or scheduled to be played). * @return {@code true} if the sound is active, {@code false} otherwise * * @param sound The sound instance to check */ public boolean isActive(SoundInstance sound) { return this.soundEngine.isActive(sound); } public void addListener(SoundEventListener listener) { this.soundEngine.addEventListener(listener); } public void removeListener(SoundEventListener listener) { this.soundEngine.removeEventListener(listener); } /** * Stops all sounds associated with the specified ID and category. * * @param id The ID of the sounds to stop, or null to stop all sounds * @param category The category of the sounds to stop, or null to stop sounds from all categories */ public void stop(@Nullable ResourceLocation id, @Nullable SoundSource category) { this.soundEngine.stop(id, category); } public String getDebugString() { return this.soundEngine.getDebugString(); } public void reload() { this.soundEngine.reload(); } /** * The Preparations class represents the prepared sound event registrations and caches for applying to the sound manager. */ @Environment(EnvType.CLIENT) protected static class Preparations { final Map registry = Maps.newHashMap(); private Map soundCache = Map.of(); void listResources(ResourceManager resourceManager) { this.soundCache = Sound.SOUND_LISTER.listMatchingResources(resourceManager); } void handleRegistration(ResourceLocation location, SoundEventRegistration registration) { WeighedSoundEvents weighedSoundEvents = (WeighedSoundEvents)this.registry.get(location); boolean bl = weighedSoundEvents == null; if (bl || registration.isReplace()) { if (!bl) { SoundManager.LOGGER.debug("Replaced sound event location {}", location); } weighedSoundEvents = new WeighedSoundEvents(location, registration.getSubtitle()); this.registry.put(location, weighedSoundEvents); } ResourceProvider resourceProvider = ResourceProvider.fromMap(this.soundCache); for (Sound sound : registration.getSounds()) { ResourceLocation resourceLocation = sound.getLocation(); Weighted weighted; switch (sound.getType()) { case FILE: if (!SoundManager.validateSoundResource(sound, location, resourceProvider)) { continue; } weighted = sound; break; case SOUND_EVENT: weighted = new 1(this, resourceLocation, sound); break; default: throw new IllegalStateException("Unknown SoundEventRegistration type: " + sound.getType()); } weighedSoundEvents.addSound(weighted); } } /** * Applies the prepared sound event registrations and caches to the sound manager. * * @param soundRegistry The sound registry to apply to * @param cache The sound cache to apply to * @param soundEngine The sound engine to apply to */ public void apply(Map soundRegistry, Map cache, SoundEngine soundEngine) { soundRegistry.clear(); cache.clear(); cache.putAll(this.soundCache); for (Entry entry : this.registry.entrySet()) { soundRegistry.put((ResourceLocation)entry.getKey(), (WeighedSoundEvents)entry.getValue()); ((WeighedSoundEvents)entry.getValue()).preloadIfRequired(soundEngine); } } } }