Stop the menu music when a world loads

Minecraft.setLevel -> updateLevelInEngines stops every sound
(SoundManager.stop), so the title-screen track ends on joining and the
music manager then waits the level's music delay. Horizon only stopped
sounds when leaving a world, so music.menu kept playing in it.

- A server's client (single player's server thread, LAN, multiplayer)
  stops every sound when it makes a new level: on Login and on a
  Respawn into another level. A dimension-changing Respawn also calls
  MusicManager.stopPlaying, as handleRespawn does.
- The in-frame path does the same when the opened world's first player
  is placed.
- Vanilla's Minecraft.player exists from handleLogin on; here it is made
  at the first teleport, so getSituationalMusic counts a logged-in client
  as in the world. Otherwise the menu's 0..10-tick replace delay would
  start the first game track almost at once.
- test_music_parity.py scripts the join: a menu-like track, stopall,
  game music, then a dimension change's stop.

Fixes #2746

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
asownerytandClaude Opus 5.5 committed 2026-10-28 00:18:31 +13:00
1 parent 512545ade7
commit 985ece811b
5 files changed
+75 -2

No files matched your search

+6
View File
@@ -4651,6 +4651,7 @@ impl Game<'_> {
};
let mut player = PlayerMotion::new(self.motion, feet)?;
player.yaw = self.yaw;
let entering = self.restore_player_state;
player.load_saved_state_once(&mut self.restore_player_state, &self.level)?;
let spectator = self.mode == horizon_client::command::args::GameMode::Spectator;
player.may_fly = self.creative || spectator;
@@ -4674,6 +4675,11 @@ impl Game<'_> {
self.capture(false);
}
self.previous_position = feet;
if entering {
// The world's first player: a server's client gets it with its
// level (Minecraft.setLevel), which stops every sound.
self.stop_sounds_for_new_level();
}
self.player = Some(player);
self.pad.player_replaced();
self.sway = Default::default();
+16 -2
View File
@@ -43,14 +43,28 @@ impl Game<'_> {
};
self.audio.tick(self.pause, &ctx);
}
/// `Minecraft.setLevel` -> `updateLevelInEngines`: `SoundManager.stop()`.
/// The music manager keeps its stopped track and, on its next tick, waits
/// the level's music delay before the next one.
pub(super) fn stop_sounds_for_new_level(&mut self) {
self.ambience.clear(&mut self.audio);
self.audio.stop_all();
}
/// `getSituationalMusic` and `getMusicVolume`.
fn situational_music(&self) -> (Option<Music>, f32) {
pub(super) fn situational_music(&self) -> (Option<Music>, f32) {
let empty = BackgroundMusic::default();
// The title screen has no player (and no level for the camera probe).
// A server's client has its player from handleLogin on, before the
// first teleport makes it here: the camera is still at the origin.
let eye = if self.title.is_some() {
None
} else {
self.eye_position()
self.eye_position().or_else(|| {
self.remote
.as_ref()
.is_some_and(|r| r.logged_in)
.then_some([0.0; 3])
})
};
let (background, probe_volume) = match eye {
Some(eye) => {
+12
View File
@@ -115,6 +115,9 @@ pub(super) struct Remote {
lang: Arc<Language>,
pub(super) registries: Arc<Dynamic>,
joined: bool,
/// `handleLogin` ran: vanilla's `Minecraft.player` exists from then on,
/// while here the player is made at the first teleport.
pub(super) logged_in: bool,
status: String,
pub(super) entity_id: i32,
/// The item the player's use began with (`LivingEntity.useItem`).
@@ -440,6 +443,7 @@ impl Game<'_> {
lang,
registries: Arc::new(Dynamic::default()),
joined: false,
logged_in: false,
status: "Connecting to the server...".into(),
entity_id: 0,
use_item: None,
@@ -1018,6 +1022,8 @@ impl Game<'_> {
self.set_mode_id(spawn.game_type);
self.dim = Dim::from_id(&spawn.dimension);
if !same {
// A new ClientLevel: Minecraft.setLevel stops every sound.
self.stop_sounds_for_new_level();
let mut streaming = Streaming::new(
Arc::new(NoTerrain),
self.pack.context.clone(),
@@ -1657,6 +1663,7 @@ impl Game<'_> {
r.carried = Some(0);
r.online_mode = p.online_mode;
r.entity_id = p.player_id;
r.logged_in = true;
r.view_distance = p.chunk_radius;
}
self.streaming.radius =
@@ -1676,7 +1683,12 @@ impl Game<'_> {
// again) and with no item in use.
self.potions.effects.clear();
self.using = None;
let dimension_changed = self.dim != Dim::from_id(&p.spawn.dimension);
self.enter_remote_dimension(&p.spawn, true);
if dimension_changed {
let (music, _) = self.situational_music();
self.music.stop_playing(music.as_ref(), &mut self.audio);
}
self.screen = Screen::Playing;
if let Some(r) = self.remote.as_mut() {
r.death_message = None;
+28
View File
@@ -467,6 +467,34 @@ mod tests {
assert_eq!(m.current(&engine), Some("minecraft:music.menu"));
}
#[test]
fn a_new_level_stops_menu_music_and_waits_for_game_music() {
let (mut engine, mut backend) = (engine(), Null::default());
let mut m = MusicManager::new(2);
let menu = Music::menu();
m.start_playing(
&menu,
&mut Bound {
engine: &mut engine,
backend: &mut backend,
},
);
// Minecraft.setLevel -> SoundManager.stop(); the next tick has a player.
engine.stop_all(&mut backend);
let game = Music::game();
m.tick(
Some(&game),
1.0,
true,
&mut Bound {
engine: &mut engine,
backend: &mut backend,
},
);
assert!(m.current_instance().is_none());
assert!((12000..=24000).contains(&m.next_song_delay()));
}
#[test]
fn zero_music_volume_fades_out_and_stops() {
let (mut engine, mut backend) = (engine(), Null::default());
+13
View File
@@ -95,6 +95,19 @@ def scripted(s):
s.tick(5)
s.add('stopall')
s.tick(5)
# Joining a world: the menu-like track plays, Minecraft.setLevel stops every
# sound, and the game music (not replacing) waits its own delay; a respawn
# into another dimension then stops the music manager's track too.
s.add('start', 11, 'DEFAULT')
s.situation(end, 20, 600, 1)
s.tick(110)
s.add('stopall')
s.situation(water, 12000, 24000, 0)
s.tick(3, loading=1)
s.tick(5)
s.add('stopall')
s.add('stop')
s.tick(5)
# Events that cannot play.
for event in ODD:
s.add('start', 7, 'DEFAULT')