diff --git a/patches/net/minecraft/client/sounds/MusicManager.java.patch b/patches/net/minecraft/client/sounds/MusicManager.java.patch
new file mode 100644
index 0000000000..9772e14aed
--- /dev/null
+++ b/patches/net/minecraft/client/sounds/MusicManager.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/client/sounds/MusicManager.java
++++ b/net/minecraft/client/sounds/MusicManager.java
+@@ -24,7 +_,15 @@
+ }
+
+ public void tick() {
+- Music music = this.minecraft.getSituationalMusic();
++ Music music = net.neoforged.neoforge.client.ClientHooks.selectMusic(this.minecraft.getSituationalMusic(), this.currentMusic);
++ if (music == null) {
++ if (this.currentMusic != null) {
++ this.stopPlaying();
++ }
++ this.nextSongDelay = 0;
++ return;
++ }
++
+ if (this.currentMusic != null) {
+ if (!music.getEvent().value().getLocation().equals(this.currentMusic.getLocation()) && music.replaceCurrentMusic()) {
+ this.minecraft.getSoundManager().stop(this.currentMusic);
diff --git a/src/main/java/net/neoforged/neoforge/client/ClientHooks.java b/src/main/java/net/neoforged/neoforge/client/ClientHooks.java
index ddef24010a..767063d3d1 100644
--- a/src/main/java/net/neoforged/neoforge/client/ClientHooks.java
+++ b/src/main/java/net/neoforged/neoforge/client/ClientHooks.java
@@ -102,6 +102,7 @@
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ReloadableResourceManager;
+import net.minecraft.sounds.Music;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.InteractionHand;
@@ -164,6 +165,7 @@
import net.neoforged.neoforge.client.event.RenderTooltipEvent;
import net.neoforged.neoforge.client.event.ScreenEvent;
import net.neoforged.neoforge.client.event.ScreenshotEvent;
+import net.neoforged.neoforge.client.event.SelectMusicEvent;
import net.neoforged.neoforge.client.event.TextureAtlasStitchedEvent;
import net.neoforged.neoforge.client.event.ToastAddEvent;
import net.neoforged.neoforge.client.event.ViewportEvent;
@@ -392,6 +394,13 @@ public static SoundInstance playSound(SoundEngine manager, SoundInstance sound)
return e.getSound();
}
+ @Nullable
+ public static Music selectMusic(Music situational, @Nullable SoundInstance playing) {
+ SelectMusicEvent e = new SelectMusicEvent(situational, playing);
+ NeoForge.EVENT_BUS.post(e);
+ return e.getMusic();
+ }
+
public static void drawScreen(Screen screen, GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
guiGraphics.pose().pushPose();
guiLayers.forEach(layer -> {
diff --git a/src/main/java/net/neoforged/neoforge/client/event/SelectMusicEvent.java b/src/main/java/net/neoforged/neoforge/client/event/SelectMusicEvent.java
new file mode 100644
index 0000000000..e0d20a712b
--- /dev/null
+++ b/src/main/java/net/neoforged/neoforge/client/event/SelectMusicEvent.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) NeoForged and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.neoforged.neoforge.client.event;
+
+import net.minecraft.client.resources.sounds.SoundInstance;
+import net.minecraft.sounds.Music;
+import net.neoforged.bus.api.Event;
+import net.neoforged.bus.api.ICancellableEvent;
+import net.neoforged.bus.api.SubscribeEvent;
+import net.neoforged.fml.LogicalSide;
+import net.neoforged.neoforge.common.NeoForge;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Fired when the {@link net.minecraft.client.sounds.MusicManager} checks what situational music should be used. This fires before the music begins playing.
+ * If the music is set to {@code null} by a modder, it will cancel any music that was already playing.
+ *
+ * Note that the higher priority you make your event listener, the earlier the music will be set.
+ * Because of this, if you want your music to take precedence over others (perhaps you want to have seperate nighttime music for a biome for instance) then you may want it to have a lower priority.
+ *
+ * To make your music instantly play rather than waiting for the playing music to stop, set the music to one that {@linkplain Music#replaceCurrentMusic() is set to replace the current music.}
+ *
+ * Higher priorities would likely be better suited for biome-based or dimension-based musics, whereas lower priority is likely good for specific structures or situations.
+ *
+ * This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}.
+ * If the event is canceled, then whatever the latest music set was will be used as the music.
+ *
+ * This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus},
+ * only on the {@linkplain LogicalSide#CLIENT logical client}.
+ *
+ */
+public class SelectMusicEvent extends Event implements ICancellableEvent {
+ private @Nullable Music music;
+ private final Music originalMusic;
+ private final @Nullable SoundInstance playingMusic;
+
+ public SelectMusicEvent(Music music, @Nullable SoundInstance playingMusic) {
+ this.music = music;
+ this.originalMusic = music;
+ this.playingMusic = playingMusic;
+ }
+
+ /**
+ * {@return the original situational music that was selected}
+ */
+ public Music getOriginalMusic() {
+ return originalMusic;
+ }
+
+ /**
+ * {@return the current track that the {@link net.minecraft.client.sounds.MusicManager} is playing, or {@code null} if there is none}
+ */
+ @Nullable
+ public SoundInstance getPlayingMusic() {
+ return playingMusic;
+ }
+
+ /**
+ * {@return the Music to be played, or {@code null} if any playing music should be cancelled}
+ */
+ @Nullable
+ public Music getMusic() {
+ return music;
+ }
+
+ /**
+ * Changes the situational music. If this is set to {@code null}, any currently playing music will be cancelled.
+ * If this was {@code null} but on the next tick isn't, the music given will be immediately played.
+ *
+ */
+ public void setMusic(@Nullable Music newMusic) {
+ this.music = newMusic;
+ }
+
+ /**
+ * Sets the music and then cancels the event so that other listeners will not be invoked.
+ * Note that listeners using {@link SubscribeEvent#receiveCanceled()} will still be able to override this, but by default they will not
+ */
+ public void overrideMusic(@Nullable Music newMusic) {
+ this.music = newMusic;
+ this.setCanceled(true);
+ }
+}