397 lines
14 KiB
Java
397 lines
14 KiB
Java
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<SoundManager.Preparations> {
|
|
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<Map<String, SoundEventRegistration>> SOUND_EVENT_REGISTRATION_TYPE = new TypeToken<Map<String, SoundEventRegistration>>() {};
|
|
private final Map<ResourceLocation, WeighedSoundEvents> registry = Maps.<ResourceLocation, WeighedSoundEvents>newHashMap();
|
|
private final SoundEngine soundEngine;
|
|
private final Map<ResourceLocation, Resource> 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<String, SoundEventRegistration> map = GsonHelper.fromJson(GSON, reader, SOUND_EVENT_REGISTRATION_TYPE);
|
|
profiler.popPush("register");
|
|
|
|
for (Entry<String, SoundEventRegistration> 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<String> getAvailableSoundDevices() {
|
|
return this.soundEngine.getAvailableSoundDevices();
|
|
}
|
|
|
|
public ListenerTransform getListenerTransform() {
|
|
return this.soundEngine.getListenerTransform();
|
|
}
|
|
|
|
/**
|
|
* Validates a sound resource
|
|
* <p>
|
|
* @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<ResourceLocation> 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<ResourceLocation, WeighedSoundEvents> registry = Maps.<ResourceLocation, WeighedSoundEvents>newHashMap();
|
|
private Map<ResourceLocation, Resource> 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<Sound> 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<ResourceLocation, WeighedSoundEvents> soundRegistry, Map<ResourceLocation, Resource> cache, SoundEngine soundEngine) {
|
|
soundRegistry.clear();
|
|
cache.clear();
|
|
cache.putAll(this.soundCache);
|
|
|
|
for (Entry<ResourceLocation, WeighedSoundEvents> entry : this.registry.entrySet()) {
|
|
soundRegistry.put((ResourceLocation)entry.getKey(), (WeighedSoundEvents)entry.getValue());
|
|
((WeighedSoundEvents)entry.getValue()).preloadIfRequired(soundEngine);
|
|
}
|
|
}
|
|
}
|
|
}
|